首先考虑一个问题,在进行并发编程的时候,如果遇到多个线程需要访问同一个变量的时候应该怎么实现呢?
方案一:使用同步机制,但它是有弊端的,涉及到何时加锁与释放锁等并且线程访问锁时需要等待,这样很浪费时间。
方案二:使用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就会导致内存泄漏,而不是因为弱引用。