作者:寒空动烟雪 | 来源:互联网 | 2024-10-10 18:41
JavaThreadLocal原理分析-1简述ThreadLocal提供了线程本地变量,它可以保证访问到的变量属于当前线程,每个线程都保存有一个变量副本,每个线程的变量都不同,而
1 简述
ThreadLocal
提供了线程本地变量,它可以保证访问到的变量属于当前线程,每个线程都保存有一个变量副本,每个线程的变量都不同,而同一个线程在任何时候访问这个本地变量的结果都是一致的。当此线程结束生命周期时,所有的线程本地实例都会被 GC。ThreadLocal 相当于提供了一种线程隔离,将变量与线程相绑定。
2 原理
ThreadLocal是一种数据结构,有点像map,但是一个ThreadLocal只能保存一个。
每个线程有一个 ThreadLocalMap 成员变量,本质是一个 map。map 里存储的 key 是一个弱引用,其包装了当前线程中构造的 ThreadLocal 对象,这意味着,只要 ThreadLocal 对象丢掉了强引用,那么在下次 GC 后,map 中的 ThreadLocal 对象也会被清除,对于那些ThreadLocal 对象为空的 map 元素,会当为垃圾,稍后会被主动清理。map 里存储的 value 就是缓存到当前线程的值,这个 value 没有弱引用去包装,需要专门的释放策略。
3 核心方法
get()
/**
* 返回当前 ThreadLocal 对象关联的值
*
* @return
*/
public T get() {
// 返回当前 ThreadLocal 所在的线程
Thread t = Thread.currentThread();
// 从线程中拿到 ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
// 从 map 中拿到 entry
ThreadLocalMap.Entry e = map.getEntry(this);
// 如果不为空,读取当前 ThreadLocal 中保存的值
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T) e.value;
return result;
}
}
// 若 map 为空,则对当前线程的 ThreadLocal 进行初始化,最后返回当前的 ThreadLocal 对象关联的初值,即 value
return setInitialValue();
}
set()
/**
* 为当前 ThreadLocal 对象关联 value 值
*
* @param value 要存储在此线程的线程副本的值
*/
public void set(T value) {
// 返回当前 ThreadLocal 所在的线程
Thread t = Thread.currentThread();
// 返回当前线程持有的map
ThreadLocalMap map = getMap(t);
if (map != null) {
// 如果 ThreadLocalMap 不为空,则直接存储键值对
map.set(this, value);
} else {
// 否则,需要为当前线程初始化 ThreadLocalMap,并存储键值对
createMap(t, value);
}
}
remove()
/**
* 清理当前 ThreadLocal 对象关联的键值对
*/
public void remove() {
// 返回当前线程持有的 map
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) {
// 从 map 中清理当前 ThreadLocal 对象关联的键值对
m.remove(this);
}
}
可以发现,不管是get、set、remove操作的都是当前线程持有的ThreadLocalMap。
每个线程都有一个自己的ThreadLocalMap。
所以如果线程A set
值到map中,线程B又 set
,两者是不会互相影响的。
4 ThreadLocalMap
最后,我们看看ThreadLocalMap的实现。
/**
* 键值对实体的存储结构
*/
static class Entry extends WeakReference> {
//当前线程关联的 value,这个 value 并没有用弱引用追踪
Object value;
/**
* 构造键值对
*
* @param k k 作 key,作为 key 的 ThreadLocal 会被包装为一个弱引用
* @param v v 作 value
*/
Entry(ThreadLocal> k, Object v) {
super(k);
value = v;
}
}
WeakReference保证GC时能够回收线程set的值。
// 返回 key 关联的键值对实体
private Entry getEntry(ThreadLocal> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
// 若 e 不为空,并且 e 的 ThreadLocal 的内存地址和 key 相同,直接返回
if (e != null && e.get() == key) {
return e;
} else {
// 从 i 开始向后遍历找到键值对实体
return getEntryAfterMiss(key, i, e);
}
}
// 在 map 中存储键值对
private void set(ThreadLocal> key, Object value) {
Entry[] tab = table;
int len = tab.length;
// 计算 key 在数组中的下标
int i = key.threadLocalHashCode & (len - 1);
// 遍历一段连续的元素,以查找匹配的 ThreadLocal 对象
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
// 获取该哈希值处的ThreadLocal对象
ThreadLocal> k = e.get();
// 键值ThreadLocal匹配,直接更改map中的value
if (k == key) {
e.value = value;
return;
}
// 若 key 是 null,说明 ThreadLocal 被清理了,直接替换掉
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 直到遇见了空槽也没找到匹配的ThreadLocal对象,那么在此空槽处安排ThreadLocal对象和缓存的value
tab[i] = new Entry(key, value);
int sz = ++size;
// 如果没有元素被清理,那么就要检查当前元素数量是否超过了容量阙值(数组大小的三分之二),以便决定是否扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold) {
// 扩容的过程也是对所有的 key 重新哈希的过程
rehash();
}
}
从源码中我们可以看到,ThreadLocalMap和HashMap有点相似,但是ThreadLocalMap没有采用数组+链表(红黑树)的方式解决hash冲突,而是采用线性探测寻址法
。
5 总结
每个Thread都有一个ThreadLocalMap成员变量,这个变量和ThreadLocal绑定在一起,很好的给线程提供了独享数据的地方。
对比多线程资源共享,采用同步机制(以时间换空间),此方式是以空间换时间,性能比较好。
不过使用ThreadLocal的时候,还需要注意内存泄露的问题,虽然是采用WeakReference,但是如果线程一直运行还是可能导致内存泄露,所以最好是使用完后 remove
一下。