惊:FastThreadLocal吞吐量竟然是ThreadLocal的3倍!!!

说明

接着上次手撕面试题ThreadLocal!!!面试官一听,哎呦不错哦!本文将继续上文的话题,来聊聊FastThreadLocal,目前关于FastThreadLocal的不少文章都有点老有点过期了(本文将澄清几个误区),不少文章关于FastThreadLocal介绍的也不全,但愿本篇文章能够带你完全理解FastThreadLocal!!!html

FastThreadLocal是Netty提供的,在池化内存分配等都有涉及到!​java

关于FastThreadLocal,零度准备从这几个方面进行讲解:git

  • FastThreadLocal的使用。
  • FastThreadLocal并非什么状况都快,你要用对才会快。
  • FastThreadLocal利用字节填充来解决伪共享问题。
  • FastThreadLocal比ThreadLocal快,并非空间换时间。
  • FastThreadLocal不在使用ObjectCleaner处理泄漏,必要的时候建议重写onRemoval方法。
  • FastThreadLocal为何快?

FastThreadLocal的使用

FastThreadLocal用法上兼容ThreadLocalgithub

FastThreadLocal使用示例代码:面试

public class FastThreadLocalTest {
    private static FastThreadLocal<Integer> fastThreadLocal = new FastThreadLocal<>();

    public static void main(String[] args) {

        //if (thread instanceof FastThreadLocalThread) 使用FastThreadLocalThread更优,普通线程也能够
        new FastThreadLocalThread(() -> {
            for (int i = 0; i < 100; i++) {
                fastThreadLocal.set(i);
                System.out.println(Thread.currentThread().getName() + "====" + fastThreadLocal.get());
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "fastThreadLocal1").start();


        new FastThreadLocalThread(() -> {
            for (int i = 0; i < 100; i++) {
                System.out.println(Thread.currentThread().getName() + "====" + fastThreadLocal.get());
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "fastThreadLocal2").start();
    }
}
复制代码

代码截图:算法

代码运行结果:数组

咱们在回顾下以前的ThreadLocal的最佳实践作法:缓存

try {
    // 其它业务逻辑
} finally {
    threadLocal对象.remove();
}
复制代码

备注: 经过上面的例子,咱们发现FastThreadLocal和ThreadLocal在用法上面基本差很少,没有什么特别区别,我的认为,这就是FastThreadLocal成功的地方,它就是要让用户用起来和ThreadLocal没啥区别,要兼容!bash

使用FastThreadLocal竟然不用像ThreadLocal那样先try ………………… 以后finally进行threadLocal对象.remove();数据结构

因为构造FastThreadLocalThread的时候,经过FastThreadLocalRunnable对Runnable对象进行了包装:

FastThreadLocalRunnable.wrap(target)从而构造了FastThreadLocalRunnable对象。
复制代码

FastThreadLocalRunnable在执行完以后都会调用FastThreadLocal.removeAll();

备注: FastThreadLocal不在使用ObjectCleaner处理泄漏,必要的时候建议重写onRemoval方法。关于这块将在本文后面进行介绍,这样是不少网上资料比较老的缘由,这块已经去掉了。

若是是普通线程,仍是应该最佳实践:

finally { fastThreadLocal对象.removeAll(); }

注意: 若是使用FastThreadLocal就不要使用普通线程,而应该构建FastThreadLocalThread,关于为何这样,关于这块将在本文后面进行介绍:FastThreadLocal并非什么状况都快,你要用对才会快。

FastThreadLocal并非什么状况都快,你要用对才会快

首先看看netty关于这块的测试用例: 代码路径:github.com/netty/netty…

备注: 在我本地进行测试,FastThreadLocal的吞吐量是jdkThreadLocal的3倍左右。机器不同,可能效果也不同,你们能够本身试试,反正就是快了很多。

关于ThreadLocal,以前的这篇:手撕面试题ThreadLocal!!!已经详细介绍了。

FastThreadLocal并非什么状况都快,你要用对才会快!!!

注意: 使用FastThreadLocalThread线程才会快,若是是普通线程还更慢! 注意: 使用FastThreadLocalThread线程才会快,若是是普通线程还更慢! 注意: 使用FastThreadLocalThread线程才会快,若是是普通线程还更慢!

netty的测试目录下面有2个类:

  • FastThreadLocalFastPathBenchmark
  • FastThreadLocalSlowPathBenchmark

路径:github.com/netty/netty…

FastThreadLocalFastPathBenchmark测试结果: 是ThreadLocal的吞吐量的3倍左右。

FastThreadLocalSlowPathBenchmark测试结果: 比ThreadLocal的吞吐量还低。

测试结论: 使用FastThreadLocalThread线程操做FastThreadLocal才会快,若是是普通线程还更慢!

注释里面给出了三点:

  • FastThreadLocal操做元素的时候,使用常量下标在数组中进行定位元素来替代ThreadLocal经过哈希和哈希表,这个改动特别在频繁使用的时候,效果更加显著!

  • 想要利用上面的特征,线程必须是FastThreadLocalThread或者其子类,默认DefaultThreadFactory都是使用FastThreadLocalThread的

  • 只用在FastThreadLocalThread或者子类的线程使用FastThreadLocal才会更快,由于FastThreadLocalThread 定义了属性threadLocalMap类型是InternalThreadLocalMap。若是普通线程会借助ThreadLocal。

咱们看看NioEventLoopGroup细节:

看到这里,和刚刚咱们看到的注释内容一致的,是使用FastThreadLocalThread的。

netty里面使用FastThreadLocal的举例经常使用的:

池化内存分配:

会使用到Recycler

而Recycler也使用了FastThreadLocal

咱们再看看看测试类:

备注: 咱们会发现FastThreadLocalFastPathBenchmark里面的线程是FastThreadLocal。

备注: 咱们会发现FastThreadLocalSlowPathBenchmark里面的线程不是FastThreadLocal

FastThreadLocal只有被的线程是FastThreadLocalThread或者其子类使用的时候才会更快,吞吐量我这边测试的效果大概3倍左右,可是若是是普通线程操做FastThreadLocal其吞吐量比ThreadLocal还差!

FastThreadLocal利用字节填充来解决伪共享问题

关于CPU 缓存 内容来源于美团:tech.meituan.com/2016/11/18/…

下图是计算的基本结构。L一、L二、L3分别表示一级缓存、二级缓存、三级缓存,越靠近CPU的缓存,速度越快,容量也越小。因此L1缓存很小但很快,而且紧靠着在使用它的CPU内核;L2大一些,也慢一些,而且仍然只能被一个单独的CPU核使用;L3更大、更慢,而且被单个插槽上的全部CPU核共享;最后是主存,由所有插槽上的全部CPU核共享。

img

当CPU执行运算的时候,它先去L1查找所需的数据、再去L二、而后是L3,若是最后这些缓存中都没有,所需的数据就要去主内存拿。走得越远,运算耗费的时间就越长。因此若是你在作一些很频繁的事,你要尽可能确保数据在L1缓存中。

另外,线程之间共享一份数据的时候,须要一个线程把数据写回主存,而另外一个线程访问主存中相应的数据。

下面是从CPU访问不一样层级数据的时间概念:

可见CPU读取主存中的数据会比从L1中读取慢了近2个数量级。

缓存行

Cache是由不少个cache line组成的。每一个cache line一般是64字节,而且它有效地引用主内存中的一起地址。一个Java的long类型变量是8字节,所以在一个缓存行中能够存8个long类型的变量。

CPU每次从主存中拉取数据时,会把相邻的数据也存入同一个cache line。

在访问一个long数组的时候,若是数组中的一个值被加载到缓存中,它会自动加载另外7个。所以你能很是快的遍历这个数组。事实上,你能够很是快速的遍历在连续内存块中分配的任意数据结构。

伪共享

因为多个线程同时操做同一缓存行的不一样变量,可是这些变量之间却没有啥关联,可是每次修改,都会致使缓存的数据变成无效,从而明明没有任何修改的内容,仍是须要去主存中读(CPU读取主存中的数据会比从L1中读取慢了近2个数量级)可是其实这块内容并无任何变化,因为缓存的最小单位是一个缓存行,这就是伪共享。

若是让多线程频繁操做的而且没有关系的变量在不一样的缓存行中,那么就不会由于缓存行的问题致使没有关系的变量的修改去影响另外没有修改的变量去读主存了(那么从L1中取是从主存取快2个数量级的)那么性能就会好不少不少。

有伪共享 和没有的状况的测试效果

代码路径:github.com/jiangxinlin…

nettydemo

利用字节填充来解决伪共享,从而速度快了3倍左右。

FastThreadLocal使用字节填充解决伪共享

以前介绍ThreadLocal的时候,说过ThreadLocal是用在多线程场景下,那么FastThreadLocal也是用在多线程场景,你们能够看下这篇:手撕面试题ThreadLocal!!!,因此FastThreadLocal须要解决伪共享问题,FastThreadLocal使用字节填充解决伪共享。

这个是我本身手算的,经过手算太麻烦,推荐一个工具JOL

openjdk.java.net/projects/co…

推荐IDEA插件:plugins.jetbrains.com/plugin/1095…

代码路径:github.com/jiangxinlin…

nettydemo

经过这个工具算起来就很容易了,若是之后有相似的须要看的,不用手一个一个算了。

FastThreadLocal被FastThreadLocalThread进行读写的时候也可能利用到缓存行

而且因为当线程是FastThreadLocalThread的时候操做FastThreadLocal是经过indexedVariables数组进行存储数据的的,每一个FastThreadLocal有一个常量下标,经过下标直接定位数组进行读写操做,当有不少FastThreadLocal的时候,也能够利用缓存行,好比一次indexedVariables数组第3个位置数据,因为缓存的最小单位是缓存行,顺便把后面的四、五、6等也缓存了,下次刚恰好另外FastThreadLocal下标就是5的时候,进行读取的时候就直接走缓存了,比走主存可能快2个数量级。

一点疑惑

问题: 为何这里填充了9个long值呢???

我提了一个issue:github.com/netty/netty…

虽然也有人回答,可是感受不是本身想要的,说服不了本身!!!

FastThreadLocal比ThreadLocal快,并非空间换时间

如今清理已经去掉,本文下面会介绍,因此FastThreadLocal比ThreadLocal快,并非空间换时间,FastThreadLocal并无浪费空间!!!

FastThreadLocal不在使用ObjectCleaner处理泄漏,必要的时候建议重写onRemoval方法

最新的netty版本中已经不在使用ObjectCleaner处理泄漏:

github.com/netty/netty…

github.com/netty/netty…

去掉缘由:

github.com/netty/netty…

咱们看看FastThreadLocal的onRemoval

若是使用的是FastThreadLocalThread能保证调用的,重写onRemoval作一些收尾状态修改等等

FastThreadLocal为何快?

FastThreadLocal操做元素的时候,使用常量下标在数组中进行定位元素来替代ThreadLocal经过哈希和哈希表,这个改动特别在频繁使用的时候,效果更加显著!计算该ThreadLocal须要存储的位置是经过hash算法肯定位置: int i = key.threadLocalHashCode & (len-1);而FastThreadLocal就是一个常量下标index,这个若是执行次数不少也是有影响的。

而且FastThreadLocal利用缓存行的特性,FastThreadLocal是经过indexedVariables数组进行存储数据的,若是有多个FastThreadLocal的时候,也能够利用缓存行,好比一次indexedVariables数组第3个位置数据,因为缓存的最小单位是缓存行,顺便把后面的四、五、6等也缓存了,下次刚恰好改线程须要读取另外的FastThreadLocal,这个FastThreadLocal的下标就是5的时候,进行读取的时候就直接走缓存了,比走主存可能快2个数量级而ThreadLocal经过hash是分散的。


若是读完以为有收获的话,欢迎点赞、关注、加公众号【匠心零度】,查阅更多精彩历史!!!

image
)
相关文章
相关标签/搜索