vlambda博客
学习文章列表

源代码系列02——ThreadLocal源码分析(基础篇)


  1    一个ThreadLocal的实际案例



相信各位读者朋友或多或少都见到过以下类似的示例:


public class ThreadLocalTest {
public static void main(String[] args) { ThreadLocal<String> threadLocal = new ThreadLocal<>();
Thread thread1 = new Thread(() -> { threadLocal.set("thread1 content"); System.out.println("thread1 result : " + threadLocal.get()); });
Thread thread2 = new Thread(() -> { try {                // 休眠100ms,保证线程1先设置变量值 TimeUnit.MILLISECONDS.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("before set, thread2 result : " + threadLocal.get()); threadLocal.set("thread2 content"); System.out.println("after set, thread2 result : " + threadLocal.get()); });
thread1.start(); thread2.start(); }
}


最终的输出结果如下所示:


thread1 result : thread1 contentbefore set, thread2 result : nullafter set, thread2 result : thread2 content


也就是说,线程1、线程2的变量值互不影响,做到了线程级别的数据隔离。那么,ThreadLocal是如何实现的呢?


  2    ThreadLocal的set方法



先来看一下调用ThreadLocal的set方法时,底层的实现原理。

  • a、首先获取线程的ThreadLocalMap,暂且认为这是一个特殊的Map。其中,key为ThreadLocal对象,value为实际的变量值。

  • b、如果map不为null,则直接设置变量值。

  • c、如果map为null,则创建map,并设置变量值。


public void set(T value) {    // 获取当前线程 Thread t = Thread.currentThread();    // a、获取线程的ThreadLocalMap,暂且认为ThreadLocalMap就是一个特殊的Map ThreadLocalMap map = getMap(t); if (map != null)        // b、map不为null时,直接设置变量值,key-ThreadLocal对象,value-线程的变量值 map.set(this, value); else        // c、map为null时,创建map,并设置变量值 createMap(t, value);}
ThreadLocalMap getMap(Thread t) { return t.threadLocals;}
void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue);}


通过源码可知,ThreadLocal的set方法,依赖ThreadLocalMap的set方法。至于ThreadLocalMap的原理,后面将会详细进行介绍。


  3    ThreadLocal的get方法



之后看一下调用ThreadLocal的get方法时,底层的实现原理。

  • a、首先获取线程的ThreadLocalMap,暂且认为这是一个特殊的Map。其中,key为ThreadLocal对象,value为实际的变量值。

  • b、如果map不为null,则直接获取变量值。否则执行步骤d。

  • c、如果变量值不为null,则进行强制类型转换,并将变量值返回。

  • d、如果map为null,或者变量值为null,则创建map或初始值,并返回初始值。


public T get() { // 获取当前线程 Thread t = Thread.currentThread(); // a、获取线程的ThreadLocalMap,暂且认为ThreadLocalMap就是一个特殊的Map ThreadLocalMap map = getMap(t); if (map != null) {        // b、map不为null时,直接通过get获取变量值,key-ThreadLocal对象 ThreadLocalMap.Entry e = map.getEntry(this);        // c、变量值不为null时,进行强制类型转换。并返回变量值 if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } }
    // d、map为null,或者变量值为null,返回初始值 return setInitialValue();}
private T setInitialValue() { // 获取初始值 T value = initialValue(); // 获取当前线程 Thread t = Thread.currentThread(); // 获取线程的ThreadLocalMap ThreadLocalMap map = getMap(t); if (map != null)        // map不为null时,直接设置变量值,key-ThreadLocal对象,value-线程的变量值 map.set(this, value); else        // map为null时,创建map,并设置变量值 createMap(t, value); return value;}

// 此方法可根据实际需要重写protected T initialValue() { return null;}


通过源码可知,ThreadLocal的get方法,依赖ThreadLocalMap的getEntry方法。那么ThreadLocalMap究竟是什么呢?


  4    ThreadLocalMap



ThreadLocalMap是ThreadLocal的内部类,其作用类似于Map。其内部元素类型为Entry,Entry继承了弱引用。


static class Entry extends WeakReference<ThreadLocal<?>> { Object value;
Entry(ThreadLocal<?> k, Object v) { super(k); value = v; }}


ThreadLocalMap主要的成员变量如下所示:


// 初始容量,默认为16private static final int INITIAL_CAPACITY = 16;// 数组,用来存放元素private Entry[] table;// 元素数量private int size = 0;// 下次扩容的阈值private int threshold;


ThreadLocalMap主要的成员变量与HashMap类似。其主要的方法也与HashMap类似。



接下来,重点关注以下3个方法:

  • 带key、value的构造方法

  • set方法

  • getEntry方法


  5    ThreadLocalMap的构造方法



ThreadLocalMap带key、value的构造方法源码如下所示:


ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { // 初始化table,默认容量为16 table = new Entry[INITIAL_CAPACITY]; // 计算key的位置,方法为key的hashCode、(数组容量-1)做与运算 int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); // 构建Entry,并放置在table中 table[i] = new Entry(firstKey, firstValue); // size设置为1 size = 1; // 设置下次扩容的阈值 setThreshold(INITIAL_CAPACITY);}
private void setThreshold(int len) {    // 下次扩容的阈值为len的2/3 threshold = len * 2 / 3;}


主要过程与HashMap类似,唯一不同的地方为计算元素的位置。ThreadLocalMap的key为ThreadLocal对象,所以threadLocalHashCode本质上是ThreadLocal的属性。正常只会在ThreadLocal创建时被赋值


// threadLocalHashCode调用了nextHashCode方法private final int threadLocalHashCode = nextHashCode();// 原子Integer变量,static变量private static AtomicInteger nextHashCode = new AtomicInteger();// 一个神奇的数字private static final int HASH_INCREMENT = 0x61c88647;private static int nextHashCode() { // nextHashCode的基础上,加HASH_INCREMENT,也就是上面那个神奇的数字 return nextHashCode.getAndAdd(HASH_INCREMENT);}


确定了元素的位置后,将元素放在数组table的对应位置即可。


  6    ThreadLocalMap的set方法



向ThreadLocalMap添加元素的源码如下所示。主要过程为:

  • a、获取元素的位置,获取方法为key的hashCode、(数组容量-1)做与运算。

  • c、如果数组对应位置entry的key为当前ThreadLocal,则直接返回。

  • d、如果数组对应位置entry的key为null,则替代此entry,并返回。至于为何entry的key会为空,将在下篇文章详细介绍。

  • e、将元素放在table的相应位置。

  • f、清空数组table的某些位置或者进行扩容,将在下篇文章详细介绍。


private void set(ThreadLocal<?> key, Object value) { // 数组table Entry[] tab = table; // 数组table的容量 int len = tab.length; // a、获取元素的位置(数组下标),获取方法为key的hashCode、(数组容量-1)做与运算 int i = key.threadLocalHashCode & (len-1);
    // b、如果数组对应位置非空,且不满足return条件    // 使用开放地址法循环获取数组的下一个位置 for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { // 获取entry的key ThreadLocal<?> k = e.get();
// c、entry的key就是当前的ThreadLocal,直接返回 if (k == key) { e.value = value; return; }        // d、entry的key为null,替代此entry        // 此处的具体逻辑会放在下篇文章详细介绍 if (k == null) { replaceStaleEntry(key, value, i); return; } }
// e、此时数组对应位置为空,将元素放在此位置 tab[i] = new Entry(key, value); // 元素数量+1 int sz = ++size; // f、清空数组table某些位置,或者进行扩容 if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash();}
private static int nextIndex(int i, int len) { // 如果到达数组的结束位置,则再从0开始 // 不用担心数组全被占满,因为扩容阈值是数组容量的2/3 return ((i + 1 < len) ? i + 1 : 0);}


通过上述步骤,value就放在线程的ThreadLocalMap中的某一位置上。entry的key即为ThreadLocal对象。


  7    ThreadLocalMap的getEntry方法



在ThreadLocalMap中查找元素的源码如下所示。主要过程为:

  • a、获取元素的位置,获取方法为key的hashCode、(数组容量-1)做与运算。

  • b、如果数组对应位置entry的key就是当前的ThreadLocal,则直接返回。

  • c、如果数组对应位置的entry为空,或者entry的key不是当前的ThreadLocal,则执行步骤d。


private Entry getEntry(ThreadLocal<?> key) {    // a、获取元素的位置(数组下标),获取方法为key的hashCode、(数组容量-1)做与运算 int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; if (e != null && e.get() == key) // b、entry的key就是当前的ThreadLocal,直接返回 return e; else // c、entry为空,或者entry的key不是当前的ThreadLocal return getEntryAfterMiss(key, i, e);}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { // 数组table Entry[] tab = table; // 数组table长度 int len = tab.length;
    // d、通过开放地址法,对比entry的key是不是当前ThreadLocal while (e != null) { ThreadLocal<?> k = e.get(); if (k == key) // entry的key就是当前ThreadLocal,直接返回 return e; if (k == null) // 协助清理脏entry expungeStaleEntry(i); else // 获取数组下个位置 i = nextIndex(i, len); e = tab[i]; }
// e、entry为null,直接返回null return null;}


通过上述步骤,即可在线程的ThreadLocalMap中,完成元素的查找。如果能够查找到,就返回元素。如果查找不到,就返回null。


  8    小结



至此本文完成了ThreadLocal中,set、get基本原理的分析。但是,仍有几个需要关注的问题:

  • 为什么ThreadLocalMap中的Entry要继承弱引用?

  • ThreadLocalMap如何进行扩容?

  • 如果一个ThreadLocal不再使用,那线程的ThreadLocalMap中的对应entry该如何销毁?是否会引起内存泄漏?

  • 主线程的数据能否携带至子线程中?

  • ……


笔者将在下篇文章详细介绍,敬请期待。


最后,祝各位读者朋友工作顺利、心想事成。