ThreadLocal
不同的线程基于同一个ThreadLocal对象可以拿到不同的值,很容易想到的策略就是map中维护不同的键值对,键就是线程的id,新增/销毁线程,修改值都需要操作这个一个map,并发的情况下肯定会有问题。所以需要加锁。但性能就会受到影响,我们看下jdk是怎么实现的
示例
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
ThreadLocal<String> threadLocalA = new ThreadLocal<>();
threadLocalA.set("qwe");
IntStream.range(1, 16).forEach(i -> {
ThreadLocal<String> temp = new ThreadLocal<>();
});
ThreadLocal<String> threadLocalB = new ThreadLocal<>();
threadLocalB.set("qwe");
}).start();
}
1. ThreadLocal 与 ThreadLocalMap
ThreadLocal构造方法
public class ThreadLocal<T> {
//每个ThreadLocal都有一个唯一的threadLocalHashCode
// ThreadLocal初始化时会调用下方nextHashCode()获取唯一hash值,在全局范围内递增0x61c88647
private final int threadLocalHashCode = nextHashCode();
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.
*/
//注释表明:通过 0x61c88647 累加生成的 threadLocalHashCode 与 2 的幂取模,
//得到的结果可以较为均匀地分布在长度为 2 的幂大小的数组中
//那么这个数组的作用 ?
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
}
我们看我们常用的api
ThreadLocal#set
public void set(T value) {
Thread t = Thread.currentThread();
//取出线程自带的threadLocals 我们先看下 下面的ThreadLocalMap是什么结构
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
//可能存在ThreadLocalMap为空的情况 那么进行初始化
//调用的就是 new ThreadLocalMap(this, firstValue); 构造方法
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
//取线程自带的threadLocals属性 ThreadLocal.ThreadLocalMap threadLocals = null,其初始值是为空的
return t.threadLocals;
}
ThreadLocal.ThreadLocalMap
static class ThreadLocalMap {
//Entry继承自弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
//key是ThreadLocal
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
//初始长度为16
private static final int INITIAL_CAPACITY = 16;
private Entry[] table;
}
ThreadLocalMap构造方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//16长度Entry数组
table = new Entry[INITIAL_CAPACITY];
//0x61c88647倍数 & 15 获取下标
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
//填充值 key为ThreadLocal
table[i] = new Entry(firstKey, firstValue);
size = 1;
//threshold = 16 * 2 / 3;
setThreshold(INITIAL_CAPACITY);
}
再看下另一个api
ThreadLocal#get
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
//以当前对象作key 也就是ThreadLocal实例 从Entry数组中获取值
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
ThreadLocal#setInitialValue
private T setInitialValue() {
//initialValue()返回null
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
//初始化ThreadLocalMap new ThreadLocalMap(this, firstValue);
createMap(t, value);
return value;
}
我们发现无论是set还是get第一次都可能触发thread的ThreadLocalMap初始化,并传入初始值,只是初始值一个是我们传入的,一个设置为null。ThreadLocalMap内部维护了一个Entry[]数组,key为 ThreadLocal,且为弱引用
2. ThreadLocalMap清理方式
断点打在示例中 threadLocalB.set("qwe"); ,我们看下已完成初始化的时,如何往Entry数组中存值的
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
//len = 16
int len = tab.length;
//threadLocalHashCode & 15 取模
//在示例中事先创建了一个threadLocalA,并初始化了当前线程的ThreadLocalMap,占用了Entry[]的下标 10
//之后又循环创建了15个ThreadLocal ,但并未与当前线程发生set/get关系,不会对Entry[]有实质影响
//再创建了threadLocalB,此时threadLocalB与threadLocalA的threadLocalHashCode & 15的结果值都等于10
//这时就需要处理下标冲突了
int i = key.threadLocalHashCode & (len-1);
//nextIndex逻辑 : ((i + 1 < len) ? i + 1 : 0);
//循环逻辑:取对应下标对应的Entry ,只要entry不为空,就执行循环体内的逻辑,一轮走完后,
//将i+1,如果下标超过容量最大值 就置为 0
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
//e.get()是key e.value才是值
ThreadLocal<?> k = e.get();
//如果key和当前的操作的ThreadLocal是一个实例,就值覆盖,填充新的值
//这里有可能下标已经不是原有的下标了,但不管怎样 找到了正确的位置
if (k == key) {
e.value = value;
return;
}
//注意 到这里的逻辑是 entry不为空,entry的key为空,这是为啥?
//前文提到Entry继承自WeakReference,JVM 垃圾回收时,只要发现了弱引用的对象,不管内存是否充足,都会被回收。
//意思就是ThreadLocalMap不会作为ThreadLocal的gc root根,如果ThreadLocal没有其他引用了,它就应当被删除。
//不然会造成内存泄漏。所以当上述情况发生时,entry的key为null了,但是value还在,还是有可能内存泄漏,
//所以接下来就是应对这种情况
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
//该下标可以直接使用
tab[i] = new Entry(key, value);
int sz = ++size;
//尝试清理无效的key,并且判断是否需要扩容,判断的依据是当前的size > len*2/3,所以Entry数组中一定有为null的情况
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
ThreadLocalMap#replaceStaleEntry
//staleSlot就是ThreadLocal为null的Entry下标
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
//先前查找 entry的key也为null的下标,如果前一位Entry为null就终止循环
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
//向后查找 entry的key也为null的下标 如果后一位Entry为null就终止循环或者找到了key相同的那个下标
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
//下标有调整 但找到了正确的坑位
if (k == key) {
e.value = value;
//两个entry位置替换,反正tab[staleSlot]的key为空 属于要清理的对象
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
//无论是向前 还是向后查找 下标都会发生变化,
//这里也就是向前 向后都没有找到entry的key为null的情况
if (slotToExpunge == staleSlot)
//将slotToExpunge置为本次循环的index,什么意思呢,
//就是找到正确的位置了,entry置换后,需要开始清理一些无效的entry了
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
//这里就是先前查找时没发现有key为null 将slotToExpunge置为本次查找的index,
//会随着循环的递进而随index变动
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
//找不到key=当前ThreadLocal的entry
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
//先前或向后有无效的entry 进行两段清理
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
ThreadLocalMap#expungeStaleEntry
//staleSlot要开始清理的位置 传入进来的必然是entry不为null 但其key为null的情况
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
//置空 size-1
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
//有一个算一个 全部清除
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
//这个大兄弟的下标也是原有坑位被占用了 ,现在看前面有没有坑位,给他挪回去。
//否则需要额外的遍历才能找到对应的值
if (h != i) {
tab[i] = null;
//去它原本该待的地方看看,如果坑位还是被占,就继续往后找,把它挪回10或者11~15的位置
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
//返回entry为null的第一个下标
return i;
}
ThreadLocalMap#cleanSomeSlots
private boolean cleanSomeSlots(int i, int n) {
//i为entry为null的下标 或者 entry正常的下标(key不为null) n为当前数组的最大容量
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
i = nextIndex(i, len);
Entry e = tab[i];
if (e != null && e.get() == null) {
n = len;
removed = true;
//向后清理 直至entry为null的情况
i = expungeStaleEntry(i);
}
}
while ( (n >>>= 1) != 0);
return removed;
}
3.ThreadLocalMap扩容
ThreadLocal.ThreadLocalMap#rehash
private void rehash() {
//遍历数组 清理所有无效值
expungeStaleEntries();
//当前长度 >= len*2/3 * 3/4 也就是超过阈值的3/4 ,也就是 len/2
if (size >= threshold - threshold / 4)
resize();
}
ThreadLocalMap#expungeStaleEntries
private void expungeStaleEntries() {
Entry[] tab = table;
int len = tab.length;
for (int j = 0; j < len; j++) {
Entry e = tab[j];
if (e != null && e.get() == null)
expungeStaleEntry(j);
}
}
ThreadLocal.ThreadLocalMap#resize
扩容,上面的逻辑一圈看下来,下面的自然也就懂了
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;
}
总结
看了一圈,发现jdk的ThreadLocal并不是靠线程安全的map实现,而是靠每个线程都各自维护一个ThreadLocalMap(Entry数组)来实现,每个线程都是自己独立的操作,自然不需要考虑线程安全的问题。
Entry的key是弱引用,在ThreadLocal对象不再使用时,Entry数组不会成为它的GC root根,其会随着gc的清理而自动销毁,但是对应的value值并没有。为了应对这种情况,在每次操作ThreadLocal时都会尝试进行清理无效的Entry值,帮助垃圾回收。所以官方建议将ThreadLocal定义为static final类型,同时在操作ThreadLocal时使用 finally来清除ThreadLocal的value值,防止内存泄漏和不必要的清理机制造成的性能影响。
每个ThreadLocal都有一个唯一的threadLocalHashCode.这决定了其在Entry数组中的下标位置,可能造成下标冲突。需要遍历查找更适应的位置。ThreadLocal使用过多时很容易出现 Hash 冲突,需要 O(n) 时间复杂度解决冲突问题,效率较低。
FastThreadLocal
Netty 为 FastThreadLocal 量身打造了 FastThreadLocalThread 和 InternalThreadLocalMap 两个重要的类
示例
private static final FastThreadLocal<String> threadLocalA = new FastThreadLocal<>();
public static void main (String[]args){
String threadName = "thread-A";
new FastThreadLocalThread(() -> {
threadLocalA.set(threadName);
System.out.println("threadName: " + threadLocalA.get());;
}, threadName).start();
}
1. FastThreadLocal 与 InternalThreadLocalMap
FastThreadLocal
public class FastThreadLocal<V> {
//variablesToRemoveIndex = 0
private static final int variablesToRemoveIndex = InternalThreadLocalMap.nextVariableIndex();
private final int index;
public FastThreadLocal() {
//静态的 AtomicInteger nextIndex = new AtomicInteger(); nextIndex.getAndIncrement();
index = InternalThreadLocalMap.nextVariableIndex();
}
}
FastThreadLocal与 ThreadLocal差异:
ThreadLocal使用全局递增的threadLocalHashCode , FastThreadLocal维护了一个全局递增1的索引值
InternalThreadLocalMap
public final class InternalThreadLocalMap extends UnpaddedInternalThreadLocalMap {
private static final int DEFAULT_ARRAY_LIST_INITIAL_CAPACITY = 8;
public static final Object UNSET = new Object();
public static InternalThreadLocalMap get() {
Thread thread = Thread.currentThread();
if (thread instanceof FastThreadLocalThread) {
return fastGet((FastThreadLocalThread) thread);
} else {
return slowGet();
}
}
private static InternalThreadLocalMap fastGet(FastThreadLocalThread thread) {
InternalThreadLocalMap threadLocalMap = thread.threadLocalMap();
if (threadLocalMap == null) {
thread.setThreadLocalMap(threadLocalMap = new InternalThreadLocalMap());
}
return threadLocalMap;
}
private InternalThreadLocalMap() {
super(newIndexedVariableTable());
}
//返会32个长度 填充固定值new Object()对象的 Object[]
private static Object[] newIndexedVariableTable() {
Object[] array = new Object[32];
Arrays.fill(array, UNSET);
return array;
}
}
InternalThreadLocalMap与ThreadLocalMap差异: ThreadLocalMap使用16长度的泛型数组,InternalThreadLocalMap使用了32长度的Object数组,且初始化了值,每个值都是相同的一个Object对象
FastThreadLocalThread
public class FastThreadLocalThread extends Thread {
private InternalThreadLocalMap threadLocalMap;
}
FastThreadLocalThread直接继承了Thread,多了一个InternalThreadLocalMap属性
FastThreadLocal如何处理“下标冲突”
FastThreadLocal#set(V)
public final void set(V value) {
if (value != InternalThreadLocalMap.UNSET) {
//这里首是拿到的是 带 32长度有初始值的Obeject[] 的 InternalThreadLocalMap
set(InternalThreadLocalMap.get(), value);
} else {
remove();
}
}
FastThreadLocal#set(io.netty.util.internal.InternalThreadLocalMap, V)
public final void set(InternalThreadLocalMap threadLocalMap, V value) {
if (value != InternalThreadLocalMap.UNSET) {
//index是FastThreadLocal的唯一下标 全局递增
if (threadLocalMap.setIndexedVariable(index, value)) {
addToVariablesToRemove(threadLocalMap, this);
}
} else {
remove(threadLocalMap);
}
}
InternalThreadLocalMap#setIndexedVariable
public boolean setIndexedVariable(int index, Object value) {
Object[] lookup = indexedVariables;
//假设创建了很多FastThreadLocal而又与InternalThreadLocalMap没有交互,这里的index是会大于32的
if (index < lookup.length) {
//如果小于Object[]的容量 直接获取 不用考虑下标冲突
Object oldValue = lookup[index];
lookup[index] = value;
//返回原有的值是不是等于 UNSET初始值
return oldValue == UNSET;
} else {
//需要扩容了 有可能存在 当前线程一个FastThreadLocal都没使用,上来就扩容的场景
expandIndexedVariableTableAndSet(index, value);
//这里直接返回true 了
return true;
}
}
InternalThreadLocalMap 扩容 InternalThreadLocalMap#expandIndexedVariableTableAndSet
private void expandIndexedVariableTableAndSet(int index, Object value) {
//indexedVariables就是 InternalThreadLocalMap维护的Object[]数组
Object[] oldArray = indexedVariables;
final int oldCapacity = oldArray.length;
//和下方 HashMap#tableSizeFor 非常相似 向上取整为 2 的次幂
int newCapacity = index;
newCapacity |= newCapacity >>> 1;
newCapacity |= newCapacity >>> 2;
newCapacity |= newCapacity >>> 4;
newCapacity |= newCapacity >>> 8;
newCapacity |= newCapacity >>> 16;
newCapacity ++;
//拷贝原有数组
Object[] newArray = Arrays.copyOf(oldArray, newCapacity);
//填充初始值
Arrays.fill(newArray, oldCapacity, newArray.length, UNSET);
//直接存放
newArray[index] = value;
//替换数组
indexedVariables = newArray;
}
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
InternalThreadLocalMap扩容非常简单粗暴,没有看到有下标冲突的地方
至此存放值得逻辑是没问题了,长度不够就扩容,够就直接赋值,我们再看下setIndexedVariable返回true/false有什么区别,如果为true就调用addToVariablesToRemove(threadLocalMap, this);方法
FastThreadLocal#addToVariablesToRemove
private static void addToVariablesToRemove(InternalThreadLocalMap threadLocalMap, FastThreadLocal<?> variable) {
//variablesToRemoveIndex 静态final int,
//同FastThreadLocal的index 来自一个全局的AtomicInteger,且是第一个取值的,所以值恒等于 0
//取出threadLocalMap中第一个元素
Object v = threadLocalMap.indexedVariable(variablesToRemoveIndex);
Set<FastThreadLocal<?>> variablesToRemove;
if (v == InternalThreadLocalMap.UNSET || v == null) {
//创建FastThreadLocal类型的set
variablesToRemove = Collections.newSetFromMap(new IdentityHashMap<FastThreadLocal<?>, Boolean>());
threadLocalMap.setIndexedVariable(variablesToRemoveIndex, variablesToRemove);
} else {
//直接强转
variablesToRemove = (Set<FastThreadLocal<?>>) v;
}
//往set中添加该FastThreadLocal
variablesToRemove.add(variable);
}
那么当setIndexedVariable返回true时,也就是原有的值为初始值或者发生扩容后,只是将当前FastTreadLocal存入了一个set中,该set占据的InternalThreadLocalMap第一个下标
那么 InternalThreadLocalMap第一个下标存放的set有什么作用呢,我们看下 remove(); 这个方法 FastThreadLocal#remove()
public final void remove() {
//getIfSet:取出FastThreadLocalThread的InternalThreadLocalMap, 如果是普通线程 Thread,从 ThreadLocal 类型的 slowThreadLocalMap 中获取。
remove(InternalThreadLocalMap.getIfSet());
}
FastThreadLocal#remove(io.netty.util.internal.InternalThreadLocalMap)
public final void remove(InternalThreadLocalMap threadLocalMap) {
if (threadLocalMap == null) {
return;
}
//取出Object[]数组中对应index中的值,将下标所在位置填充 初始值,如果下标超过长度,直接返回初始值
Object v = threadLocalMap.removeIndexedVariable(index);
//如果map中第一位是set类型,就调用set.remove 从set中移除
removeFromVariablesToRemove(threadLocalMap, this);
if (v != InternalThreadLocalMap.UNSET) {
try {
//用户可以继承实现
onRemoval((V) v);
} catch (Exception e) {
PlatformDependent.throwException(e);
}
}
}
可以看到FastThreadLocal 在调用remove方法时,会同时将Object[]数组对应下标重置为初始值,同时还需要从 Object[]第一位保存的set 中移除相应FastThreadLocal ,为什么要多次一举保存一个set呢? 从上面的代码我们可以看出,FastThreadLocal没有考虑下标冲突的情况,每个FastThreadLocal都维持一个全局递增的index,长度不够了直接扩容,读写可以快速定位到对应地方,时间复杂度恒为 O(1)。但是是属于 空间换时间的 做法, 如果FastThreadLocal有很多个实例,相应的数组也会扩充到很大。
此时Object[]第一位保存的set作用就体现出来了,不需要遍历整个数组,如果需要清除InternalThreadLocalMap中所有的引用,只需要取出set,遍历set就可以了。同时也可以做到更好的内存管理,方便后期进行扩展。
总结
- FastThreadLocal不需要处理下标冲突,读写事件复杂度恒为O(1),空间使用上较ThreadLocal会高一点。
- FastThreadLocal扩容更方便,以 index 为基准向上取整到 2 的次幂作为扩容后容量,然后把原数据拷贝到新数组。而 ThreadLocal 由于采用的哈希表,所以在扩容后需要再做一轮 rehash。
- FastThreadLocalThread 和FastThreadLocalThread 类型的线程搭配使用才会更快,如果是普通线程反而会更慢(参考InternalThreadLocalMap#slowGet)