Netty精粹之设计更快的ThreadLocal

Stella981
• 阅读 604

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

Netty精粹之设计更快的ThreadLocal

解释一下上面的图,每个线程对象关联着一个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源码和进行性能实验得出总结,如有问题还请多多指教。

点赞
收藏
评论区
推荐文章
blmius blmius
2年前
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
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Stella981 Stella981
2年前
Netty序章之BIO NIO AIO演变
Netty序章之BIONIOAIO演变Netty是一个提供异步事件驱动的网络应用框架,用以快速开发高性能、高可靠的网络服务器和客户端程序。Netty简化了网络程序的开发,是很多框架和公司都在使用的技术。更是面试的加分项。Netty并非横空出世,它是在BIO,NIO,AIO演变中的产物,是一种
Stella981 Stella981
2年前
Netty 入门初体验
Netty简介Netty是一款异步的事件驱动的网络应用程序框架,支持快速开发可维护的高性能的面向协议的服务器和客户端。Netty主要是对java的nio包进行的封装为什么要使用Netty上面介绍到Netty是一款高性能的网络通讯框架,那么我们为什么要使用Netty,换句话说,
Stella981 Stella981
2年前
Netty网络编程(初识)
Netty简单介绍核心架构图(现在还看不是很懂):!netty(https://static.oschina.net/uploads/img/201712/02192156_X1e5.png"netty")Netty是由JBOSS提供的一个java开源框架。Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高
Stella981 Stella981
2年前
Netty(RPC高性能之道)原理剖析
1,Netty简述Netty是一个基于JAVANIO类库的异步通信框架,用于创建异步非阻塞、基于事件驱动、高性能、高可靠性和高可定制性的网络客户端和服务器端RPC高性能分析,请参考文章“【总结】RPC性能之道”特点异步、非阻塞、基于事件驱动的NIO框架支持多种传输层通信协议,包括T
Stella981 Stella981
2年前
Netty概述
1.Netty概念异步事件驱动框架,用于快速开发高性能服务端和客户端封装了JDK底层BIO和NIO模型,提供高度可用的API自带编解码器解决拆包粘包问题,用户只用关心业务逻辑精心设计的reactor线程模型支持高并发海量连接自带各种协议栈让你处理任何一种通用协议都几乎不用亲自动手
Stella981 Stella981
2年前
Netty堆外内存泄露排查与总结
导读Netty是一个异步事件驱动的网络通信层框架,用于快速开发高可用高性能的服务端网络框架与客户端程序,它极大地简化了TCP和UDP套接字服务器等网络编程。Netty底层基于JDK的NIO,我们为什么不直接基于JDK的NIO或者其他NIO框架:1.使用JDK自带的NIO需要了解太多的概念,编程复杂。2
Stella981 Stella981
2年前
Netty 线程模型与Reactor 模式
前言     Netty 的线程模型是基于NIO的Selector 构建的,使用了异步驱动的Reactor 模式来构建的线程模型,可以很好的支持成百上千的SocketChannel 连接。由于READ/WRITE 都是非阻塞的,可以充分提升I/O线程的运行效率,避免了IO阻塞导致线程挂起, 同时可以让一个线程支持对多个客户端的连接So
为什么mysql不推荐使用雪花ID作为主键
作者:毛辰飞背景在mysql中设计表的时候,mysql官方推荐不要使用uuid或者不连续不重复的雪花id(long形且唯一),而是推荐连续自增的主键id,官方的推荐是auto_increment,那么为什么不建议采用uuid,使用uuid究