文档章节

Netty精粹之设计更快的ThreadLocal

Float_Luuu
 Float_Luuu
发布于 2016/02/09 17:18
字数 2498
阅读 5121
收藏 47

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内。到这里我们可以总结一下几点:

  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源码和进行性能实验得出总结,如有问题还请多多指教。

© 著作权归作者所有

Float_Luuu
粉丝 223
博文 47
码字总数 104674
作品 0
长宁
高级程序员
私信 提问
加载中

评论(2)

llhkyzg
llhkyzg
第四点不讲,最好就不要写,这会造成读者去做不必要思考,影响文章的重点考虑
爱吃大肉包
爱吃大肉包
有个小问题, ThreadLocalMap#set 里,如果 if (k != key) , value又有值呢, 如hashmap的链表处理,即hash碰撞怎么处理的的, 好像没看到代码里的处理
Netty源码阅读入门实战(十)-性能优化

1 性能优化工具类 FastThreadLocal 传统的ThreadLocal ThreadLocal最常用的两个接口是set和get 最常见的应用场景为在线程上下文之间传递信息,使得用户不受复杂代码逻辑的影响 我们使用set的...

芥末无疆sss
2018/10/22
0
0
为什么Netty的FastThreadLocal速度快

前言 最近在看netty源码的时候发现了一个叫FastThreadLocal的类,jdk本身自带了ThreadLocal类,所以可以大致想到此类比jdk自带的类速度更快,主要快在什么地方,以及为什么速度更快,下面做一...

ksfzhaohui
2019/10/14
918
0
除了马云亲笔推荐的《码出高效》外,2018年还有哪些值得一看的程序员进阶书籍?附赠书活动

  前言   2018倒计时进入尾声,即将迎来2019年元旦,年终盘点了下218年入手的N多本技术书籍,大浪淘沙,师长觉得以下几本书籍最值得入手:   码出高效:Java开发手册   Redis 深度历...

java进阶架构师
2018/12/30
0
0
惊:FastThreadLocal吞吐量居然是ThreadLocal的3倍!!!

说明 接着上次手撕面试题ThreadLocal!!!面试官一听,哎呦不错哦!本文将继续上文的话题,来聊聊FastThreadLocal,目前关于FastThreadLocal的很多文章都有点老有点过时了(本文将澄清几个误...

匠心零度
2019/07/02
0
0
牛逼哄哄的Dubbo框架,底层到底是什么原理?

搞了N年Java,仍有不少朋友困惑:用了很多年Dubbo,觉得自己挺厉害,跳槽面试时一问RPC,一问底层通讯,一问NIO和AIO,就一脸懵逼,到底该怎么办? (大家有没有这样的感触?Dubbo用得很熟,...

Java猫
2019/03/27
0
0

没有更多内容

加载失败,请刷新页面

加载更多

jsp web 大文件上传源代码

我们平时经常做的是上传文件,上传文件夹与上传文件类似,但也有一些不同之处,这次做了上传文件夹就记录下以备后用。 首先我们需要了解的是上传文件三要素: 1.表单提交方式:post (get方式提...

东方雨
26分钟前
53
0
读懂这一篇,集群节点不下线

作者 | 声东 阿里云售后技术专家 导读:排查完全陌生的问题、完全不熟悉的系统组件,是售后工程师的一大工作乐趣,当然也是挑战。今天借这篇文章,跟大家分析一例这样的问题。排查过程中,需...

阿里巴巴云原生
32分钟前
95
0
如何让scss变量能够当做js变量来使用

如何让scss变量能够当做js变量来使用 当前我们使用scss变量有两个痛点: 需要手动导入 无法与js建立联系或者很难,后续不能在此基础上做一些骚操作 为了解决这两个问题,我们以创建js文件以j...

念其蔚蓝
41分钟前
83
0
Java日期加减

public static String getDate(String dateGiven,Integer day) throws Exception{ SimpleDateFormat sdf= new SimpleDateFormat("yyyy-MM-dd"); Date date=sdf.parse(dateGiven);......

那个猩猩很亮
50分钟前
117
0
创龙TI TMS320C6748定点/浮点DSP C674xSD卡接口、拓展IO信号

TL138/1808/6748-EVM是广州创龙基于SOM-TL138/1808/6748核心板开发的一款开发板。由于SOM-TL138/1808/6748核心板管脚兼容,所以此三个核心板共用同一个底板。开发板采用核心板+底板的设计方式...

Tronlong创龙
50分钟前
81
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部