源代码系列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就是一个特殊的MapThreadLocalMap 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就是一个特殊的MapThreadLocalMap map = getMap(t);if (map != null) {// b、map不为null时,直接通过get获取变量值,key-ThreadLocal对象ThreadLocalMap.Entry e = map.getEntry(this);// c、变量值不为null时,进行强制类型转换。并返回变量值if (e != null) {T result = (T)e.value;return result;}}// d、map为null,或者变量值为null,返回初始值return setInitialValue();}private T setInitialValue() {// 获取初始值T value = initialValue();// 获取当前线程Thread t = Thread.currentThread();// 获取线程的ThreadLocalMapThreadLocalMap 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,默认容量为16table = new Entry[INITIAL_CAPACITY];// 计算key的位置,方法为key的hashCode、(数组容量-1)做与运算int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);// 构建Entry,并放置在table中table[i] = new Entry(firstKey, firstValue);// size设置为1size = 1;// 设置下次扩容的阈值setThreshold(INITIAL_CAPACITY);}private void setThreshold(int len) {// 下次扩容的阈值为len的2/3threshold = 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) {// 数组tableEntry[] 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的keyThreadLocal<?> 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);// 元素数量+1int sz = ++size;// f、清空数组table某些位置,或者进行扩容if (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();}private static int nextIndex(int i, int len) {// 如果到达数组的结束位置,则再从0开始// 不用担心数组全被占满,因为扩容阈值是数组容量的2/3return ((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不是当前的ThreadLocalreturn getEntryAfterMiss(key, i, e);}private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {// 数组tableEntry[] tab = table;// 数组table长度int len = tab.length;// d、通过开放地址法,对比entry的key是不是当前ThreadLocalwhile (e != null) {ThreadLocal<?> k = e.get();if (k == key)// entry的key就是当前ThreadLocal,直接返回return e;if (k == null)// 协助清理脏entryexpungeStaleEntry(i);else// 获取数组下个位置i = nextIndex(i, len);e = tab[i];}// e、entry为null,直接返回nullreturn null;}
通过上述步骤,即可在线程的ThreadLocalMap中,完成元素的查找。如果能够查找到,就返回元素。如果查找不到,就返回null。
8 小结
至此本文完成了ThreadLocal中,set、get基本原理的分析。但是,仍有几个需要关注的问题:
为什么ThreadLocalMap中的Entry要继承弱引用?
ThreadLocalMap如何进行扩容?
如果一个ThreadLocal不再使用,那线程的ThreadLocalMap中的对应entry该如何销毁?是否会引起内存泄漏?
主线程的数据能否携带至子线程中?
……
笔者将在下篇文章详细介绍,敬请期待。
最后,祝各位读者朋友工作顺利、心想事成。
