Netty精粹之设计更快的ThreadLocal

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的关系可以用下图表示:

175021_gtWk_1759553.png

解释一下上面的图,每个线程对象关联着一个ThreadLocalMap实例,ThreadLocalMap实例主要是维护着一个Entry数组。Entry是扩展了WeakReference,提供了一个存储value的地方。一个线程对象可以对应多个ThreadLocal实例,一个ThreadLocal也可以对应多个Thread对象,当一个Thread对象和每一个ThreadLocal发生关系的时候会生成一个Entry,并将需要存储的值存储在Entry的value内。到这里我们可以总结一下几点:

  1. 一个ThreadLocal对于一个Thread对象来说只能存储一个值,为Object类型。

  2. 多个ThreadLocal对于一个Thread对象,这些ThreadLocal和线程相关的值存储在Thread对象关联的ThreadLocalMap中。

  3. 使用扩展WeakReference的Entry作为数据节点在一定程度上防止了内存泄露。

  4. 多个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循环:

  1. 由于threadLocalHashCode&(len-1)的值对应的槽有内容,因此满足tab[i]!=null条件,进入for循环,如果满足条件且当前key不是当前threadlocal只能说明hash冲突了。

  2. 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来计算出位置,当没有命中的时候,这里没有命中分为三种情况:

  1. 当前ThreadLocal之前没有设值过,并且当前槽位没有值。

  2. 当前槽位有值,但是对于的不是当前threadlocal,且那个ThreadLocal没有被回收。

  3. 当前槽位有值,但是对于的不是当前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源码和进行性能实验得出总结,如有问题还请多多指教。

转载于:https://my.oschina.net/andylucc/blog/614359

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值