java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > ThreadLocal与ThreadLocalMap

Java中的ThreadLocal与ThreadLocalMap详解

作者:卑微小童

这篇文章主要介绍了Java中的ThreadLocal与ThreadLocalMap详解,ThreadLocal 是一个线程局部变量,其实的功用非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,是Java中一种较为特殊的线程绑定机制,需要的朋友可以参考下

ThreadLocal与ThreadLocalMap(jdk 1.8)

使用场景

作用

好处

主要方法

1)initialValue()

该方法会返回当前线程对应的初始值,采用了懒加载机制,当第一次get的时候才会触发,当线程第一次使用get方法的时候才会触发。除非线程先前调用了set方法,在这种情况下,不会再调用InitValue方法

2)set(T value)

未当前线程设置一个新的值

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
}

T get()

public T get() {
    Thread t = Thread.currentThread();//获取当前线程
    ThreadLocalMap map = getMap(t);//从当前线程中获取ThreadLocalMap
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);//获取Entry
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;//返回对象
        }
    }
    return setInitialValue();//如果第一次调用get,ThreadLocalMap未空或者在ThreadLocalMap中还未存储对象,则进行初始化并返回存储对象
  }

remove()

//移除线程所存储对象
public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

原理

Thread类中又这样一个ThreadLocalMap 类型成员变量threadLocals

ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocalMap 是ThreadLocal的内部类,其结构如HashMap很相似,在其内部还有个Entry,保存ThreadLocal和其保存的对象。其默认容量也为16,负载因子未2/3,并且不存在next指针,哈希冲突后采用的延后策略。具体请看最后问题栏

static class ThreadLocalMap {
    static class Entry extends WeakReference<ThreadLocal<?>> {
        Object value;
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    private static final int INITIAL_CAPACITY = 16;
    private Entry[] table;
    private int size = 0;
    private int threshold; //阈值
    private void setThreshold(int len) {
            threshold = len * 2 / 3;  //负载因子是2/3,
    }
    //......省略............
}

在这里插入图片描述

总的来说,在Thead中维护了一个Map,在Map中存储了ThreadLocal和其绑定的对象

每次获取对象都会从当前线程中获取map并将ThreadLocal传入从而获得对象

内存泄露

ThreadLocal被用作TheadLocalMap的弱引用key,这种设计也是ThreadLocal被讨论内存泄露的热点问题,因此有必要了解一下什么是弱引用。

弱引用

弱引用是用来描述非必须的对象的,但它的强度比软引用更弱,被弱引用关联的对象只能生存到下一次GC发生之前,也就是说下一次GC就会被回收。JDK1.2之后,提供了WeakReference来实现弱引用。

​ 由于ThreadLocalMap是以弱引用的方式引用着ThreadLocal,换句话说,就是ThreadLocal是被ThreadLocalMap以弱引用的方式关联着,因此如果ThreadLocal没有被ThreadLocalMap以外的对象引用,则在下一次GC的时候,ThreadLocal实例就会被回收,那么此时ThreadLocalMap里的一组KV的K就是null了,因此在没有额外操作的情况下,此处的V便不会被外部访问到,而且只要Thread实例一直存在,Thread实例就强引用着ThreadLocalMap,因此ThreadLocalMap就不会被回收,那么这里K为null的V就一直占用着内存

综上,发生内存泄露的条件是

示例

class Test{
    byte data[]=new byte[1024*1024*10];
    @Override
    protected void finalize() throws Throwable {
        System.out.println("destroy");
    }
}
public class ThreadLocalDemo {
    public ThreadLocal<Test> t = new ThreadLocal<>();
    public static void main(String[] args) {
        ThreadLocalDemo threadLocalDemo = new ThreadLocalDemo();
        Test test = new Test();
        threadLocalDemo.t.set(test);
        test = null;
        //threadLocalDemo.t.remove();
        threadLocalDemo = null;
        System.out.println("start gc");
        System.gc();
        try {
            Thread.sleep(1000L);
        }catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("end");
    }
}

输出
/*
start gc
end
*/
//当threadLocalDemo.t.remove();不被注释
/*
输出:
start gc
destroy
end
*/

当不在持有ThreadLocalDemo对象,因为thread中ThreadLoaclMap中保存有ThreadLocal的引用 ,如果ThreadLocal不是弱引用的话,ThreadLocal是不可能被gc的。而如果ThreadLocal与ThreadLocalMap之间是弱引用,如果除Thread外没有任何对象可以获得ThreadLocal,则ThreadLocal是可以为回收的

​ 当然,其仍然仍然存在一定的内存泄露,即value与TreadLcoalMap之间存在引用,当ThreadLocal被gc时value是无法被gc的,但是在ThreadLocalMap内部也存在一些机制,当map扩容或者发生hash冲突的时候会判断key键是否为null(即判断ThreadLocal对象是否被回收),如果是null,则会将value值同样设为Null.从而帮助value gc

ThreadLocal为什么经常设置为static

public class ThreadLocalDemo2 {
    public ThreadLocal<Test> t = new ThreadLocal<>();
	//public static ThreadLocal<Test> t = new ThreadLocal<>();
    public static void main(String[] args) {
        ThreadLocalDemo2 threadLocalDemo = new ThreadLocalDemo2();
        Test test = new Test();
        test.name = "xxxx";
        threadLocalDemo.t.set(test);
        ThreadLocalDemo2 threadLocalDemo2 = new ThreadLocalDemo2();
        Test test2 = new Test();
        test2.name = "yyyyy";
        threadLocalDemo2.t.set(test2);
        System.out.println(threadLocalDemo.t.get().name);
        System.out.println(threadLocalDemo2.t.get().name);
    }
}

/*
输出:
xxxx
yyyyy

static 修饰 ThreadLocal
输出:
yyyyy
yyyyy
*/

static修饰ThreadLocal后,单个线程无论创建多个对象,其ThreadLocal示例仅仅只有一个。

如果变量ThreadLocal是非static的就会造成每次生成实例都要生成不同的ThreadLocal对象,虽然这样程序不会有什么异常,但是会浪费内存资源,甚至会造成内存泄漏.。

建议

问题:

为什么ThreadLocalMap不用HashMap而是自己写了个Map

ThreadLocalMap达到扩容的阈值时会真正的扩容吗?

不会,达到阈值之后,进行一个散列表的扫描清楚过期的数据,如果清理完之后,数据量仍然达到其阈值的75%,才进行扩容

扩容源码:

private void rehash() {
    expungeStaleEntries();//清理
    if (size >= threshold - threshold / 4)//数据量仍然达到其阈值的75%,才进行扩容
        resize();
}
private void resize() {
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
    int newLen = oldLen * 2;
    Entry[] newTab = new Entry[newLen];//新建一个数组
    int count = 0;
    for (int j = 0; j < oldLen; ++j) {//遍历
        Entry e = oldTab[j];
        if (e != null) {
            ThreadLocal<?> k = e.get();
            if (k == null) {
                e.value = null; //将value设为null从而帮助GC
            } else {
                //重新进行hash
                int h = k.threadLocalHashCode & (newLen - 1);
                while (newTab[h] != null)
                    h = nextIndex(h, newLen);//采用的时自定义hash算法
                newTab[h] = e;
                count++;
            }
        }
    }
    setThreshold(newLen);//计算新的阈值
    size = count;
    table = newTab;
}

ThreadLocalMap获取Entry的流程

private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);//hash运算计算出位置
    Entry e = table[i];
    if (e != null && e.get() == key)//未发生过Hash冲突
        return e;
    else//发生过冲突
        return getEntryAfterMiss(key, i, e);//进行下一个位置的判断
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;
    while (e != null) {
        ThreadLocal<?> k = e.get();
        if (k == key)
            return e;
        if (k == null)//如果为空,则说明此位置被GC了,为过期数据
            expungeStaleEntry(i);//为了防止内存泄漏,触发一个“探测式”过期数据回收逻辑
        else
            i = nextIndex(i, len);//计算下一个位置
        e = tab[i];
    }
    return null;
}
//“探测式”过期数据回收逻辑
private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    tab[staleSlot].value = null;//将value设为空,帮助GC
    tab[staleSlot] = null;
    size--;
    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len);//根据hash和寻址算法遍历所有与当前hash相同的槽点
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        if (k == null) {//帮助GC
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            //如果key不为空,则重新进行hash,将其移动到一个更靠近其hash位置的槽点(提高下次get的效率)
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
                tab[i] = null;
                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;
}

ThreadLocalMap中set的具体流程

private void set(ThreadLocal<?> key, Object value) {
	//寻址
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
	//遍历可能的slot
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();
		//如果key相同,则替换
        if (k == key) {
            e.value = value;
            return;
        }
		//如果k为空,则进行取代算法
        if (k == null) {
            //大体就是遍历可能的槽点,直到碰到key值相同的,则将其移动到距离真实hash位置最近的点,如果没有,则再最有好的位置new一个新的Entry
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    tab[i] = new Entry(key, value);
    int sz = ++size;//判断是否达到扩容条件
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

到此这篇关于Java中的ThreadLocal与ThreadLocalMap详解的文章就介绍到这了,更多相关ThreadLocal与ThreadLocalMap内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文