ThreadLocal底层原理笔记
最近学习小傅哥的面经手册,学习到ThreadLocal,这里做个笔记加深印象,也方便日后复习。
ThreadLocal是除了加锁这种同步方式之外的一种规避多线程访问出现线程不安全的方法,它的核心思想是:共享变量在每个线程都有一个副本,每个线程操作的都是自己的副本,对另外的线程没有影响。
一、ThreadLocal简单使用
public class ThreadLocalDemo {
static ThreadLocal<String> localVar = new ThreadLocal<>();
static void print(String str){
//打印当前线程中本地内存中本地变量的值
System.out.println(str + " : " + localVar.get());
//清除本地内存中的本地变量
localVar.remove();
}
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
localVar.set("localVar1");
print("localVar1");
//打印本地变量
System.out.println("after remove : " + localVar.get());
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
localVar.set("localVar2");
print("localVar2");
//打印本地变量
System.out.println("after remove : " + localVar.get());
}
});
thread1.start();
thread2.start();
}
}
打印结果:
localVar1 : localVar1
after remove : null
localVar2 : localVar2
after remove : null
一、ThreadLocal结构
Thread类中有ThreadLocalMap类的成员变量threadLocals
,这变量由ThreadLoacl类维护。
ThreadLocalMap是ThreadLocal类的静态内部类,内部有一个Entry静态内部类,继承了WeakReference<ThreadLocal<?>>
。所以存储在Entry
中的ThreadLocal
键是弱引用。
弱引用:当一个对象仅仅被weak reference指向, 而没有任何其他strong reference指向的时候, 如果GC运行, 那么这个对象就会被回收。
ThreadLocalMap内部还定义了一个成员变量Entry[] table
。
所以ThreadLocal的整体结构:
【图片来源】:ThreadLocal一个线程只能存放一个变量吗?想存多个怎么搞? - 苏三说技术的回答 - 知乎
【图片来源】:ThreadLocal一个线程只能存放一个变量吗?想存多个怎么搞? - 苏三说技术的回答 - 知乎
二、如何存放元素
ThreadLocal
存放数据的底层数据结构:
图片来源:面经手册 · 第12篇《面试官,ThreadLocal 你要这么问,我就挂了!》 | 小傅哥 bugstack 虫洞栈
ThreadLocal
使用的是斐波那契(Fibonacci)散列法 + 开发寻址存储数据到数组结构Entry[] table
中。
ThreadLocale类 set(T value)
源码流程:
-
先获取当前线程的
threadLocals
(ThreadLocalMap类),-
if(map != null)
如果threadLocals
不为空直接向threadLocals
中添加元素map.set(this, value);
-
else
,否则就为该线程创建一个ThreadLocalMap对象,并将元素存放进去,赋给threadLocals
ThreadLocal
set(T value)
源码如下:
-
-
map.set(this, value);
向
map
即threadLocals
中添加元素,其实操作的是map中的Entry数组table
。ThreadLocal
是基于数组结构的开放寻址方式存储,那就一定会有哈希的计算,利用key值即当前ThreadLocal对象的threadLocalHashCode
值计算下标,int i = key.threadLocalHashCode & (len-1);
。如果当前下标:
-
是空位置直接插入
-
不为空,key 相同,直接更新
-
不为空,key 不相同,开放寻址,
e = tab[i = nextIndex(i, len)]
,也就是同一个下标位置发生冲突时,则 +1向后寻址,直到找到空位置或垃圾回收位置进行存储。 -
不为空,
key == null
,碰到过期key。遇到的是弱引用发生GC时产生的情况。碰到这种情况,ThreadLocal
会进行探测清理过期key,replaceStaleEntry()
,探测式清理过期元素
之后的以下这段代码会判断是否需要进行扩容。
int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash();
ThreadLocalMap类
set(ThreadLocal<?> key, Object value)
源码: -
-
关于ThreadLocal类的
threadLocalHashCode
属性:ThreadLocal
使用的是斐波那契(Fibonacci)散列法计算哈希值。0x61c88647
,这是一个哈希值的黄金分割点,让散列分布更分散减少哈希碰撞。
流程图:(这图中拉链法,感觉该改为开放寻址法的线性探测?)
图片来源:面经手册 · 第12篇《面试官,ThreadLocal 你要这么问,我就挂了!》 | 小傅哥 bugstack 虫洞栈
三、扩容机制
ThreadLocalMap类 set(ThreadLocal<?> key, Object value)
源码的最后一段代码:
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
这段代码的源码流程:
-
进行启发式清理,
cleanSonmeSlots()
调用expungeStaleEntry()
清除过期元素。返回boolean值表示是否有清理过期元素,false
:未清理,true
:清理了过期元素。源码如下:
-
判断
sz >= threshold
,其中threshold = len * 2 / 3
,也就是说数组中填充的元素,大于len * 2 / 3
,并且如果执行完启发式清理工作后,未清理到任何数据,就需要扩容了。 -
rehash()
,扩容并重新计算元素位置。rehash()
方法中先是调用了expungeStaleEntries()
,探测式清理过期元素,以及判断清理后是否满足扩容条件,size >= threshold * 3/4。满足后执行扩容操作
resize()
,把旧数组中的元素重新散列分布到新的数组中。rehash()
源码如下:
真正的进行扩容的操作resize()
:
-
创建一个容量为原来2倍大小的新
Entry[]
。 -
遍历原来的Entry数组
table
,获取entry元素的对应的ThreadLocal,即key值。 -
检测key值的
if (k == null)
,将对应value=null
,方便GC。 -
通过key值
ThreadLocal
的threadLoaclHashCode
,重新计算下标,重新放到新数组中。 -
在放置数组的过程中,如果发生哈希碰撞,则调用
nextIndex()
,向后寻址。
resize()
源码如下:
四、如何获取元素
ThreadLocal类 get()
方法源码流程:
-
先获取threadLoacls即map
-
如果
map != null
,获取当前ThreadLocal对象对应的Entry对象,map.getEntry(this)
。ThreadLocal
get()
源码如下: -
map.getEntry(this)
:-
通过ThreadLocal对象的
threadLocalHashCode
,计算该ThreadLocal对象对应的Entry对象在Entry数组table
中的下标,定位到对应的Entry对象; -
直接定位到,没有哈希冲突,直接返回元素即可。
if (e != null && e.get() == key)
; -
没有直接定位到,
return getEntryAfterMiss(key, i, e)
,获取Entry对象。
ThreadLoaclMap类
getEntry(ThreadLocal<?>): Entry
源码如下: -
-
getEntryAfterMiss(key, i, e)
:-
key不同,需要开放寻址,
i = nextIndex(i, len);
。 -
key不同,开放寻址,遇到GC清理元素,需要探测式清理,再寻找元素,
if (k == null) expungeStaleEntry(i);
。
ThreadLoaclMap类
getEntryAfterMiss(ThreadLocal<?>, int, Entry): Entry
源码如下:
-
五、清理元素
探测式清理[expungeStaleEntry]
探测式清理,是以当前遇到的 GC 元素开始,向后不断的清理。直到遇到 null 为止,才停止 rehash 计算Rehash until we encounter null
, for(...; (e = tab[i]) != null; ...)
。
源码流程:
for循环中:
-
获取当前Entry对象,对应的ThreadLocal对象,即Key值,
ThreadLocal<?> k = e.get();
。 -
如果key值为null,已经被GC回收,清理,
e.value = null; tab[i] = null;
-
如果key值不为null,计算下标。如果和当前在tab中的下标不同,将该Entry对象,放至新的下标位置。如果有哈希冲突,开放寻址。
int h = k.threadLocalHashCode & (len - 1); if (h != i) { tab[i] = null; // Unlike Knuth 6.4 Algorithm R, we must scan until // null because multiple entries could have been stale. while (tab[h] != null) h = nextIndex(h, len); tab[h] = e; }
ThreadLoaclMap类 expungeStaleEntry(int): int
源码如下:
启发式清理[cleanSomeSlots]
启发式清理,有这么一段注释,大概意思是;试探的扫描一些单元格,寻找过期元素,也就是被垃圾回收的元素。当添加新元素或删除另一个过时元素时,将调用此函数。它执行对数扫描次数,作为不扫描(快速但保留垃圾)和与元素数量成比例的扫描次数之间的平衡,这将找到所有垃圾,但会导致一些插入花费O(n)时间。
while 循环中不断的右移进行寻找需要被清理的过期元素,最终都会使用 expungeStaleEntry
进行处理,这里还包括元素的移位。
ThreadLoaclMap类 cleanSomeSlots(int, int): boolean
源码如下:
问题:
-
为什么不直接重写hashcode(),而是定义
threadLocalHashCode
成员变量? -
为什么 Entry 使用弱引用?
参考资料
ThreadLocal一个线程只能存放一个变量吗?想存多个怎么搞? - 苏三说技术的回答 - 知乎