ThreadLocal初探

什么是ThreadLocal

首先ThreadLocal是一个数据结构,它内部有点像Map,也是key-value,其中key是线程名。

先来看看下面的例子:

ThreadLocal<String> local = new ThreadLocal();
local.set("Hello");
String value = local.get();

在线程1中初始化了一个ThreadLocal对象local,并通过set方法,保存了一个值,同时在线程1中通过local.get()可以拿到之前设置的值,但是如果在线程2中,拿到的将是一个null。

这就是ThreadLocal最重要的一个特性,对于线程不安全,它并没有采取锁的思想,而是使得每个线程都有自己的副本,在上面的例子中线程1有的副本值是Hello,线程二因为没有set过所以副本值是null,从根源上避免了并发的问题。

那么它是如何做到的呢?我们很容易自然而然的以为它内部也许是维护着一个HashMap,key是线程名,value是存储的值,但实际上并不是,因为这种方案还是需要锁对HashMap保证线程安全。

接下来我们看看set(T value)和get()方法的源码:

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

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

先看set方法,可以发现数据是存在ThreadLocalMap结构中的,我们可以先不管这结构具体是什么样的,再看看后面的gewMap方法,你会发现ThreadLocalMap数据实际上不是由ThreadLocal维护的,而是存储在线程自己的手中

总的来说,ThreadLocal只是操作Thread中的ThreadLocalMap,每个Thread都有一个map,ThreadLocalMap是线程内部属性,ThreadLocalMap生命周期是和Thread一样的,不依赖于ThreadMap。

通过这样巧妙的设计,ThreadLocal就不必考虑Map并发问题,使用锁导致效率降低。

那每个线程中的ThreadLoalMap究竟是什么?

什么是ThreadLoalMap

从名字上看,可以猜到它也是一个类似HashMap的数据结构,但是在ThreadLocal中,并没实现Map接口。

在ThreadLoalMap中,也是初始化一个大小16的Entry数组,Entry对象用来保存每一个key-value键值对,只不过这里的key永远都是ThreadLocal对象。

efe59d369c834532a39ee96fcf75d862.png

这里需要注意的是,ThreadLoalMap的Entry是继承WeakReference,和HashMap很大的区别是,Entry中没有next字段,所以就不存在链表的情况了。

那么不存链表,set时候发生冲突了怎么办?

该Map使用开放地址法处理hash冲突.

每个ThreadLocal对象都有一个hash值threadLocalHashCode,每初始化一个ThreadLocal对象,hash值就增加一个固定的大小0x61c88647。

如果set插入时,发现该位置上已经存在Entry,并且已存在Entry的key值也就是threadLocalHashCod与将要插入的不一致的话,那么只能找下一个空位置。

下面是ThreadLoalMap中set的实现:

private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);

    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();
        // 要插入位置已有Entry但key相同,直接更新值
        if (k == key) {
            e.value = value;
            return;
        }
         // 第一次循环到这里时说明要插入位置还没有有Entry,插入即可
         // 第二次循环到这里时说明是我们上文说到的情况。
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}
内存泄漏

通过前面我们知道,每个线程都会有一个ThreadLoalMap,其中key对于ThreadLocal是弱引用。

这里简单说下强引用、弱引用,如果一个对象具有强引用,那么JVM宁愿抛出OOM的异常也不会回收它,但如果一个对象只有弱引用,一旦垃圾回收器发现了,就会回收这个对象。

现在可以知道,假如我们不需要用这个ThreadLocal,把它为了null,那么因为线程中的key是弱引用,所以并不会阻止ThreadLocal对象的垃圾回收,这是符合期望的结果。

但是呢,Entry的中value却不是弱引用啊,因为存在一条从current thread连接过来的强引用.

只有当前thread结束以后, current thread就不会存在栈中,强引用断开, Current Thread, Map, value将全部被GC回收。

b2547c2d801f47ad9af62fd40c0216b9.png
因此就导致了一个结果,如果线程没有结束,哪怕ThreadLoca已经被回收了,value也还存在,这就是内存泄漏。

那么线程没有结束的情况会有吗?答案是肯定的。很可能你使用线程是通过线程池,线程并不会被销毁,最终内存泄漏。

如何避免内存泄漏

既然已经发现有内存泄露的隐患,自然有应对的策略,在调用ThreadLocal的get()、set()可能会清除ThreadLocalMap中key为null的Entry对象,这样对应的value就没有GC Roots可达了,下次GC的时候就可以被回收。

另外显式的remove,肯定会删除对应的Entry对象。

ThreadLocal<String> local = new ThreadLocal();
try {
    localName.set("Hello");
    // 业务逻辑...
} finally {
    local.remove();
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值