netty④ 源码 ThreadLocal 与 FastThreadLocal

147 阅读11分钟

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就可以了。同时也可以做到更好的内存管理,方便后期进行扩展。

总结

  1. FastThreadLocal不需要处理下标冲突,读写事件复杂度恒为O(1),空间使用上较ThreadLocal会高一点。
  2. FastThreadLocal扩容更方便,以 index 为基准向上取整到 2 的次幂作为扩容后容量,然后把原数据拷贝到新数组。而 ThreadLocal 由于采用的哈希表,所以在扩容后需要再做一轮 rehash。
  3. FastThreadLocalThread 和FastThreadLocalThread 类型的线程搭配使用才会更快,如果是普通线程反而会更慢(参考InternalThreadLocalMap#slowGet)