ThreadLocal源码及相关问题分析

Posted ouhaitao

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ThreadLocal源码及相关问题分析相关的知识,希望对你有一定的参考价值。

前言

  在高并发的环境下,当我们使用一个公共的变量时如果不加锁会出现并发问题,例如SimpleDateFormat,但是加锁的话会影响性能,对于这种情况我们可以使用ThreadLocal。ThreadLocal是将公共变量copy一份到线程私有内存中以消除并发问题,ThreadLocal是JDK内部提供的高效解决并发问题的工具类之一,本文介绍ThreadLocal的重要方法的源码实现以及相关问题的分析。

数据结构

技术图片

  由上图可以看出,在Thread中维护了一个Entry的列表,Entry存储的是公共变量的copy,这个列表是由ThreadLocal维护,每次从ThreadLocal中获取的是一个公共变量的副本,所以避免了并发问题,接下来在看实现方法之前我们先看一下上面提到的类的定义。

变量

  首先来看一下ThreadLocal重要的成员变量。

 1 public class ThreadLocal<T> 
 2     private final int threadLocalHashCode = nextHashCode(); 3 
 4     private static AtomicInteger nextHashCode =
 5         new AtomicInteger();
 6 
 7     private static final int HASH_INCREMENT = 0x61c88647;
 8 
 9     private static int nextHashCode() 
10         return nextHashCode.getAndAdd(HASH_INCREMENT);
11     
12 

  threadLocalHashCode是ThreadLocal实例的ID,其主要用于做散列(后面会讲到),从上面可以看出来threadLocalHashCode是所有ThreadLocal贡献的原子变量nextHashCode加上一个固定的HASH_INCREMENT生成的。为什么

  HASH_INCREMENT的值是0x61c88647?

  前面说了,ThreadLocal是使用散列做存储的,而这个数字可以让生成出来的ThreadLocal的ID较为均匀地分布在大小为2的N次方的数组中。 

  接下来看一下ThreadLocal中真正存储数据的Entry类。

 1 static class ThreadLocalMap 
 2 
 3         static class Entry extends WeakReference<ThreadLocal<?>> 
 4  
 5             Object value;
 6 
 7             Entry(ThreadLocal<?> k, Object v) 
 8                 super(k);
 9                 value = v;
10             
11         
12 
13         private static final int INITIAL_CAPACITY = 16;
14 
15         private Entry[] table;
16 
17         private int size = 0;
18 
19         private int threshold; 
20 

  ThreadLocalMap是ThreadLocal的内部类,Entry继承WeakReference,前面提到Entry是K-V形式,key就是WeakReference的成员变量referent,value就是Entry的value。我们可以发现,key是一个弱引用,它的生命周期到下一个GC就结束了,为什么要这样设计呢?在后面的内存泄漏中会提到。

  table是一个大小为2的N次方的数组,threshold是数组扩容的临界点(与HashMap一个作用),默认是数组的大小的2/3,数组每次扩容是将长度扩大1倍。

方法

  ThreadLocal重要的方法如下:

1 public T get(); //获取值
2 
3 public void set(T value); //设置值
4 
5 public void remove(); //删除值

  首先来看set()方法:

1 public void set(T value) 
2         Thread t = Thread.currentThread();
3         ThreadLocalMap map = getMap(t); //获取当前Thread中的ThreadLocalMap 
4         if (map != null)
5             map.set(this, value); //设置值
6         else
7             createMap(t, value); //创建一个ThreadLocalMap
8 

  在Thread类中有 ThreadLocal.ThreadLocalMap threadLocals = null; 这样的成员变量,就是用来存储当前线程设置的值,所有的ThreadLocal都是在操作这个成员变量。

  接下来看一下map.set()这个方法:

 1 private void set(ThreadLocal<?> key, Object value) 
 2         Entry[] tab = table;
 3         int len = tab.length;
 4         int i = key.threadLocalHashCode & (len - 1); // 计算hash散列
 5         
 6         // 从i开始遍历列表
 7         for (Entry e = tab[i];
 8              e != null;
 9              e = tab[i = nextIndex(i, len)]) 
10             ThreadLocal<?> k = e.get();
11             // 找到了key值
12             if (k == key) 
13                 e.value = value;
14                 return;
15             
16             // key=null表示该key值已被回收
17             if (k == null) 
18                 replaceStaleEntry(key, value, i);19                 return;
20             
21         
22         // 当遍历到一个可以插入数据的空位置时
23         tab[i] = new Entry(key, value);
24         int sz = ++size;
25         if (!cleanSomeSlots(i, sz) // 清理列表中已经被GC回收的
26             && sz >= threshold) // 判断是否需要扩容
27             rehash();// 扩容并重新计算hash散列
28 

  ThreadLocal通过遍历Entry数组,如果当前key已存在则覆盖,没有则新增,如果在遍历过程中遇到已经被GC回收的key,则会清除掉无效的key对应的值。

  从上面的循环可以看出来,ThreadLocal采用hash散列的线性探测存储,这种方式实现简单但是无法存储大量的数据,所以不建议用ThreadLocal存储大量的数据。这里还有一个问题是,nextIndex()方法是循环的获取数组下标,所以如果table满的就会导致无限循环,所以threshold的值是小于table的大小并且每次set之后都会清理一次数组的无效数据。

  我们先看一下cleanSomeSlots()方法:

 1 private boolean cleanSomeSlots(int i, int n) 
 2     boolean removed = false;
 3     Entry[] tab = table;
 4     int len = tab.length;
 5     do 
 6         i = nextIndex(i, len);
 7         Entry e = tab[i];
 8         if (e != null && e.get() == null) 
 9             n = len;
10             removed = true;
11             i = expungeStaleEntry(i); // 从下标i开始清除数组中的无效元素
12         
13      while ( (n >>>= 1) != 0); // 该循环会循环log2(n)次
14     return removed;
15 

  该方法是通过线性查找,从下标为i开始查找已经被GC清理的key对应的值,一般情况下会查找log2(n)次,expungeStaleEntry()是真正的清理方法,源码如下:

 1 private int expungeStaleEntry(int staleSlot) 
 2     Entry[] tab = table;
 3     int len = tab.length;
 4 
 5     // 清理对应数据
 6     tab[staleSlot].value = null;
 7     tab[staleSlot] = null;
 8     size--;
 9 
10     // 重新计算hash散列,直到遇见null
11     Entry e;
12     int i;
13     for (i = nextIndex(staleSlot, len);
14          (e = tab[i]) != null;
15          i = nextIndex(i, len)) 
16         ThreadLocal<?> k = e.get();
17         if (k == null) 
18             e.value = null;
19             tab[i] = null;
20             size--;
21          else 
22             int h = k.threadLocalHashCode & (len - 1);
23             if (h != i) 
24                 tab[i] = null;
25                 while (tab[h] != null)
26                     h = nextIndex(h, len);
27                 tab[h] = e;
28             
29         
30     
31     return i;
32 

  前面提到,ThreadLocal存储数据的方式是使用线性探测法,所以expungeStaleEntry()方法在清理掉一个数据之后会将该下标之后的所有非null位置重新计算一次hash散列,经过这样的操作之后,数组中的元素最终会符合hash散列的要求,如果不重新计算一次hash散列,那么最终数组结果可能不符合hash散列的要求,比如:元素a、b计算后得到存储位置冲突,通过线性探测法,最终结果是元素a在下标0,元素b在下标1,如果删除a之后,b就应该放在下标0而不是下标1上。

  我们在回过头来看看map.set()方法中的replaceStaleEntry()方法:

 1 private void replaceStaleEntry(ThreadLocal<?> key, Object value,
 2                                        int staleSlot) 
 3     Entry[] tab = table;
 4     int len = tab.length;
 5     Entry e;
 6     // slotToExpunge用来记录清除无效key的开始位置,初始值等于staleSlot,staleSlot的值是无效key的下标
 7     int slotToExpunge = staleSlot;
 8     // 向前查找无效的key
 9     for (int i = prevIndex(staleSlot, len);
10          (e = tab[i]) != null;
11          i = prevIndex(i, len))
12         if (e.get() == null)
13             slotToExpunge = i;
14 
15     // 向后查找无效的key,直到遍历到一个有效key
16     for (int i = nextIndex(staleSlot, len);
17          (e = tab[i]) != null;
18          i = nextIndex(i, len)) 
19         ThreadLocal<?> k = e.get();
20 
21         // 如果在查找的过程中找到了我们需要找的key,则将无效key与该值替换
22         if (k == key) 
23             e.value = value;
24 
25             tab[i] = tab[staleSlot];
26             tab[staleSlot] = e;
27 
28             // 判断在向前查找无效key的过程中有没有找到
29             if (slotToExpunge == staleSlot)
30                 slotToExpunge = i;
31             // 清理无效key
32             cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
33             return;
34         
35         // 如果向前没有查找到无效key并且当前key为无效key
36         if (k == null && slotToExpunge == staleSlot)
37             slotToExpunge = i;
38     
39 
40     // 将需要插入的数据插入到staleSlot位置
41     tab[staleSlot].value = null;
42     tab[staleSlot] = new Entry(key, value);
43 
44     // 如果向前查找到了无效key或者向后查找到了无效key,则清理无效key
45     if (slotToExpunge != staleSlot)
46         cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
47 

  该方法时在set数据值遍历到了一个无效key执行的方法,ThreadLocal认为,出现无效key的位置附近也会出现无效key,所以在清理该无效key的时候回查找附近连续的无效key。

  值得注意的是,代码中找到了我们需要找的key的时候,将无效key与有效key交换的原因是,清除方法expungeStaleEntry()只能够清除连续的无效的key,如果向前没有找到无效key,向后找到了无效key的情况下,会出现如下情况:

技术图片

  SS是staleSlot,SE是slotToExpunge,如果不交换,则无法清理到SS位置的无效key,而且需要找的key后面同样也可能会出现无效key,同样无法清除到。

  除了这种情况,还有一种情况是向前找到了无效key,会出现如下情况:

技术图片

  这种情况下,交换了SS跟需要找的key之后会出现无法清理SS位置的无效key的请款,所以代码中不止调用了一次expungeStaleEntry(),还会调用cleanSomeSlots(),源码在前面已经分析过了,这样就可以把SS以及之后的无效key都清掉。

  以上set()方法的源码就分析完了,接下来是get()方法,get()方法是调用ThreadLocalMap的getEntry()方法,所以我们直接分析getEntry()方法:

 1 private Entry getEntry(ThreadLocal<?> key) 
 2     int i = key.threadLocalHashCode & (table.length - 1);
 3     Entry e = table[i];
 4     if (e != null && e.get() == key)
 5         return e;
 6     else
 7         return getEntryAfterMiss(key, i, e); // hash散列之后的位置不是需要找的key,即发生了hash碰撞
 8 
 9 
10 private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) 
11     Entry[] tab = table;
12     int len = tab.length;
13 
14     while (e != null) 
15         ThreadLocal<?> k = e.get();
16         if (k == key)
17             return e;
18         if (k == null)
19             expungeStaleEntry(i); // 清理无效key
20         else
21             i = nextIndex(i, len); // 向后遍历
22         e = tab[i];
23     
24     return null;
25 

  get()方法较为简单,这里不做过多赘述,接下来是remove()方法,同样该方法是直接调用的ThreadLocalMap的remove()方法,源码如下:

 1 private void remove(ThreadLocal<?> key) 
 2     Entry[] tab = table;
 3     int len = tab.length;
 4     int i = key.threadLocalHashCode & (len-1);
 5     for (Entry e = tab[i];
 6          e != null;
 7          e = tab[i = nextIndex(i, len)]) 
 8         if (e.get() == key) 
 9             e.clear();// 将Entry的key置为null
10             expungeStaleEntry(i); // 清理该key
11             return;
12         
13     
14 

为什么会内存泄漏

  我们已经知道Entry的key是一个弱引用,当他被GC之后,那么这个Entry就是一个无效的对象,无法被外部获取到。而在Thread对象中是持有了ThreadLocalMap的引用,而ThreadLocalMap又持有了Entry数组的引用,所以该无效的Entry是无法被回收的,如果不手动的删除就会发生内存泄漏,好在ThreadLocal在set(),get()方法中都会主动去清理无效的Entry防止内存泄漏,但是依然会存在内存泄漏的风险,所以在编程中需要注意remove()不需要的值。

  这么一看,似乎ThreadLocal发生内存泄漏的原因是Entry使用了弱引用,其实使用弱引用恰恰是为了防止内存泄漏。试想,如果一个线程的生命周期很长,其中在使用完了ThreadLocal之后我们手动将其引用设置为null,希望GC清理调它,如果Entry是强引用的话,GC是无法清理调的,因为还存在一个Thread->ThreadLocalMap->Entry->ThreadLocal的强引用链,导致整个Entry都不会被回收,从而发生内存泄漏。Java团队使用弱引用的原因就是为了处理生命周期长的线程而使用的。

以上是关于ThreadLocal源码及相关问题分析的主要内容,如果未能解决你的问题,请参考以下文章

Java中线程局部变量ThreadLocal使用教程及源码分析

多线程爬坑之路-ThreadLocal源码及原理的深入分析

ThreadLocal介绍以及源码分析

ThreadLocal定义使用案例及源码分析

史上最详细之ThreadLocal源码分析

ThreadLocal源码分析