前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Netty对ThreadLocal的升级

Netty对ThreadLocal的升级

作者头像
早安嵩骏
发布2020-08-11 16:44:31
8770
发布2020-08-11 16:44:31
举报
文章被收录于专栏:程序猿人程序猿人

ThreadLocal概念

以上是ThreadLocal的注释,大致意思是:ThreadLocal提供了线程局部变量的能力。这些变量与普通变量的不同之处在于每个线程都有自己独立的副本变量,ThreadLocal实例通常是类中希望将状态与线程关联起来的私有静态字段(例如用户ID或者事务ID)

另外,使用ThreadLocal而不使用普通变量还有一层原因就是ThreadLocal封装的非常优雅。

ThreadLocal实现

该实现应用了ThreadLocal所暴露出的所有APi

代码语言:javascript
复制
ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "init");
Thread[] threads = new Thread[5];
for (int i = 0; i < threads.length; i++) {
    threads[i] = new Thread(new Runnable() {
        @Override
        public void run() {
            threadLocal.set("threadName:" + Thread.currentThread().getName());
            if (Thread.currentThread().getName().equals("thread1")) threadLocal.remove();
            System.out.println(threadLocal.get());
        }
    }, "thread" + i);
}

for (Thread thread : threads) {
    thread.start();
}

执行结果:

ThreadLocal源码

以上是idea导出类图。ThreadLocal有2个内部类:ThreadLocalMap和SupliedThreadLocal。

ThreadLocalMap

ThreadLocal的主要结构是一个散列表结构,key为ThreadLocal本身,value为通过set()方法进去的数据。ThreadLocal的散列表由其内部类ThreadLocalMap实现。它与我们平时经常使用的HashMap不同,HashMap解决散列冲突的方法是拉链法,而ThreadLocalMap使用的是开放定址法,这也反向说明不要在ThraedLocal存储过多数据。

开放定址法:也叫闭散列,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的下一个空位置中去。这块可以具体看下代码:

代码语言:javascript
复制
private void set(ThreadLocal<?> key, Object value) {
    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();
}

这里有个变量:threadLocalHashCode获取逻辑等同如下公式:

代码语言:javascript
复制
(n*0x61c88647)&(table.length - 1)
  • n 为当前ThreadLocal申请的次序,从0开始;
  • table.length 为2个指数,初始值为16;
  • 0x61c88647是一个黄金数字,等于(√5-1) * (2^31),它能够很大程度地防止hash碰撞。

在上述代码中,有段逻辑值得深究:

代码语言:javascript
复制
if (k == null) {
    replaceStaleEntry(key, value, i);
    return;
}

在这段逻辑中Entry对象不为null,但是k却为null。这种情况是因为Entry中key值的实现是弱引用,对象在只有弱引用存在的时候,发生GC就会被回收。

当key被回收,value却还在,ThreadLocalMap中有多处实现会清空这类对象,replaceStaleEntry就是。但是这依赖于二次调用的过程,如果我们是在使用池化线程,还是需要将不用的ThreadLocal remove掉,以避免内存泄漏的发生。

ThreadLocalMap虽然维护在ThreadLocal中,但是它却被存储在Thread中,每个Thread独自保存自己的ThreadLocalMap,也就是这一实现赋予了ThreadLocal线程安全的特点,如下:

其中第二个与常规ThreadLocal不同的是,它由ThreadLocal的子类InheritableThreadLocal维护,且支持继承父线程的InheritableThreadLocal。这一实现可以查看Thread类的init方法看到。

SupliedThreadLocal

SupliedThreadLocal同样是ThradLocal的静态内部子类,它的实现主要是提供一个初始化方法:

在我们开始的实现中使用了withInitial方法,其目的就是将ThreadLocal转换为了其静态内部子类SupliedThreadLocal,这样在使用threadlocal.get结果为null的时候,就会自动调用我们给到的lambda函数,源码如下:

FastThreadLocal的升级点

数组

Netty对ThreadLocal进行了进一步优化,在FastThreadLocal中不再需要散列表,而是直接使用数组,使其在频繁访问时具有更高的性能。

使用FastThreadLocal必须是在Netty实现的FastThreadLocalThread或者其子类中,由于这个原因由DefaultThreadFactory创建的所有线程都是FastThreadLocalThread。

防内存泄漏

线程执行完执行FastThreadLocal的remove操作。

variablesToRemoveIndex=0,这个过程也就是将另外一个Set放在了数组0的位置,Set存放的是当前线程所有的FastThreadLocal,方便线程执行完的时候统一清理。

使用字节填充解决伪共享

众所周知,CPU和磁盘之间的速度相差悬殊,为了弥补这种差距,计算机设计了多层缓存。当CPU执行运算的时候,它先去L1查找所需的数据、再去L2、然后是L3,如果最后这些缓存中都没有,所需的数据就要去主内存拿。走得越远,运算耗费的时间就越长。所以如果你在做一些很频繁的事,你要尽量确保数据在L1缓存中。

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

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

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

Cache Line

现代CPU以一整块连续的块为单位,称为Cache Line(缓存行)。所以通常情况下访问连续存储的数据会比随机访问的快。缓存行的大小一般为64b,所以对于CPU来说,每次读取数据都会加载连续的64b。

伪共享

根据MESI协议,如果一个核正在使用缓存被其他核修改,那么整个缓存行就会失效。这个时候多个核的多个数据共享一个缓存行,就会导致缓存行的频繁失效。这种没有数据竞争而导致的缓存行失效就叫做伪共享。在java中一般采用字节填充的方式来解决伪共享问题。

在FastThreadLocal中有如下代码:

这里填充了72个字节来保证在启用CompressedOops的情况下,该类的一个实例至少占用128字节,一次来解决FastThreadLocal的伪共享问题,进而提高速度。

不过目前看这块有一点问题:通过在IDEA上安装插件JOL,查看IntenalThreadLocalMap

可见其一个对象136个字节,这个有点奇怪。https://github.com/netty/netty/issues/9284 问了同样的问题,

我觉得这个回答比较靠谱。

不过在JDK1.8中增加了注解@Contended来解决伪共享的问题,默认情况下,除了 JDK 内部的类,JVM 会忽略该注解。要应用代码支持的话,要设置 -XX:-RestrictContended=false,它默认为 true(意味仅限 JDK 内部的类使用)。当然,也有个 –XX: EnableContented 的配置参数,来控制开启和关闭该注解的功能,默认是 true,如果改为 false,可以减少 Thread 和 ConcurrentHashMap 类的大小。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-08-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 程序猿人 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • ThreadLocal实现
  • ThreadLocal源码
    • ThreadLocalMap
      • SupliedThreadLocal
        • 数组
        • 防内存泄漏
          • 使用字节填充解决伪共享
            • Cache Line
            • 伪共享
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档