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中就会出现keynullEntry,就没有办法访问这些keynullEntryvalue,如果当前线程再迟迟不结束的话,这些keynullEntry对应value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。

3.2 为什么要弱引用?

因为如果这里使用普通的key-value形式来定义存储结构,实质上就会造成节点的生命周期与线程强绑定,只要线程没有销毁,那么节点在GC分析中一直处于可达状态,没办法被回收,而程序本身也无法判断是否可以清理节点。弱引用是Java中四档引用的第三档,比软引用更加弱一些,如果一个对象没有强引用链可达,那么一般活不过下一次GC。当某个ThreadLocal已经没有强引用可达,则随着它被垃圾回收,在ThreadLocalMap里对应的Entry的键值会失效,这为ThreadLocalMap本身的垃圾清理提供了便利。

 因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。

猜你喜欢

转载自www.cnblogs.com/jygroup/p/9262273.html