ThreadLocal深入分析
首先考虑一个问题,在进行并发编程的时候,如果遇到多个线程需要访问同一个变量的时候应该怎么实现呢?
方案一:使用同步机制,但它是有弊端的,涉及到何时加锁与释放锁等并且线程访问锁时需要等待,这样很浪费时间。
方案二:使用ThreadLocal工具类。
以下是本文目录大纲:
一.ThreadLocal简述
二.深入解析ThreadLocal类
三.ThreadLocal内存泄漏分析
一.ThreadLocal简述
我们看一下Josh Bloch和Doug Lea对于ThreadLocal的介绍:
1 * This class provides thread-local variables. These variables differ from 2 * their normal counterparts in that each thread that accesses one (via its 3 * <tt>get</tt> or <tt>set</tt> method) has its own, independently initialized 4 * copy of the variable. <tt>ThreadLocal</tt> instances are typically private 5 * static fields in classes that wish to associate state with a thread (e.g., 6 * a user ID or Transaction ID). 7 * <p>Each thread holds an implicit reference to its copy of a thread-local 8 * variable as long as the thread is alive and the <tt>ThreadLocal</tt> 9 * instance is accessible; after a thread goes away, all of its copies of 10 * thread-local instances are subject to garbage collection (unless other 11 * references to these copies exist).
大概是这么个意思:
这个类提供线程本地变量。这个变量里面的值(通过get方法或者set方法获取)是和其他线程分割开来的,变量的值只有当前线程能访问到,不像一般的类型比如Person,Student类型的变量,只要访问到声明该变量的对象,即可访问其全部内容,而且各个线程的访问的数据是无差别的。Thread的典型应用是提供一个与程序运行状态相关静态变量,比如一次访问回话的表示符号:USERID,或者一次事务里面的事务id:Transaction ID。
每个线程都持有对其线程本地副本的隐式引用只要线程是活动的,并且ThreadLocal实例访问;一根线消失后,它的所有副本线程本地实例受制于垃圾收集(除非其他实例)存在对这些副本的引用)。
二.深入解析ThreadLocal类
1.ThreadLocal相关API
1 public class ThreadLocal<T> { 2 public ThreadLocal() {} //构造方法 3 protected T initialValue() {} //获取局部变量在当前线程的初始值 4 public T get() {} // 获取局部变量在当前线程副本中的值 5 public void set(T value) {} //设置局部变量在当前线程副本中的值为指定值 6 public void remove() {} //移除局部变量在当前线程中的值 7 }
2.ThreadLocal实现原理
ThreadLocal提供了get与set等访问接口或方法,这些方法为每个使用该变量的线程都存有一份独立的副本(即每个线程的threadLocals属性),因此get总是返回由当前执行线程在调用set时设置的最新值。
2.1 set()方法
1 public void set(T value) { 2 Thread t = Thread.currentThread(); //获取当前线程 3 ThreadLocalMap map = getMap(t); // 获取当前线程对应的ThreadLocalMap 4 // 当前线程的ThreadLocalMap不为空则调用set方法, this为调用该方法的ThreadLocal对象 5 if (map != null) 6 map.set(this, value); 7 // map为空则调用createMap方法创建一个新的ThreadLocalMap, 并新建一个Entry放入该ThreadLocalMap 8 else 9 createMap(t, value); 10 } 11 12 ThreadLocalMap getMap(Thread t) { 13 return t.threadLocals; // 返回线程t的threadLocals属性 14 } 15 16 void createMap(Thread t, T firstValue) { 17 t.threadLocals = new ThreadLocalMap(this, firstValue); 18 }
- 先拿到当前线程,再使用getMap方法拿到当前线程对应的threadLocals变量
- 如果threadLocals不为空,则将当前ThreadLocal作为key,传入的值作为value,调用ThreadLocalMap.set方法,插入threadLocals。
- 如果threadLocals为空则调用创建一个ThreadLocalMap,并新建一个Entry放入该ThreadLocalMap, 调用set方法的ThreadLocal和传入的value作为该Entry的key和value
注意:此处的threadLocals变量类型为ThreadLocal.ThreadLocalMap,是Thread的一个局部变量,因此它只与当前线程绑定。
1 public class Thread implements Runnable { 2 ... 3 /* ThreadLocal values pertaining to this thread. This map is maintained 4 * by the ThreadLocal class. */ 5 ThreadLocal.ThreadLocalMap threadLocals = null; 6 ... 7 }
2.2 ThreadLocalMap实现原理
static class ThreadLocalMap { /** * The entries in this hash map extend WeakReference, using * its main ref field as the key (which is always a * ThreadLocal object). Note that null keys (i.e. entry.get() * == null) mean that the key is no longer referenced, so the * entry can be expunged from table. Such entries are referred to * as "stale entries" in the code that follows. */ 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的内部类,主要有一个Entry数组,Entry的key可以简单的认为是ThreadLocal,实际上ThreadLocal中存放的是ThreadLocal的弱引用。value为实际放入的值。每个线程都有一个ThreadLocalMap类型的threadLocals变量。
2.3 get()方法
1 public T get() { 2 Thread t = Thread.currentThread(); 3 ThreadLocalMap map = getMap(t); 4 if (map != null) { 5 //调用getEntry方法, 通过调用get()方法的ThreadLocal获取对应的Entry 6 ThreadLocalMap.Entry e = map.getEntry(this); 7 if (e != null) { //Entry不为空则代表找到目标Entry, 返回该Entry的value值 8 @SuppressWarnings("unchecked") 9 T result = (T)e.value; 10 return result; 11 } 12 } 13 return setInitialValue(); //该线程的ThreadLocalMap为空则初始化一个 14 }
- 跟set方法差不多,先拿到当前的线程,再使用getMap方法拿到当前线程的threadLocals变量
- 如果threadLocals不为空,则将ThreadLocal作为key,调用ThreadLocalMap.getEntry方法找到对应的Entry。
- 如果threadLocals为空或者找不到目标Entry,则调用setInitialValue方法进行初始化。
三.ThreadLocal内存泄漏分析
ThreadLocal
的实现是这样的:每个Thread
维护一个 ThreadLocalMap
映射表,这个映射表的 key
是 ThreadLocal
实例本身,value
是真正需要存储的 Object
。
也就是说 ThreadLocal
本身并不存储值,它只是作为一个 key
来让线程从 ThreadLocalMap
获取 value
。值得注意的是图中的虚线,表示 ThreadLocalMap
是使用 ThreadLocal
的弱引用作为 Key
的,弱引用的对象在 GC 时会被回收。
3.1 为什么会内存泄漏?
ThreadLocalMap
使用ThreadLocal
的弱引用作为key
,如果一个ThreadLocal
没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal
势必会被回收,这样一来,ThreadLocalMap
中就会出现key
为null
的Entry
,就没有办法访问这些key
为null
的Entry
的value
,如果当前线程再迟迟不结束的话,这些key
为null
的Entry对应
的value
就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
永远无法回收,造成内存泄漏。
3.2 为什么要弱引用?
因为如果这里使用普通的key-value形式来定义存储结构,实质上就会造成节点的生命周期与线程强绑定,只要线程没有销毁,那么节点在GC分析中一直处于可达状态,没办法被回收,而程序本身也无法判断是否可以清理节点。弱引用是Java中四档引用的第三档,比软引用更加弱一些,如果一个对象没有强引用链可达,那么一般活不过下一次GC。当某个ThreadLocal已经没有强引用可达,则随着它被垃圾回收,在ThreadLocalMap里对应的Entry的键值会失效,这为ThreadLocalMap本身的垃圾清理提供了便利。
因此,ThreadLocal
内存泄漏的根源是:由于ThreadLocalMap
的生命周期跟Thread
一样长,如果没有手动删除对应key
就会导致内存泄漏,而不是因为弱引用。