聊一聊ThreadLocal是个啥东东

引言

ThreadLocal提供了线程私有的局部变量,可以在整个线程存活的过程中随时取用,从而减少了在同一个线程内多个函数或者组件之间公共变量传递的复杂度。同时,由于ThreadLocal变量的线程私有性,故不存在并发线程安全的问题。

要满足上述特性,需要解决3个问题:

  • 与线程绑定,实现私有性;
  • 提供合适的容器,方便变量的存取;
  • 设计合理的垃圾回收机制,避免内存泄露。

实现原理

为解决前2个问题,JDK最早期的设计是在ThreadLocal类内部维护一个线程安全的Map,线程的ID作为Map的key,实例对象作为Map的value,进而达到各个线程值隔离的效果。

该种中心化的模式下,通过Map的key来进行线程的绑定,而Map同时又作为变量的容器,ThreadLocal类需要处理复杂的多线程同步及变量回收问题,笨重且效率较低,所以后期JDK换了一种去中心化的方式,将管理权下放给了下面的各个线程,下面通过源码来阐述。

首先看set方法:

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

可以看到,set方法主要分为以下两个步骤:

  1. 通过getMap()获取当前线程的ThreadLocalMap(map);

  2. 通过map.set(this, value)将当前的ThreadLocal(this)作为key,T(value)作为value添加进获取到的ThreadLocalMap(map)中。

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

threadLocals为Thread类的的成员变量,初始化为null。

/* ThreadLocal values pertaining to this thread. This map is maintained
 * by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

get方法与之类似,可以看到现在的ThreadLocal是通过ThreadLocalMap来实现线程绑定和变量容器的,每个Thread均维护一个ThreadLocalMap映射表,key为ThreadLocal实例本身,而value是真正需要存储的的Object,该种模式带来3个直接的好处:

  • ThreadLocalMap与线程同生命周期,当Thread销毁之后,对应的ThreadLocalMap也随之销毁,减少内存使用量;
  • 映射表的Entity数量变小了,以前是Thread的数量,现在是ThreadLocal的数量,映射表更加轻量,故性能得到有效提高;
  • 每个线程均有自己的ThreadLocalMap,保证了并发环境下的线程安全。

ThreadLocalMap

ThreadLocalMap是ThreadLocal的静态内部类,它具有HashMap的部分特性,比如容量、扩容阈值等,内部通过Entity类来存储key和value,Entity的定义为:

static class Entry extends WeakReference> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal k, Object v) {
        super(k);
        value = v;
    }
}

Entry继承自WeakReference,通过上述源码super(k),可以知道,ThreadLocalMap是使用ThreadLocal的弱引用作为Key的。

分析到这里,可以得到下面ThreadLocal对象之间的引用结构图(其中,实线为强引用,虚线为弱引用):

聊一聊ThreadLocal是个啥东东_第1张图片

可以看到,Java堆上的ThreadLocal对象存在两个引用链,外部声明的强引用和ThreadLocalMap的key的弱引用,考虑以下情况:

删除ThreadLocal外部声明的强引用,即将ThreadLocal Ref置为null,此时只剩ThreadLocalMap的key的弱引用,当下次GC时,Java堆上的ThreadLocal对象将被回收,ThreadLocalMap上的key变为null。

需要注意的是,由于CurrentThread->Thread->ThreadLocalMap->Entity->value强引用链的存在,即使该Entity的key已经为null,value仍旧不会被回收,只有当线程被销毁之后,value的这部分内存空间才会被回收掉。

一般情况下,上述问题影响不大,但是在线程一直运行不被销毁的环境中(如线程池中的核心线程),会存在内存泄露的问题。

内存回收

由于Entity的value是与线程同生命周期的,所以当线程持续不被回收时,会造成内存泄露,为此ThreadLocalMap通过以下机制来解决上述问题.

首先看ThreadLocalMap的getEntry(ThreadLocal key)方法:

private Entry getEntry(ThreadLocal key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}

首先计算key所属entity在映射表中的位置i,如果映射表中i位置的entity非空且其key与方法传入的key相等,则直接返回entity;否则,调用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)
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

getEntryAfterMiss方法通过i = nextIndex(i, len),从table的当前位置i往后遍历table中非空的entity,若entity的key与传入的key相等,则返回该entity(可以看出ThreadLocalMap是采用线性探查的方式来解决hash冲突的,探查步长为1);若entity的key为null,则调用expungeStaleEntry(i)方法。

private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    // expunge entry at staleSlot
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    // Rehash until we encounter null
    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal k = e.get();
        if (k == null) {
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
                tab[i] = null;

                // Unlike Knuth 6.4 Algorithm R, we must scan until
                // null because multiple entries could have been stale.
                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;
}

expungeStaleEntry(i)首先清理掉当前key为null的entity:

// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;

接着进行rehash过程,其基本过程是遍历table中非null的entity,若遍历到的entity,其key为null,则清理掉该entity;若遍历到的entity的key不为null,则检查key的散列值是否属于当前位置,若不属于,将当前位置置空(tab[i] = null;),不断往后探查,若table的某位置为null,则将entity移动到该位置:

tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
    h = nextIndex(h, len);
tab[h] = e;

这里的rehash过程,其实还有一种方法,就是当key的散列值j不属于当前位置i时,将table的位置i置空(tab[i] = null;),然后将位置i原有的entity移动到table的j位置,若发生hash冲突,则往后线性探查。读者可以思考为啥JDK没有采用这种方法。

本质上讲,rehash过程一方面释放掉了key为null的entity的内存,避免了当前线程(CurrentThread)长期存在时的内存泄露问题。同时,rehash()将不属于某位置的entity移动到其他位置,避免了后面可能存在的hash冲突。

在ThreadLocalMap的set()和getEntity()方法中,均会调用expungeStaleEntry(int staleSlot)方法,但是当我们既不添加value也不获取value时,还是可能存在内存泄露的,所以最佳实践是:当ThreadLocal不需要时,手动调用其remove()方法,来删除对应的Entity及其value:

public void remove() {
     ThreadLocalMap m = getMap(Thread.currentThread());
     if (m != null)
         m.remove(this);
 }

最终调用的是ThreadLocalMap的remove函数:

private void remove(ThreadLocal key) {
    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)]) {
        if (e.get() == key) {
            e.clear();
            expungeStaleEntry(i);
            return;
        }
    }
}

若对应的key存在,remove会调用expungeStaleEntry(i)方法来删除对应的entity和value。

总结

  1. ThreadLocal是通过ThreadLocalMap来实现线程绑定和变量存储的;

  2. ThreadLocalMap的实体结构Entity继承自WeakReference,其key是对ThreadLocal对象的弱引用;

  3. ThreadLocal的get()、set(T)、remove()均会触发ThreadLocalMap的回收机制,删除无效的Entity。

小贴士:ThreadLocal的源码加注释也就722行,难度不大,设计得很是精巧,推荐读者阅读。

欢迎您扫一扫上面的二维码,关注我的微信公众号!

你可能感兴趣的:(聊一聊ThreadLocal是个啥东东)