Netty是一款优秀的开源的NIO框架,其异步的、基于IO事件驱动的设计以及简易使用的API使得用户快速构建基于NIO的高性能高可靠性的网络服务器成为可能。Netty除了使用Reactor设计模式加上精心设计的线程模型之外,对于线程创建的具体细节也进行了重新设计,由于Netty的应用场景主要面向高并发高负载的场景下,这也是Netty能够大显身手的场景,因此,Netty不放过任何优化性能的机会。这篇文章主要介绍Netty线程模型基础部分——线程创建相关以及FastThreadLocal实现方面的一些细节以及和传统的ThreadLocal之间的性能比较数据。
传统的ThreadLocal
ThreadLocal最常用的两个接口是set和get,前者是用于往ThreadLocal设置内容,后者是从ThreadLocal中取内容。最常见的应用场景为在线程上下文之间传递信息,使得用户不受复杂代码逻辑的影响。我们来看看他们的实现原理:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
t.threadLocals;
我们使用set的时候实际上是获取Thread对象的threadLocals属性,把当前ThreadLocal当做参数然后调用其set(ThreadLocal,Object)方法来设值。threadLocals是ThreadLocal.ThreadLocalMap类型的。因此我们可以知道Thread、ThreadLoca以及ThreadLocal.ThreadLocalMap的关系可以用下图表示:
解释一下上面的图,每个线程对象关联着一个ThreadLocalMap实例,ThreadLocalMap实例主要是维护着一个Entry数组。Entry是扩展了WeakReference,提供了一个存储value的地方。一个线程对象可以对应多个ThreadLocal实例,一个ThreadLocal也可以对应多个Thread对象,当一个Thread对象和每一个ThreadLocal发生关系的时候会生成一个Entry,并将需要存储的值存储在Entry的value内。到这里我们可以总结一下几点:
一个ThreadLocal对于一个Thread对象来说只能存储一个值,为Object类型。
多个ThreadLocal对于一个Thread对象,这些ThreadLocal和线程相关的值存储在Thread对象关联的ThreadLocalMap中。
使用扩展WeakReference的Entry作为数据节点在一定程度上防止了内存泄露。
多个Thread线程对象和一个ThreadLocal发生关系的时候其实真是数据的存储是跟着线程对象走的,因此这种情况不讨论。
我们在看看ThreadLocalMap#set:
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();
一般情况每个ThreadLocal实例都有一个唯一的threadLocalHashCode初始值。上面首先根据threadLocalHashCode值计算出i,有下面两种情况会进入for循环:
由于threadLocalHashCode&(len-1)的值对应的槽有内容,因此满足tab[i]!=null条件,进入for循环,如果满足条件且当前key不是当前threadlocal只能说明hash冲突了。
ThreadLocal实例之前被设值过,因此足tab[i]!=null条件,进入for循环。
进入for循环会遍历tab数组,如果遇到以当前threadLocal为key的槽,即上面第(2)种情况,有则直接将值替换;如果找到了一个已经被回收的ThreadLocal对应的槽,也就是当key==null的时候表示之前的threadlocal已经被回收了,但是value值还存在,这也是ThreadLocal内存泄露的地方。碰到这种情况,则会引发替换这个位置的动作,如果上面两种情况都没发生,即上面的第(1)种情况,则新创建一个Entry对象放入槽中。
看看ThreadLocalMap的读取实现:
private Entry getEntry(ThreadLocal key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
当命中的时候,也就是根据当前ThreadLocal计算出来的i恰好是当前ThreadLocal设置的值的时候,可以直接根据hashcode来计算出位置,当没有命中的时候,这里没有命中分为三种情况:
当前ThreadLocal之前没有设值过,并且当前槽位没有值。
当前槽位有值,但是对于的不是当前threadlocal,且那个ThreadLocal没有被回收。
当前槽位有值,但是对于的不是当前threadlocal,且那个ThreadLocal被回收了。
上面三种情况都会调用getEntryAfterMiss方法。调用getEntryAfterMiss方法会引发数组的遍历。
总结一下ThreadLocal的性能,一个线程对应多个ThreadLocal实例的场景中,在没有命中的情况下基本上一次hash就可以找到位置,如果发生没有命中的情况,则会引发性能会急剧下降,当在读写操作频繁的场景,这点将成为性能诟病。
Netty FastThreadLocal
Netty重新设计了更快的FastThreadLocal,主要实现涉及FastThreadLocalThread、FastThreadLocal和InternalThreadLocalMap类,FastThreadLocalThread是Thread类的简单扩展,主要是为了扩展threadLocalMap属性。
public class FastThreadLocalThread extends Thread {
private InternalThreadLocalMap threadLocalMap;
FastThreadLocal提供的接口和传统的ThreadLocal一致,主要是set和get方法,用法也一致,不同地方在于FastThreadLocal的值是存储在InternalThreadLocalMap这个结构里面的,传统的ThreadLocal性能槽点主要是在读写的时候hash计算和当hash没有命中的时候发生的遍历,我们来看看FastThreadLocal的核心实现。先看看FastThreadLocal的构造方法:
public FastThreadLocal() {
index = InternalThreadLocalMap.nextVariableIndex();
}
实际上在构造FastThreadLocal实例的时候就决定了这个实例的索引,而索引的生成相关代码我们再看看:
public static int nextVariableIndex() {
int index = nextIndex.getAndIncrement();
static final AtomicInteger nextIndex = new AtomicInteger();
nextIndex是InternalThreadLocalMap父类的一个全局静态的AtomicInteger类型的对象,这意味着所有的FastThreadLocal实例将共同依赖这个指针来生成唯一的索引,而且是线程安全的。上面讲过了InternalThreadLocalMap实例和Thread对象一一对应,而InternalThreadLocalMap维护着一个数组:
Object[] indexedVariables;
这个数组用来存储跟同一个线程关联的多个FastThreadLocal的值,由于FastThreadLocal对应indexedVariables的索引是确定的,因此在读写的时候将会发生随机存取,非常快。
另外这里有一个问题,nextIndex是静态唯一的,而indexedVariables数组是实例对象的,因此我认为随着FastThreadLocal数量的递增,这会造成空间的浪费。
性能数据:
我么分析,性能问题主要存在的场景为一个线程对应多个ThreadLocal实例,因为只有在这种场景下才会出现多个ThreadLocal对应的值存储在同一个数组中,从而会有hash没有命中或hash冲突的可能,我写了两段代码来简单测试传统ThreadLocal和FastThreadLocal的性能,然后适当调整读取数和ThreadLocal数进行对比:
代码片段1,传统ThreadLocal测试:
public static void main(String ...s) {
final int threadLocalCount = 1000;
final ThreadLocal<String>[] caches = new ThreadLocal[threadLocalCount];
final Thread mainThread = Thread.currentThread();
for (int i=0;i<threadLocalCount;i++) {
caches[i] = new ThreadLocal();
}
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for (int i=0;i<threadLocalCount;i++) {
caches[i].set("float.lu");
}
long start = System.nanoTime();
for (int i=0;i<threadLocalCount;i++) {
for (int j=0;j<1000000;j++) {
caches[i].get();
}
}
long end = System.nanoTime();
System.out.println("take[" + TimeUnit.NANOSECONDS.toMillis(end - start) +
"]ms");
LockSupport.unpark(mainThread);
}
});
t.start();
LockSupport.park(mainThread);
}
代码片段2,FastThreadLocal测试:
public static void main(String ...s) {
final int threadLocalCount = 1000;
final FastThreadLocal<String>[] caches = new FastThreadLocal[threadLocalCount];
final Thread mainThread = Thread.currentThread();
for (int i=0;i<threadLocalCount;i++) {
caches[i] = new FastThreadLocal();
}
Thread t = new FastThreadLocalThread(new Runnable() {
@Override
public void run() {
for (int i=0;i<threadLocalCount;i++) {
caches[i].set("float.lu");
}
long start = System.nanoTime();
for (int i=0;i<threadLocalCount;i++) {
for (int j=0;j<1000000;j++) {
caches[i].get();
}
}
long end = System.nanoTime();
System.out.println("take[" + TimeUnit.NANOSECONDS.toMillis(end - start) +
"]ms");
LockSupport.unpark(mainThread);
}
});
t.start();
LockSupport.park(mainThread);
}
两段代码逻辑相同,分别先进行稍稍的读预热,再适当调整对应的参数,分别统计5次结果:
1000个ThreadLocal对应一个线程对象对应一个线程对象的100w次的计时读操作:
ThreadLocal:3767ms | 3636ms | 3595ms | 3610ms | 3719ms
FastThreadLocal: 15ms | 14ms | 13ms | 14ms | 14ms
1000个ThreadLocal对应一个线程对象对应一个线程对象的10w次的计时读操作:
ThreadLocal:384ms | 378ms | 366ms | 647ms | 372ms
FastThreadLocal:14ms | 13ms | 13ms | 17ms | 13ms
1000个ThreadLocal对应一个线程对象对应一个线程对象的1w次的计时读操作:
ThreadLocal:43ms | 42ms | 42ms | 56ms | 45ms
FastThreadLocal:15ms | 13ms | 11ms | 15ms | 11ms
100个ThreadLocal对应一个线程对象对应一个线程对象的1w次的计时读操作:
ThreadLocal:16ms | 21ms | 18ms | 16ms | 18ms
FastThreadLocal:15ms | 15ms | 15ms | 17ms | 18ms
上面的实验数据可以看出,当ThreadLocal数量和读写ThreadLocal的频率较高的时候,传统的ThreadLocal的性能下降速度比较快,而Netty实现的FastThreadLocal性能比较稳定。上面实验模拟的场景不够具体,但是已经在一定程度上我们可以认为,FastThreadLocal相比传统的的ThreadLocal在高并发高负载环境下表现的比较优秀。
本文由作者原创,仅由学习Netty源码和进行性能实验得出总结,如有问题还请多多指教。