Netty精粹之设计更快的ThreadLocal

Netty是一款优秀的开源的NIO框架,其异步的、基于IO事件驱动的设计以及简易使用的API使得用户快速构建基于NIO的高性能高可靠性的网络服务器成为可能。Netty除了使用Reactor设计模式加上精心设计的线程模型之外,对于线程创建的具体细节也进行了重新设计,由于Netty的应用场景主要面向高并发高负载的场景下,这也是Netty能够大显身手的场景,因此,Netty不放过任何优化性能的机会。这篇文章主要介绍Netty线程模型基础部分——线程创建相关以及FastThreadLocal实现方面的一些细节以及和传统的ThreadLocal之间的性能比较数据。

传统的ThreadLocal

ThreadLocal最常用的两个接口是set和get,前者是用于往ThreadLocal设置内容,后者是从ThreadLocal中取内容。最常见的应用场景为在线程上下文之间传递信息,使得用户不受复杂代码逻辑的影响。我们来看看他们的实现原理:

1public void set(T value) { 2    Thread t = Thread.currentThread(); 3    ThreadLocalMap map = getMap(t); 4 5 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:

1Entry[] tab = table; 2int len = tab.length; 3int i = key.threadLocalHashCode & (len-1); 4 5for (Entry e = tab[i]; 6     e != null; 7     e = tab[= nextIndex(i, len)]) { 8    ThreadLocal k = e.get(); 9    if (== key) { 10        e.value = value; 11        return; 12    } 13    if (== null) { 14        replaceStaleEntry(key, value, i); 15        return; 16    } 17} 18tab[i] = new Entry(key, value); 19int sz = ++size; 20if (!cleanSomeSlots(i, sz) && sz >= threshold) 21    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的读取实现:

1private Entry getEntry(ThreadLocal key) { 2    int i = key.threadLocalHashCode & (table.length - 1); 3    Entry e = table[i]; 4    if (!= null && e.get() == key) 5        return e; 6    else 7        return getEntryAfterMiss(key, i, e); 8}

当命中的时候,也就是根据当前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属性。

1public class FastThreadLocalThread extends Thread { 2 3    private InternalThreadLocalMap threadLocalMap;

FastThreadLocal提供的接口和传统的ThreadLocal一致,主要是set和get方法,用法也一致,不同地方在于FastThreadLocal的值是存储在InternalThreadLocalMap这个结构里面的,传统的ThreadLocal性能槽点主要是在读写的时候hash计算和当hash没有命中的时候发生的遍历,我们来看看FastThreadLocal的核心实现。先看看FastThreadLocal的构造方法:

1public FastThreadLocal() { 2    index = InternalThreadLocalMap.nextVariableIndex(); 3}

实际上在构造FastThreadLocal实例的时候就决定了这个实例的索引,而索引的生成相关代码我们再看看:

1public static int nextVariableIndex() { 2    int index = nextIndex.getAndIncrement(); 3 4static 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测试:

1public static void main(String ...s) { 2    final int threadLocalCount = 1000; 3    final ThreadLocal<String>[] caches = new ThreadLocal[threadLocalCount]; 4    final Thread mainThread = Thread.currentThread(); 5    for (int i=0;i<threadLocalCount;i++) { 6        caches[i] = new ThreadLocal(); 7    } 8    Thread t = new Thread(new Runnable() { 9        @Override 10        public void run() { 11            for (int i=0;i<threadLocalCount;i++) { 12                caches[i].set("float.lu"); 13            } 14            long start = System.nanoTime(); 15            for (int i=0;i<threadLocalCount;i++) { 16                for (int j=0;j<1000000;j++) { 17                    caches[i].get(); 18                } 19            } 20            long end = System.nanoTime(); 21            System.out.println("take[" + TimeUnit.NANOSECONDS.toMillis(end - start) + 22                    "]ms"); 23            LockSupport.unpark(mainThread); 24        } 25 26    }); 27    t.start(); 28    LockSupport.park(mainThread); 29}

代码片段2,FastThreadLocal测试:

1public static void main(String ...s) { 2    final int threadLocalCount = 1000; 3    final FastThreadLocal<String>[] caches = new FastThreadLocal[threadLocalCount]; 4    final Thread mainThread = Thread.currentThread(); 5    for (int i=0;i<threadLocalCount;i++) { 6        caches[i] = new FastThreadLocal(); 7    } 8    Thread t = new FastThreadLocalThread(new Runnable() { 9        @Override 10        public void run() { 11            for (int i=0;i<threadLocalCount;i++) { 12                caches[i].set("float.lu"); 13            } 14            long start = System.nanoTime(); 15            for (int i=0;i<threadLocalCount;i++) { 16                for (int j=0;j<1000000;j++) { 17                    caches[i].get(); 18                } 19            } 20            long end = System.nanoTime(); 21            System.out.println("take[" + TimeUnit.NANOSECONDS.toMillis(end - start) + 22                    "]ms"); 23            LockSupport.unpark(mainThread); 24        } 25 26    }); 27    t.start(); 28    LockSupport.park(mainThread); 29}

两段代码逻辑相同,分别先进行稍稍的读预热,再适当调整对应的参数,分别统计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源码和进行性能实验得出总结,如有问题还请多多指教。

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

Netty序章之BIO NIO AIO演变

Netty序章之BIONIOAIO演变Netty是一个提供异步事件驱动的网络应用框架,用以快速开发高性能、高可靠的网络服务器和客户端程序。Netty简化了网络程序的开发,是很多框架和公司都在使用的技术。更是面试的加分项。Netty并非横空出世,它是在BIO,NIO,AIO演变中的产物,是一种

Netty 入门初体验

Netty简介Netty是一款异步的事件驱动的网络应用程序框架,支持快速开发可维护的高性能的面向协议的服务器和客户端。Netty主要是对java的nio包进行的封装为什么要使用Netty上面介绍到Netty是一款高性能的网络通讯框架,那么我们为什么要使用Netty,换句话说,