ThreadLocal笔记

35 阅读5分钟

ThreadLocal

ThreadLocal是多线程并发质性过程中很重要的一个对象,它往往会保存一些在单个线程内不同函数之间共享的一些变量。

如,在网络请求中,我们可以把一次请求调用视作单线程,接着我们在线程内用一些函数获取用户的信息,将用户的信息贮存在ThreadLocal当中,之后其他函数需要,就直接从其中获取。

这就有一点容器的思想了。Spring中也会通过一个context保存各种Bean,如果你希望获取这个context,你只需要实现相应的aware接口,接着Spring在初始化的时候就会检测所有的Bean,拥有这个接口的Bean,Spring就会把context传给你,你可以使用它进行一些操作,也可以干脆持久化一点,把它当做自己的变量,持久化引用它。

ThreadLocal是如何为每一个线程保存一个独立的本地值呢?

我们可以把它理解成一个Map,每一个线程都拥有一个这样的Map,它的key是一个ThreadLocal实例。

换言之,我们可以创建多个ThreadLocal实例,每一个ThreadLocal实例都相当于一个key,

 伪代码
 local = new ThreadLocal() ---- map.put(local, null)
 local.set(value) ---map.put(local, value)
 local.get(value) ---map.get(local, value)
 local.remove() ---map.remove(local)

image.png 早起的ThreadLocal则是另外一种情况,Map的拥有者是一个ThreadLocal对象,Map的键则是一个Thread实例。

新版本的优势在于:

多线程的情况下,新版本的Map的体积依旧比较小。

随着Thread的销毁,Map也会随之销毁,减少了内存的消耗。

所以ThreadLocal不需要传递key,因为它自己就是一个key。

源码分析:

 public class ThreadLocal<T> {
     public ThreadLocal() {
     }
     public void set(T value) {
         set(Thread.currentThread(), value);
         if (TRACE_VTHREAD_LOCALS && Thread.currentThread().isVirtual()) {
             printStackTrace();
         }
     }
     private void set(Thread t, T value) {
         ThreadLocalMap map = getMap(t);
         if (map != null) {
             map.set(this, value);
         } else {
             createMap(t, value);
         }
     }
     void createMap(Thread t, T firstValue) {
         t.threadLocals = new ThreadLocalMap(this, firstValue);
     }
     ThreadLocalMap getMap(Thread t) {
         return t.threadLocals;
     }    
 ​

从set方法中就可以看出来,ThreadLocal类为当前Thread创建了一个Map。

我们深入这个Map的源码:

     static class ThreadLocalMap {
         static class Entry extends WeakReference<ThreadLocal<?>> {
             /** The value associated with this ThreadLocal. */
             Object value;
 ​
             Entry(ThreadLocal<?> k, Object v) {
                 super(k);
                 value = v;
             }
         }
         ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
             table = new Entry[INITIAL_CAPACITY];
             int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
             table[i] = new Entry(firstKey, firstValue);
             size = 1;
             setThreshold(INITIAL_CAPACITY);
         }
         private Entry[] table;
         ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
             table = new Entry[INITIAL_CAPACITY];
             int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
             table[i] = new Entry(firstKey, firstValue);
             size = 1;
             setThreshold(INITIAL_CAPACITY);
         }
         // 这里我们重点关注set方法。
         private void set(ThreadLocal<?> key, Object value) {
 ​
             // We don't use a fast path as with get() because it is at
             // least as common to use set() to create new entries as
             // it is to replace existing ones, in which case, a fast
             // path would fail more often than not.
 ​
             Entry[] tab = table;
             int len = tab.length;
             //这里通过`int i = key.threadLocalHashCode & (len-1);`获得当前key的初始位置,接着从这个位置遍历map,也就是一个Entry数组。
             //为什么`i`的求法会这样写,这里不了解的同学可以参考HashMap的求法,
             //因为这里的len必然是2的幂,所以`&(len-1)`相当于取模运算,在底层使用位运算效率更高。
             int i = key.threadLocalHashCode & (len-1);
 ​
             for (Entry e = tab[i];
                  e != null;
                  //接着,从这个位置遍历table,为什么使用`nextIndex(i, len)`,因为数组是从中间开始遍历的,可能需要回到开头。
                  e = tab[i = nextIndex(i, len)]) {
                 //找到了key,就可以篡位了。
                 if (e.refersTo(key)) {
                     e.value = value;
                     return;
                 }
             //找到了空位,其实这里是一个异常的槽位,之后讲。
                 if (e.refersTo(null)) {
                     replaceStaleEntry(key, value, i);
                     return;
                 }
             }
 ​
             tab[i] = new Entry(key, value);
             int sz = ++size;
             if (!cleanSomeSlots(i, sz) && sz >= threshold)
                 rehash();
         }

这里我们重点关注set方法。

这里通过int i = key.threadLocalHashCode & (len-1);获得当前key的初始位置,接着从这个位置遍历map,也就是一个Entry数组。

为什么i的求法会这样写,这里不了解的同学可以参考HashMap的求法,因为这里的len必然是2的幂,所以&(len-1)相当于取模运算,在底层使用位运算效率更高。

接着,从这个位置遍历table,为什么使用nextIndex(i, len),因为数组是从中间开始遍历的,可能需要回到开头。

为什么Entry需要extends WeakReference<ThreadLocal<?>>,换言之,为什么它的key需要一个弱引用?

 public void funA(){
     ThreadLocal local = new ThreadLocal();
     local.set(110);
     local.get();
     local = null;
     }

这里函数执行结束之后,我们的ThreadLocal依旧被内部类的Entry 的key引用着。

使用弱引用的时候,仅被弱引用引用的对象在GC发生的时候,无论内容是否够用,都会被回收。相应的,对应的key就会变成null,也就有了上面的那个情况,

 Entry e != null && e.refersTo(null)

尽管如此,这只是治标不治本,Entry的Value依旧存在着。

所以,一般规范都要求,要记得自己手动在不需要的时候清除ThreadLocal。

推荐使用static final修饰ThreadLocal对象。因为ThreadLocal本来就是为了共享而存在的。不过,这样就出现了一个问题,Entry的弱引用形同宿舍,因为只要这个这个类存在,它就会始终指向这个ThreadLocal实例,换言之,除非线程结束,或者主动释放,否则这个ThreadLocal就会永远存在。

当然,也推荐使用private封装,需要的时候调用公开的方法就好了,这样更方便定制。

如果希望在ThreadLocal只是存在更复杂的数据,可以存入一个map,或者设置更多的ThreadLocal对象,其实都差不多,看自己的使用场景。

使用建议:

可以定制线程池的afterExecute方法,

综上,使用private static final,让一个类持有一个ThreadLocal对象,但是每一个Thread都可以在这个对象中拥有自己的一个访问空间。