Netty 如何实现更快的ThreadLocal
引言
在前面关于内存分配的代码分析文章中,我们在多个场合见到了一个类FastThreadLocal。从名字可以看出,这应该是ThreadLocal的高性能版本。
在Netty中,为了打磨性能的细节,在很多方面都做了一些改造,这篇文章我们就来分析下关于FastThreadLocal设计与实现。
原生的ThreadLocal
在介绍Netty的FastThreadLocal之前,我们先来看下再JDK中的ThreadLocal是怎么样的一种解决方案,才能对比出二者的设计上差异化思考。
ThreadLocal的设计意图是让每一个线程都持有自身独立的变量副本,这样,多个线程之间使用的数据是独立的副本,没有交互,也就不会出现多线程并发下可能导致的数据并发安全问题。
ThreadLocalMap
要了解ThreadLocal的原理,首先先了解一个数据结构ThreadLocal.ThreadLocalMap。这是一个内部对象,并没有对开发者暴露其API,其API的使用主要都是通过ThreadLocal委托的。首先来看其属性定义,如下
private Entry[] table;//ThreadLocalMap 是一个Map映射的结构,使用数组作为背后的元素存储
private int size = 0; //当前Map内元素的个数
private int threshold; //当前table数组扩容的阀值
static class Entry extends WeakReference < ThreadLocal <? >>
{
Object value;
Entry(ThreadLocal <? > k, Object v)
{
super(k);
value = v;
}
}
这里面比较有意思是Entry这个类,它继承了WeakReference,并且使用ThreadLocal对象作为引用对象。这意味着当ThreadLocal被GC回收后,这个Entry就无法定位了。这个特性,后续的方法中我们会看到它的用途。
设置元素
当我们调用ThreadLocal.set方法时,最终会委托给ThreadLocalMap的set方法,如下
private void set(ThreadLocal <? > key, Object value)
{
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len - 1);
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;
if(!cleanSomeSlots(i, sz) && sz >= threshold) rehash();
}
和HashMap的思路类似,首先是求出Key的hash作为散列的依据,只不过和HashMap中自己计算不同,这边是直接使用了ThreadLocal#threadLocalHashCode属性值作为散列依据,通过key.threadLocalHashCode & (len - 1)得到散列后在table数组中的下标。
不过与HashMap不同的是,如果散列后的槽位本身上已经有数据了,ThreadLocalMap并不是在对应的槽位上通过链表挂载相同散列值的Key,而是按照顺序寻找下一个槽位,并且再次判断,重复这个过程直到找到key相同或者key不存在的槽位,亦或者本身不存在元素的槽位。
如果找到对应的槽位,其上还不存在Entry,则新建一个Entry。每当新建Entry也就是新增元素成功,都会首先尝试清除“陈旧”的Entry。上文说过,Entry继承了WeakReference,如果其引用的ThreadLocal对象被GC了,则意味着Key消失,也就是意味着Entry内的value无法被访问到。此时认为这个Entry陈旧,可以被清除。下面来看下cleanSomeSlots的具体实现,如下
private boolean cleanSomeSlots(int i, int 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;
i = expungeStaleEntry(i);
}
} while ((n >>>= 1) != 0);
return removed;
}
入参i是刚刚新Entry插入的位置,必然不是陈旧,从这个槽位开始,向后检查 log2n 次。检查的方式就是通过Entry.get来确认其弱引用指向的ThreadLocal是否已经被回收。如果某个槽位上的Entry已经陈旧,通过方法expungeStaleEntry来清除它,代码如下
private int expungeStaleEntry(int staleSlot)
{
Entry[] tab = table;
int len = tab.length;
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;
while(tab[h] != null) h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
expungeStaleEntry方法的思路可以分为两个阶段:
- 第一阶段,将槽位
staleSlot上的Entry的属性value设置为null,使得其指向的对象可以被GC。并且将该槽位设置为null,让该Entry对象本身也被GC。 - 第二阶段,从槽位
staleSlot开始向后遍历直到空槽位为止,如果遇到Entry.get为null的情况,对应的将该槽位上的数据释放;如果Entry.get不为null的情况,判断threadLocalHashCode散列之后的槽位是否是当前的槽位,如果不是的话,则尝试将其挪动到散列后的正确位置上,如果对应位置有Entry,继续向后挪动直到放下为止。
expungeStaleEntry方***尝试在一定数量范围内(从当前陈旧的槽位向后遍历到空槽位),清除弱引用被GC的Entry元素。并且在这个过程中协助将散列位置不足够恰当的元素尝试挪动到散列最恰当的位置。
cleanSomeSlots方法执行完毕后,如果在扫描范围内没有陈旧的槽位,再检查当前元素个数是否超过了阈值,超过的情况下就启动扩容,也即是方法rehash。rehash方法内部实现很简单,首先是遍历table数组,清除所有的陈旧Entry,在清楚陈旧Entry后剩余元素个数仍然超过3/4的给定阈值,则将数组扩容两倍长度,并且将旧数组中的元素放入新数组即可。
获取元素
与设置元素相对的就是获取元素,当调用ThreadLocal.get时,底层是委托到ThreadLocalMap.getEntry方法上的,其代码如下
private Entry getEntry(ThreadLocal <? > key)
{
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if(e != null && e.get() == key) re
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
<p> 通过本专刊的学习,对网络开发所需掌握的基础理论知识会更加牢固,对网络应用涉及的线程模型,设计模式,高性能架构等更加明确。通过对Netty的源码深入讲解,使得读者对Netty达到“知其然更之所以然”的程度。在遇到一些线上的问题时,具备了扎实理论功底的情况,可以有的放矢而不会显得盲目。 本专刊购买后即可解锁所有章节,故不可以退换哦~ </p> <p> <br /> </p>