ThreadLocal实现主要涉及Thread,ThreadLocal,ThreadLocalMap这三个类。
作用
ThreadLocal和Synchronized都是为了解决多线程中相同变量的访问冲突问题,不同的点是:
- Synchronized是通过线程等待,
牺牲时间来解决访问冲突
- ThreadLocal是通过每个线程单独一份存储空间,
牺牲空间来解决冲突
- 并且相比于Synchronized,ThreadLocal具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问到想要的值。
正因为ThreadLocal的线程隔离特性,使他的应用场景相对来说更为特殊一些。当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,就可以考虑采用ThreadLocal。
public方法
ThreadLocal只提供了4个public方法
- get
- remove
- set
withInitial:Creates a thread local variable,类型是SuppliedThreadLocal
protected方法
自身、子类及同一个包中类可以访问
-
default方法
同一包中的类可以访问
childValue
- createInheritedMap:静态方法,这个方法仅被Thread构造方法调用。
- createMap:get/set方法中调用,get方法中,getMap不存在时,用来创建map
getMap:返回Thread关联的threadLocals
内部类ThreadLocalMap
ThreadLocalMap
内维护了一个数组:Entry[] table
,Entry
是ThreadLocalMap
的内部类,继承了WeakReference
。Thread&ThreadLocal
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class.
*/
ThreadLocal.ThreadLocalMap threadLocals = null;
/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
从以上代码我们了解到,一个Thread拥有一个ThreadLocalMap,假设我们在一个线程里声明了3个ThreadLocal变量:
ThreadLocal<Integer> tl1 = new ThreadLocal<>();
ThreadLocal<String> tl2 = new ThreadLocal<>();
ThreadLocal<Double> tl3 = new ThreadLocal<>();
可以知道这个三个变量tl1、tl2、tl3都是存储在同一个map里的。实际上,它们存储在一个数组的不同位置,而这个数组就是上面提到的Entry型的数组table。ThreadLocalMap是如何计算索引位置的呢?
//ThreadLocalMap中set方法。
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
Entry[] tab = table;
int len = tab.length;
//获取索引值,这个地方是比较特别的地方
int i = key.threadLocalHashCode & (len-1);
//遍历tab如果已经存在则更新值
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
//如果上面没有遍历成功则创建新值
tab[i] = new Entry(key, value);
int sz = ++size;
//满足条件数组扩容x2
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
在ThreadLocalMap中的set方法与构造方法能看到以下代码片段。
int i = key.threadLocalHashCode & (len-1)
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1)
简而言之就是将threadLocalHashCode进行一个位运算(取模)得到索引i
因为static的原因,在每次new ThreadLocal时因为threadLocalHashCode的初始化,会使threadLocalHashCode值自增一次,增量为//ThreadLocal中threadLocalHashCode相关代码.
private final int threadLocalHashCode = nextHashCode();
/**
* The next hash code to be given out. Updated atomically. Starts at
* zero.
*/
private static AtomicInteger nextHashCode = new AtomicInteger();
/**
* The difference between successively generated hash codes - turns
* implicit sequential thread-local IDs into near-optimally spread
* multiplicative hash values for power-of-two-sized tables.
*/
private static final int HASH_INCREMENT = 0x61c88647;
/**
* Returns the next hash code.
*/
private static int nextHashCode() {
//自增
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
0x61c88647
。0x61c88647
是斐波那契散列乘数,它的优点是通过它散列(hash)出来的结果分布会比较均匀,可以很大程度上避免hash冲突。
总结如下:
- 对于某一个ThreadLocal 变量来讲,它的索引值i是确定的。
- 不同线程访问同一个ThreadLocal变量,实际访问的是不同线程的threadLocals,即不同的table数组的同一位置即都为table[i]。
- 对于同一线程的不同ThreadLocal来讲,这些ThreadLocal实例共享一个table数组,然后每个ThreadLocal实例在table中的索引i是不同的。
魔数0x61c88647
约等于0.618
ThreadLocal
中使用了斐波那契散列法,来保证哈希表的离散度。而它选用的乘数值即是2^32 * 黄金分割比
https://zhuanlan.zhihu.com/p/40515974
扩容机制
set时扩容
//ThreadLocalMap的set方法 private void set(ThreadLocal<?> key, Object value) {
//......略
// 扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
rehash源码:
private void rehash() { expungeStaleEntries();//删除过期的元素 // Use lower threshold for doubling to avoid hysteresis if (size >= threshold - threshold / 4) //threshold = len * 2 / 3;初始容量16,size>=10就可以扩容了。 resize(); }
数组容量增加1倍:
private void resize() { Entry[] oldTab = table; int oldLen = oldTab.length; int newLen = oldLen * 2; Entry[] newTab = new Entry[newLen]; int count = 0;
for (int j = 0; j < oldLen; ++j) { Entry e = oldTab[j]; if (e != null) { ThreadLocal<?> k = e.get(); if (k == null) { e.value = null; // Help the GC } else { int h = k.threadLocalHashCode & (newLen - 1); while (newTab[h] != null) h = nextIndex(h, newLen); newTab[h] = e; count++; } } } setThreshold(newLen); size = count; table = newTab; }
注意事项
通常弱引用都会和引用队列配 合清理机制使用,但是 ThreadLocal 是个例外,它并没有这么做。这意味着,废弃项目的回收依赖于显式地触发,否则就要等待线程结束,进而回收相应 ThreadLocalMap!这就是很多 OOM 的来源,所以通常都会建议,应用一定要自己负责 remove
,并且不要和线程池配合
,因为 worker 线程往往是不会退出的。
常见问题
- 为什么要用ThreadLocal?
解决高并发线程安全``空间换时间
- ThreadLocal的原理是什么?
源码
- 为什么用ThreadLocal做key?
- Entry的key为什么设计成弱引用?
内存泄漏
- ThreadLocal真的会导致内存泄漏吗?
会
- 如何解决内存泄漏?
set、get时``remove
- ThreadLocal是如何定位数据的?
hashCode
- ThreadLocal是如何扩容的?`容量的2/3``set结束```
- 父子线程如何共享ThreadLocal数据?
可继承的ThreadLocal
- 线程池中如何共享ThreadLocal数据?
TransmittableThreadLocal,阿里开源
- ThreadLocal有哪些用处?
Spring事务``日期``用户上下文``临时保存权限信息``日志