深入理解ThreadLocal

1 定义

ThreadLocal是存储线程局部变量的容器。

它为每一个使用该变量的线程都提供了一个变量值的副本,是Java中一种较为特殊的线程绑定机制。

每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本发生冲突。

2 原理分析

在Java中,Thread类代表线程。

查看Thread源码,如下:

1public class Thread implements Runnable { 2 ...... 3 4 /** 5 * 与此线程相关的ThreadLocal值。 6 * 这个map由ThreadLocal类维护。 7 */ 8 ThreadLocal.ThreadLocalMap threadLocals = null; 9 10 /* 11 * 与此线程相关的那些从父线程继承而来的ThreadLocal值。 12 * 这个map由InheritableThreadLocal类维护。 13 */ 14 ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; 15 16

可以看出,在Thread中是通过ThreadLocal.ThreadLocalMap来发挥ThreadLocal的功能的。

下面来看一下ThreadLocal的工作原理。

ThreadLocal提供了set(T value)和get()方法,用来存取线程局部变量。

2.1 ThreadLocal的set(T value)方法

查看ThreadLocal的set(T value)方法的源码:

1 /** 2 * 设置当前线程中线程局部变量的值 3 */ 4 public void set(T value) { 5 // 获取当前线程对象 6 Thread t = Thread.currentThread(); 7 // 获取ThreadLocalMap 8 ThreadLocalMap map = getMap(t); 9 // 如果map存在,就将设置的value存入map中;如果map不存在,就创建一个map并写入值 10 if (map != null) 11 map.set(this, value); 12 else 13 createMap(t, value); 14 } 15 16

getMap(t)源码如下:

1 ThreadLocalMap getMap(Thread t) { 2 return t.threadLocals; 3 } 4 5

createMap(t, value)的源码如下:

1 void createMap(Thread t, T firstValue) { 2 t.threadLocals = new ThreadLocalMap(this, firstValue); 3 } 4 5

可以看出,getMap是将当前线程对象t传入,然后获取当前线程对象t中threadLocals的引用。因为每个线程Thread都有自己的threadLocals,所以getMap(t)返回的ThreadLocalMap是每个线程自己的。

每个线程中都有一个独立的ThreadLocalMap副本,它所存储的值,只能被当前线程读取和修改。

ThreadLocal类通过操作每一个线程特有的ThreadLocalMap副本,从而实现了变量访问在不同线程中的隔离。因为每个线程的变量都是自己的,完全不会有并发错误。

ThreadLocalMap存储的键值对中的键是this对象指向的ThreadLocal对象,而值就是所设置的对象。

2.2 ThreadLocal的get()方法

查看ThreadLocal的get()方法源码:

1 /** 2 * 设置当前线程中线程局部变量的值 3 */ 4 public T get() { 5 // 获取当前线程对象 6 Thread t = Thread.currentThread(); 7 // 获取ThreadLocalMap 8 ThreadLocalMap map = getMap(t); 9 // 1.如果map存在 10 if (map != null) { 11 // 从map中获取键值对,键为当前ThreadLocal对象 12 ThreadLocalMap.Entry e = map.getEntry(this); 13 // 如果键值对存在 14 if (e != null) { 15 // 获取键值对中的值,并返回 16 @SuppressWarnings("unchecked") 17 T result = (T)e.value; 18 return result; 19 } 20 } 21 // 2.如果map不存在,设置初始化值并返回它 22 return setInitialValue(); 23 } 24 25

getMap(t)源码如下:

1 ThreadLocalMap getMap(Thread t) { 2 return t.threadLocals; 3 } 4 5

setInitialValue()源码如下:

1 /** 2 * 设置初始化值 3 */ 4 private T setInitialValue() { 5 T value = initialValue(); 6 Thread t = Thread.currentThread(); 7 ThreadLocalMap map = getMap(t); 8 if (map != null) 9 map.set(this, value); 10 else 11 createMap(t, value); 12 return value; 13 } 14 15 /** 16 * 初始化 17 */ 18 protected T initialValue() { 19 return null; 20 } 21 22

可以看出,和set(T value)同理,也是通过ThreadLocalMap来获取线程的局部变量的。这样就能保证获取到的值都是每个线程自己的副本,线程之间不会相互影响。

2.3 总结

实际使用的时候,ThreadLocal变量作为类中的实例域,会被所有的线程共享。

但是,每个线程获取ThreadLocal对象之后,通过set(T value)方法设置值的时候,首先是获取线程自己的ThreadLocalMap对象,然后将设置的值存入ThreadLocalMap中,键为这个线程获取的ThreadLocal对象,值为设置的value。

所以,对于同一个ThreadLocal来说,在每个线程中的ThreadLocalMap中的键都是同一个对象;每个线程中的ThreadLocalMap可以有多个键值对,那么不同的键对应的就是不同的ThreadLocal实例域对象。

get()方法的原理和set(T value)一样的,也是通过通过ThreadLocalMap来实现线程隔离的。

3 需要注意的点

3.1 ThreadLocalMap不是Map接口的实现

ThreadLocalMap不是Map接口的实现,内部使用的是Entry[] table来保存键值对的。

1 /** 2 * The table, resized as necessary. 3 * table.length MUST always be a power of two. 4 */ 5 private Entry[] table; 6 7

并且,通过hash算法来做散列:

1 // 计算数组索引 2 int i = key.threadLocalHashCode & (len-1); 3 4 ...... 5 6 // 生成threadLocal对象的hash值 7 private final int threadLocalHashCode = nextHashCode(); 8 9 ...... 10 11 /** 12 * 返回下一个hash值 13 */ 14 private static int nextHashCode() { 15 return nextHashCode.getAndAdd(HASH_INCREMENT); 16 } 17 18 19

3.2 ThreadLocal使用不当引发的内存泄漏问题

ThreadLocal可能存在内存泄漏问题的根源在于:

ThreadLocal中的key是弱引用的。

源码:

1 /** 2 * Entry继承WeakReference, 3 * map中的键(ThreadLocal对象)是弱引用。 4 * 注意,null键(entry.get() == null)表示这个键不再被引用,因此这个entry可以从数组中移除。 5 */ 6 static class Entry extends WeakReference<ThreadLocal<?>> { 7 /** The value associated with this ThreadLocal. */ 8 Object value; 9 10 Entry(ThreadLocal<?> k, Object v) { 11 super(k); 12 value = v; 13 } 14 } 15 16

3.2.1 为什么使用弱引用

要理解为什么ThreadLocalMap中需要使用WeakReference作为key类型,那么首先需要理解WeakReference的意义。

WeakReference是Java语言规范中为了区别直接的对象引用(程序中通过构造函数声明出来的对象引用)而定义的另外一种引用关系。WeakReference标志性的特点是:不会影响到被引用对象的GC回收行为(即,只要对象被除了WeakReference对象之外所有的对象解除引用后,该对象便可以被GC回收),只不过在被引用对象回收之后,通过WeakReference获得被引用对象时程序会返回null。

理解了WeakReference之后,ThreadLocalMap使用它的目的也相对清晰了:

当ThreadLocal实例可以被GC回收时(该实例没有任何强引用了),系统可以通过弱引用检测到该ThreadLocal对应的Entry是否已经过期(根据reference.get() == null来判断,如果为true则表示过期,程序内部称为stale slots)来做一些自动清除工作,否则如果不清除的话容易产生内存无法释放的问题——value对应的对象即使不再使用,但由于被ThreadLocalMap所引用导致无法被GC回收。

3.2.2 内存泄漏问题

下面的图展示了ThreadLocal、ThreadLocalMap、Entry之间的关系:

在这里插入图片描述

上图中,实线代表强引用,虚线代表弱引用。

  • 如果ThreadLocal实例对象的外部强引用ThreadLocalRef被置为null(threadLocalRef == null)的话,ThreadLocal实例对象就没有一条引用链路可达,很显然在GC的时候势必会被回收。

  • 因此这个ThreadLocal实例对应的Entry就存在key为null的情况,程序是无法通过一个key为null去访问到该Entry的value。

  • 如果当前线程未被销毁。那么,就存在这样一条引用链:currentThreadRef -> currentThread -> threadLocalMap -> entry -> valueRef -> valueMemory,导致在垃圾回收的时进行可达性分析的时候,value可达从而不会被回收掉,但是该value永远不能被访问到了,这样导致了 **内存泄漏 ** 的问题。

  • 当然,如果线程执行结束后,栈被销毁,那么threadLocalRef、currentThreadRef就会断掉。因此ThreadLocal、ThreadLocalMap、Entry都会被回收掉,对应的value也会被回收,不会出现**内存泄漏 **。

  • 可是,在实际使用中我们大多数情况都会用线程池去维护我们的线程,线程在使用完之后并不会被销毁,而是返回到线程池中,这时候很可能出现ThreadLocal内存泄漏的问题,需要我们多加关注。

3.2.3 已经做出了哪些改进?

实际上,为了解决ThreadLocal潜在的内存泄漏的问题,Josh Bloch and Doug Lea大师已经做了一些改进。

在ThreadLocal的set和get方法中都有相应的处理。

下文为了叙述,针对key == null的entry,源码注释为stale entry,直译为“不新鲜的entry”,这里我就称之为“脏entry”。

查看ThreadLocalMap的set方法:

1 private void set(ThreadLocal<?> key, Object value) { 2 3 // We don't use a fast path as with get() because it is at 4 // least as common to use set() to create new entries as 5 // it is to replace existing ones, in which case, a fast 6 // path would fail more often than not. 7 8 Entry[] tab = table; 9 int len = tab.length; 10 int i = key.threadLocalHashCode & (len-1); 11 12 for (Entry e = tab[i]; 13 e != null; 14 e = tab[i = nextIndex(i, len)]) { 15 ThreadLocal<?> k = e.get(); 16 17 if (k == key) { 18 e.value = value; 19 return; 20 } 21 22 // 脏entry 23 if (k == null) { 24 // 替换这个脏entry 25 replaceStaleEntry(key, value, i); 26 return; 27 } 28 } 29 30 // 插入新的entry 31 tab[i] = new Entry(key, value); 32 int sz = ++size; 33 if (!cleanSomeSlots(i, sz) && sz >= threshold) 34 rehash(); 35 } 36 37

在该方法中针对 脏entry做了这样的处理:

  1. 如果当前table[i]!= null的话,说明hash冲突,就需要向后环形查找,若在查找过程中遇到脏entry就通过replaceStaleEntry(key, value, i)进行处理;
  2. 如果当前table[i] == null的话,说明这是新的entry,可以直接插入,但是插入后会调用cleanSomeSlots(i, sz)方法检测并清除脏entry。

具体的源码分析,参见https://www.jianshu.com/p/dde92ec37bd1

3.3 ThreadLocal最佳实践

因为在线程池中使用ThreadLocal的时候,很可能引发内存泄漏的问题,所以:

在确定不再使用ThreadLocal的时候,请调用remove()方法删除数据。

下面是remove的源码:

1 /** 2 * 移除当前ThreadLocal对应的线程局部变量 3 */ 4 public void remove() { 5 // 拿到当前线程的ThreadLocalMap 6 ThreadLocalMap m = getMap(Thread.currentThread()); 7 // 如果map不是null,调用map的remove方法删除当前这个ThreadLocal对应的Entry 8 if (m != null) 9 m.remove(this); 10 } 11 12 13 /** 14 * 移除key对应的Entry 15 */ 16 private void remove(ThreadLocal<?> key) { 17 // 根据key计算数组中的索引位置i 18 Entry[] tab = table; 19 int len = tab.length; 20 int i = key.threadLocalHashCode & (len-1); 21 22 for (Entry e = tab[i]; 23 e != null; 24 e = tab[i = nextIndex(i, len)]) { 25 // 查找到与key对应的Entry 26 if (e.get() == key) { 27 // 通过clear方法将key的引用置为null,这个entry就变成了一个“脏Entry” 28 e.clear(); 29 // 通过 30 expungeStaleEntry(i); 31 return; 32 } 33 } 34 } 35 36 37 /** 38 * 删除“脏Entry” 39 */ 40 private int expungeStaleEntry(int staleSlot) { 41 Entry[] tab = table; 42 int len = tab.length; 43 44 // 删除entry 45 tab[staleSlot].value = null; 46 tab[staleSlot] = null; 47 size--; 48 49 // Rehash until we encounter null 50 Entry e; 51 int i; 52 for (i = nextIndex(staleSlot, len); 53 (e = tab[i]) != null; 54 i = nextIndex(i, len)) { 55 ThreadLocal<?> k = e.get(); 56 if (k == null) { 57 e.value = null; 58 tab[i] = null; 59 size--; 60 } else { 61 int h = k.threadLocalHashCode & (len - 1); 62 if (h != i) { 63 tab[i] = null; 64 65 // Unlike Knuth 6.4 Algorithm R, we must scan until 66 // null because multiple entries could have been stale. 67 while (tab[h] != null) 68 h = nextIndex(h, len); 69 tab[h] = e; 70 } 71 } 72 } 73 return i; 74 } 75 76

4 应用场景

最常见的ThreadLocal使用场景为 用来解决 数据库连接、Session管理等。

例如:

1 private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() { 2 public Connection initialValue() { 3 return DriverManager.getConnection(DB_URL); 4 } 5 }; 6 7 public static Connection getConnection() { 8 return connectionHolder.get(); 9 } 10 11
1 private static final ThreadLocal<Session> threadSession = new ThreadLocal<>(); 2 3 public static Session getSession() throws InfrastructureException { 4 Session s = (Session) threadSession.get(); 5 try { 6 if (s == null) { 7 s = getSessionFactory().openSession(); 8 threadSession.set(s); 9 } 10 } catch (HibernateException ex) { 11 throw new InfrastructureException(ex); 12 } 13 return s; 14 } 15 16

代码交流 2021