源代码系列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 content
before set, thread2 result : null
after 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) {
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主要的成员变量如下所示:
// 初始容量,默认为16
private 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该如何销毁?是否会引起内存泄漏?
主线程的数据能否携带至子线程中?
……
笔者将在下篇文章详细介绍,敬请期待。
最后,祝各位读者朋友工作顺利、心想事成。