Java -- 基于JDK1.8的ThreadLocal源码分析

1,最近在做一个需求的时候需要对外部暴露一个值得应用  ,一般来说直接写个单例,将这个成员变量的值暴露出去就ok了,但是当时突然灵机一动(现在回想是个多余的想法),想到handle源码里面有使用过ThreadLocal这个类,想了想为什么不想直接用ThreadLocal保存数据源然后使用静态方法暴露出去呢,结果发现使用ThreadLocal有时候会获取不到值,查了下原因原来同事是在子线程中调用的(捂脸哭泣),所以还是要来看一波源码,看看ThreadLocal底层实现,适用于哪些场景

 

2,我们现在网上搜索一下前人对于ThreadLocal这个类的一些总结

ThreadLocal特性及使用场景:
1、方便同一个线程使用某一对象,避免不必要的参数传递;
2、线程间数据隔离(每个线程在自己线程里使用自己的局部变量,各线程间的ThreadLocal对象互不影响);
3、获取数据库连接、Session、关联ID(比如日志的uniqueID,方便串起多个日志);

从上面的总结来看,主要是用来线程间的数据隔离的,即ThreadLocal 对象可以在多个线程中共享, 但每个线程只能读写其中自己的数据副本。

ThreadLocal<Boolean> mBooleanThreadLocal = new ThreadLocal<>();
        mBooleanThreadLocal.set(true);
        Boolean result = mBooleanThreadLocal.get();

 主要就是这三个方法  ,那咱们就一个一个来看

2.1 构造函数

 /**
     * ThreadLocals rely on per-thread linear-probe hash maps attached
     * to each thread (Thread.threadLocals and
     * inheritableThreadLocals).  The ThreadLocal objects act as keys,
     * searched via threadLocalHashCode.  This is a custom hash code
     * (useful only within ThreadLocalMaps) that eliminates collisions
     * in the common case where consecutively constructed ThreadLocals
     * are used by the same threads, while remaining well-behaved in
     * less common cases.
     */
    private final int threadLocalHashCode = nextHashCode();

    /**
     * The next hash code to be given out. Updated atomically. Starts at
     * zero.
     */
    private static AtomicInteger nextHashCode =
        new AtomicInteger();

    /**
     * The difference between successively generated hash codes - turns
     * implicit sequential thread-local IDs into near-optimally spread
     * multiplicative hash values for power-of-two-sized tables.
     */
    private static final int HASH_INCREMENT = 0x61c88647;

    public ThreadLocal() {
    }

  ThreadLocal的构造方法是一个空方法 ,但是有三个参数,nextHashCode 和HASH_INCREMENT 是ThreadLocal类的静态变量,真正变量只有 threadLocalHashCode 这一个,这三个参数都不是善茬啊。

  HASH_INCREMENT 英文注释解释是“连续生成的哈希码之间的差异——将隐式顺序线程本地id转换为接近最优扩散的乘法哈希值,用于大小为2的幂的表。” 这句解释看得我们一脸蒙蔽啊,不过我记得看HashMap源码的时候 有解决哈希冲突这一说,我们先不探究这么多,先将0x61c88647这个奇怪的值记在心里一下。

  nextHashCode 的表示了即将分配的下一个ThreadLocal实例的threadLocalHashCode 的值。

  threadLocalHashCode 见名知意 这个ThreadLocal对象的hashcode

private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

  调用的是nextHashCode方法  ,就是将ThreadLocal类的下一个hashCode值即nextHashCode的值赋给实例的threadLocalHashCode,然后nextHashCode的值增加HASH_INCREMENT这个值。

  我们先不管这些参数的生产方式,先知道有这三个参数就行,继续往下面看流程

2,2 set方法

 1 public void set(T value) {
 2         Thread t = Thread.currentThread();
 3         ThreadLocalMap map = getMap(t);
 4         if (map != null)
 5             map.set(this, value);
 6         else
 7             createMap(t, value);
 8     }
 9 
10 ThreadLocalMap getMap(Thread t) {
11         return t.threadLocals;
12     }
13 
14 void createMap(Thread t, T firstValue) {
15         t.threadLocals = new ThreadLocalMap(this, firstValue);
16     }

   我们可以看到,首先通过当前调用的线程获取到线程中对应的threadLocals变量(这里我一脸懵逼  线程中竟然使用到ThreadLocal了,赶紧去看看线程的源码)

public class Thread implements Runnable {
     ThreadLocal.ThreadLocalMap threadLocals = null;
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}

  果然Thread中拥有变量threadLocals,孤陋寡闻啊,各位老铁,咋们继续往下看代码,判断一下从线程中获取到的ThreadLocalMap,如果不为空则,调用ThreadLocalMap类的set方法保存一下,,如果为空,则new一个ThreadLocalMap对象出来 这里涉及到了ThreadLocalMap这个类,我们来详细的看一下这个类

2.3 ThreadLocalMap类

2.3.1 构造函数

 1 static class Entry extends WeakReference<ThreadLocal<?>> {
 2              /** The value associated with this ThreadLocal. */
 3             Object value;
 4 
 5              Entry(ThreadLocal<?> k, Object v) {
 6                  super(k);
 7                  value = v;
 8              }
 9          }
10  
11          /**
12           * The initial capacity -- MUST be a power of two.
13           */
14          private static final int INITIAL_CAPACITY = 16;
15  
16          /**
17          * The table, resized as necessary.
18           * table.length MUST always be a power of two.
19           */
20          private Entry[] table;
21  
22          /**
23           * The number of entries in the table.
24          */
25          private int size = 0;
26  
27          /**
28           * The next size value at which to resize.
29           */
30          private int threshold; // Default to 0
31  
32  ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
33              table = new Entry[INITIAL_CAPACITY];
34              int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
35              table[i] = new Entry(firstKey, firstValue);
36              size = 1;
37              setThreshold(INITIAL_CAPACITY);
38  }
39 
40 /**
41          * Set the resize threshold to maintain at worst a 2/3 load factor.
42          */
43         private void setThreshold(int len) {
44             threshold = len * 2 / 3;
45         }
46  /**
47          * The next size value at which to resize.
48          */
49         private int threshold; // Default to 0

        第32-33行 :  我们看到ThreadLocalMap是ThreadLocal的匿名内部类,且构造方法也就是将该ThreadLocal实例作为key,要保持的对象作为值。变量table用来用来存放存放数据,我们可以看到table数组的初始大小是INITIAL_CAPACITY = 16  英文注释是“初始容量——必须是2的幂。”   这里我们又碰到了“2的幂”关键字了,我们继续往下看

     第34行: int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);  这行代码完全有些看不懂 ,为了不理解流程,我们可以先不用懂,就模糊的理解为通过ThreadLoad的threadLocalHashCode变量哈希码来生成一个下标位置,继续往下看

  第35 - 49行:将ThreadLocal对象和value值保存到Entry对象中再保存到table数组中,设置初始的size,设置threshold阈值,这个阈值适用于扩容,当发现存入的数据大小打到了当前长度size的二分之三,就会触发扩容,将当前table数组的大小扩充到原来的两倍。

  然后我们可以看到Entry对象中对我们的ThreadLocal参数是采用弱引用的,这点对我们后续分析ThreadLocal内存泄漏这款有所帮助,先提醒一下大家。

  然后我们全面来看一下int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);   这行代码所带来的的意思,我们再和上面有可能有联系的一些代码全部给粘贴在一起

private static final int HASH_INCREMENT = 0x61c88647;
private static AtomicInteger nextHashCode = new AtomicInteger();
private final int threadLocalHashCode = nextHashCode();


int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
 /**
         * The initial capacity -- MUST be a power of two.
         */
private static final int INITIAL_CAPACITY = 16;

  在看代码之前我们先来了解一个小的知识点,看过HashMap源码的同学一定知道“哈希冲突”这个问题

我们使用同一个哈希函数来计算不止一个的待存放的数据在表中的存放位置,
总是会有一些数据通过这个转换函数计算出来的存放位置是相同的,这就是哈希冲突。
也就是说,不同的关键字通过同一哈希转换函数计算出相同的哈希地址。

  通过ThreadLocal和ThreadLocalMap的源码可以知道,里面存值table的下标是通过ThreadLocal的哈希码生成的,那么在ThreadLocalMap同样的存在这个哈希冲突问题,那我们来看看ThreadLocal是怎么来解决这个问题的呢?

  我们知道ThreadLocalMap的初始长度为16,每次扩容都增长为原来的2倍,即它的长度始终是2的n次方,大小必须是2的N次方呀(len = 2^N),那 len-1 的二进制表示就是低位连续的N个1,以16为例,16-1的二进制是15(十进制) = 1111(二进制),而 key.threadLocalHashCode & (len-1) 的值就是 threadLocalHashCode 的低N位(&运算符我就不给大家进行解释了,大家自己百度一下位运算符),而这样做的目的是能均匀的产生哈希码的分布,我一脸懵逼,那让我们来看一下

public static  int HASH_INCREMENT = 0x61c88647 ;

public static void range(int value){
        for (int i = 0; i < value; i++) {
            int nextHashCode = i*HASH_INCREMENT + HASH_INCREMENT;
            System.out.println((nextHashCode & (value - 1))+",");
        }
    }

range(16);



//输出结果
7 14 5 12 3 10 1 8 15 6 13 4 11 2 9 0

  卧槽,真的可以均匀的产生,难道0x61c88647这个数值这么神奇?下面是网上搜到的关于这个数字的节选,我反正是看不懂,还是给大家贴出来吧

①这个魔数的选取与斐波那契散列有关,0x61c88647对应的十进制为1640531527。
②斐波那契散列的乘数可以用(long) ((1L << 31) * (Math.sqrt(5) - 1))可以得到2654435769,如果把这个值给转为带符号的int,则会得到-1640531527。
换句话说 (1L << 32) - (long) ((1L << 31) * (Math.sqrt(5) - 1))得到的结果就是
1640531527也就是0x61c88647 。
③通过理论与实践,当我们用0x61c88647作为魔数累加为每个ThreadLocal分配各自的ID也就是threadLocalHashCode再与2的幂取模,得到的结果分布很均匀。

  理解了生成方式我们继续往下看ThreadLocalMap的源码吧

2.3.2 set函数

 1  private void set(ThreadLocal<?> key, Object value) {
 2 
 3             Entry[] tab = table;
 4             int len = tab.length;
 5             int i = key.threadLocalHashCode & (len-1);
 6 
 7             for (Entry e = tab[i];
 8                  e != null;
 9                  e = tab[i = nextIndex(i, len)]) {
10                 ThreadLocal<?> k = e.get();
11 
12                 if (k == key) {
13                     e.value = value;
14                     return;
15                 }
16 
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) && sz >= threshold)
26                 rehash();
27         }

  set方法也很简单,就是去table中获取第i位的数据,如果发现当前第i为的ThreadLocal等于当前传入的ThreadLocal,就更新i位的value,如果发现第i为的ThreadLocal为空,由于我们的Entry对ThreadLocal是弱引用,就表示之前保存的ThreadLocal已经被回收了,replaceStaleEntry()方法就不和大家细看了,就是首先清理掉空的Entry,然后将后面的 Entry 进行 rehash 填补空洞,25-27行就是对阈值的判断,如果超过了就进行扩容。

2.3.3 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);
 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);
20                 else
21                     i = nextIndex(i, len);
22                 e = tab[i];
23             }
24             return null;
25         }

  

1 private static int nextIndex(int i, int len) {
2     return ((i + 1 < len) ? i + 1 : 0);
3 }

  getEntry函数也很简单,使用位运算找到哈希槽。若哈希槽中为空或 key 不是当前 ThreadLocal 对象则会调用getEntryAfterMiss方法,可以看到getEntryAfterMiss 方法会循环查找直到找到或遍历所有可能的哈希槽, 在循环过程中可能遇到4种情况:

①哈希槽中是当前ThreadLocal, 说明找到了目标 
②哈希槽中为其它ThreadLocal, 需要继续查找 
③哈希槽中为null, 说明搜索结束未找到目标 
④哈希槽中存在Entry, 但是 Entry 中没有 ThreadLocal 对象。因为 Entry 使用弱引用, 这种情况说明 ThreadLocal 被GC回收。 为了处理GC造成的空洞(stale entry), 需要调用expungeStaleEntry方法进行清理。

 2.3.3 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();
10                     expungeStaleEntry(i);
11                     return;
12                 }
13             }
14         }

  remove方法和get方法类似,也是找出对应的Entry对象,然后调用其clean方法清理

  

  ok,这里我们就把ThreadLocalMap的源码看完了,其实类似ThreadLocal的源码一样,来我们继续往下看

2.4  get函数

public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

 private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

protected T initialValue() {
    return null;
}

   都很简单,首先去获取当前线程的ThreadLocalMap中的Entry对象,如果不为空直接返回Entry的value值,如果为空,则调用initialValue方法,这个方法可以被重写 ,默认返回为null,将当前线程的ThreadLocal对象保存在ThreadLocalMap中,返回上层null。

2.5 remove函数

 public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

  也很简单,这里就不在啰嗦了  ,ok,到这里我们已经看完了ThreadLocal源码 ,真正的功能都是在ThreadLocalMap中进行操作的,这里我有一个疑问,为什么不适用HashMap来替代ThreadLocalMap呢?如一下代码:

class ThreadLocal { 
  private Map values = Collections.synchronizedMap(new HashMap());
 
  public Object get() {
    Thread curThread = Thread.currentThread();
    Object o = values.get(curThread);
    if (o == null && !values.containsKey(curThread)) {
      o = initialValue();
      values.put(curThread, o);
    }
    return o;
  }
 
  public void set(Object newValue) {
    values.put(Thread.currentThread(), newValue);
  }
}

    这样貌似也没问题啊,乍一看的确没毛病,但是我们知道ThreadLocal本意是避免并发,用一个全局Map显然违背了这一初衷,且会导致内存泄漏,用Thread当key,除非手动调用remove,否则即使线程退出了会导致:1)该Thread对象无法回收;2)该线程在所有ThreadLocal中对应的value也无法回收。

  这时候会有同学提出疑问了,你使用ThreadLocalMap的话就不会导致内存泄漏吗?

  很多人认为:threadlocal里面使用了一个存在弱引用的map,当释放掉threadlocal的强引用以后,map里面的value却没有被回收.而这块value永远不会被访问到了。在这种情况下我们的确会存在value的泄漏。

  我们看上图,每个thread中都存在一个map, map的类型是ThreadLocal.ThreadLocalMap. Map中的key为一个threadlocal实例. 这个Map的确使用了弱引用,不过弱引用只是针对key. 每个key都弱引用指向threadlocal. 当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收, 但是,我们的value却不能回收,因为存在一条从current thread连接过来的强引用. 只有当前thread结束以后, current thread就不会存在栈中,强引用断开, Current Thread, Map, value将全部被GC回收。

  我们从之前的ThreadLocalMap源码可以知道,弱引用只存在于key上,所以key会被回收,但当线程还在运行的情况下,value还是在被线程强引用而无法释放,只有当线程结束之后或者我们调用set、get方法的时候回去移除已被回收的Entry( replaceStaleEntry这个方法),给出的建议是,当ThreadLocal使用完成的时候,调用remove方法将value移除掉,这样就不会存在内存泄漏了。

 

3,总结

  我们代码看完了,需要对ThreadLocal的使用场景进行总结一下 :

  ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。

  ok,感觉好久都没有些博客了,思路和语言表达都有些生疏,争取后面一直坚持写一写,加油

  

  

posted @ 2019-08-20 17:52  阿呆哥哥  阅读(858)  评论(1编辑  收藏  举报