前言
在开始之前先看下代码如下:
ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();
new Thread(() -> {
stringThreadLocal.set("测试");
System.out.println("子线程获取:" + stringThreadLocal.get());
}).start();
Thread.sleep(200);
System.out.println("主线程获取:" + stringThreadLocal.get());
复制代码
输出如下:
子线程获取:测试
主线程获取:null
复制代码
是不是感觉哪里不对劲。按道理,如果是其他的类似于集合的数据结构,我只要在子线程进行了赋值操作,又在主线程执行了sleep()操作那么主线程应该是可以获取到最新的赋值的,比如将上面ThreadLocal改为ArraList,代码如下:
ArrayList<String> arrayList = new ArrayList<>();
new Thread(() -> {
arrayList.add("测试");
System.out.println("子线程获取:" + arrayList.get(0));
}).start();
Thread.sleep(200);
System.out.println("主线程获取:" + arrayList.get(0));
复制代码
输出如下:
子线程获取:测试
主线程获取:测试
复制代码
再看下如下代码:
ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();
stringThreadLocal.set("测试");
Thread.sleep(200);
new Thread(() -> {
System.out.println("子线程获取:" + stringThreadLocal.get());
}).start();
System.out.println("主线程获取:" + stringThreadLocal.get());
复制代码
输出结果如下:
主线程获取:测试
子线程获取:null
复制代码
通过以上三个代码片段,似乎可以发现一些端倪,那就是ThreadLoal通过set()操作进行的赋值操作的作用范围似乎只能是在当前线程,那它是如何实现这种线程隔离的效果的呢? Talk is cheap,show me the fuck code.
正文
正文主要分亮大部分ThreadLocal本身的函数、与ThreadLocal息息相关的ThreadLocalMap
ThreadLocal本身
先看构造函数:
public ThreadLocal() {
}
复制代码
没啥特殊的,那就看下set()函数:
set()函数
public void set(T value) {
//注释1
Thread t = Thread.currentThread();
//注释2
ThreadLocalMap map = getMap(t);
if (map != null)
//注释4
map.set(this, value);
else
//注释3
createMap(t, value);
}
复制代码
注释1处获取当前线程t,然后把t当做参数通过getMap,获取ThreadLocalMap,如下:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
复制代码
返回的是是Thread的一个成员变量,那我们就来看看threadLocals初始化和赋值的地方如下:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
复制代码
可以发现threadLocals是在ThreadLocal#createMap()里面赋值的,再回到代码片段4的注释2显然一开始这里的map为null,逻辑走到了注释3的createMap(),也就是在这里完成了threadLocals的初始化以及赋值。关于ThreadLocalMap晚点再看,我们趁热打铁来看get()
get()
get()函数如下:
public T get() {
//注释1
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
//注释2
if (map != null) {
//注释3
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//注释4
return setInitialValue();
}
复制代码
和set()一样,先获取当前线程然后通过getMap()获取ThreadLocalMap,因为前面已经调用过来set()函数,所以map不等于null,走到注释3,通过 ThreadLocalMap#getEntry()获取Entry并返回Entry的value,如果map等于走到注释4的逻辑 如下:
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;
}
复制代码
这里可以看做是走了以便set(null)的操作,并返回空值
小结
通过以上set()和get()方法的分析能够知道,两者都需要调用getMap()拿到threadLocals,再通过threadLocals去取值或者赋值,而threadLocals是Thread的成员变量,不同线程实例有自己的hreadLocals,天然形成了线程隔离,这也就能回答文章开头提出的问题。
ThreadLocalMap
通过源码分析知道ThreadLoal的各种操作与ThreadLocalMap紧密相关,那接下来我们看看ThreadLocalMap的构造函数
构造函数
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//注释1 初始化数组
table = new Entry[INITIAL_CAPACITY];
//注释2 通过hash与INITIAL_CAPACITY取模(对一个2的整次幂的减一数进行&操作相当于取模)获取firstKey在数组的位置i
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
//注释3 插入数据
table[i] = new Entry(firstKey, firstValue);
size = 1;
注释4 设置扩容阈值
setThreshold(INITIAL_CAPACITY);
}
复制代码
通过构造函数底层的存储结构是个数组,通过对 ThreadLocal 的hash值与默认容量取模获取到在数组的位置,插入、更新、删除、数据,并且在必要的时候进行扩容
Entry
看下Entry这个类
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
复制代码
可以看到Entry继承自WeakReference<ThreadLocal<?>>,是个弱引用,也就是再内存不足的时候,触发GC会被回收
插入数据
从代码片段4我们知道,ThreadLocal#set()函数,会调用到代码片段4注释4出把自身当做key,传入到ThreadLocalMap#set(),如下:
代码片段10
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
//注释1 从当前位置往后插桩适合插入的位置
for (Entry e = tab[i];
e != null;
//注释2 nextIndex到达尾部之后,会回到开头
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
//注释3 数组中已经存在,直接更新返回
e.value = value;
return;
}
//注释4 Entry不为空,key已经被GC
if (k == null) {
// 注释5 把脏数据替换为新值
replaceStaleEntry(key, value, i);
return;
}
}
//注释6 找到一个空的位置,插入数据
tab[i] = new Entry(key, value);
// 注释7size 加1
int sz = ++size;
//清理脏数据,判断是否是需要扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
复制代码
以上代码可知,ThreadLocalMap#set()的关键是找到合适的位置,首先如果数组中通过key的hash与数组取模得到的索引i,如果i这个位置是空的,那么直接加入,否则循环查找如果找到存在key值一样直接更新,或者找到一个脏数据那就直接替换,否则就找最靠近i的位置,进行插入: 流程图如下:
读取数据
代码片段11
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
复制代码
逻辑很简单,不过多解释
小结
通过以上对ThreadLocalMap的源码分析,相信对ThreadLocalMap有了初步了解,其实它的底层数据结构就是一个数组,通过开放地址法解决hash冲突。
总结
考虑到本文行文已经过长,决定对ThreadLocalMap的删除数据、扩容缩容以及清除脏数据等细节实现,单独开篇,敬请期待