ThreadLocal你搞懂了嘛?InternalThreadLocal ,FastThreadLocal如何做到提高性能

一篇文章让你搞懂ThreadLocal,InternalThreadLocal,FastThreadLocal

ThreadLocal

一. 能干啥用?

ThreadLocal 字面意思可以知道是线程本地变量,作为实现“线程封闭”的最主要的编程手段,经常被使用。比如,比如,比如,传统的SimpleDateFormat,不是线程安全的。如果你声明成全局变量,在并发环境下就会产生时间错乱。一种好的解决方式,就是使用ThreadLocal。

你想象你有一个场景,调用链路非常的长。当你在其中某个环节中查询到了一个数据后,最后的一个节点需要使用一下。
这个时候你怎么办?你是在每个接口的入参中都加上这个参数,传递进去,然后只有最后一个节点用吗?
可以实现,但是不太优雅。
你再想想一个场景,你有一个和业务没有一毛钱关系的参数,比如 traceId ,纯粹是为了做日志追踪用。
你加一个和业务无关的参数一路透传干啥玩意?
通常我们的做法是放在 ThreadLocal 里面,作为一个全局参数,在当前线程中的任何一个地方都可以直接读取。当然,如果你有修改需求也是可以的,视需求而定。
绝大部分的情况下,ThreadLocal 是适用于读多写少的场景中。

二. 使用非常广泛。举三个框架源码中的例子,大家品一品。

1. Spring 的事务

Spring 的事务是基于 AOP 实现的,AOP 是基于动态代理实现的。所以 @Transactional 注解如果想要生效,那么其调用方,需要是被 Spring 动态代理后的类。

因此如果在同一个类里面,使用 this 调用被 @Transactional 注解修饰的方法时,是不会生效的。

为什么?

因为 this 对象是未经动态代理后的对象。

那么我们怎么获取动态代理后的对象呢?

其中的一个方法就是通过 AopContext 来获取。
            在这里插入图片描述
其中第三步是这样获取的:AopContext.currentProxy();

然后我还非常高冷的(咦,想想就觉得羞耻)说了句:对于 AopContext 我多说几句。

看一下 AopContext 里面的 ThreadLocal:
在这里插入图片描述调用 currentProxy 方法时,就是从 ThreadLocal 里面获取当前类的代理类。

那他是怎么放进去的呢?

我高冷的第二句是这样说的:
在这里插入图片描述
对应的代码位置如下:
在这里插入图片描述
可以看到,经过一个 if 判断,如果为 true ,则调用 AopContext.setCurrentProxy 方法,把代理对象放到 AopContext 里面去。

而这个 if 判断的配置默认是 false,所以需要通过刚刚说的配置修改为 true,这样 AopContext 才会生效。

附送一个知识点给你,不客气。
                  在这里插入图片描述


2. mybatis 的分页插件,PageHelper。

大家一般是这样使用的吧。。
在这里插入图片描述
这里它为什么说:紧跟着的第一个 select 方法会被分页。

或者说:什么情况下会导致不安全的分页?

来,就当是一个面试题,并且我给你提示了:从 ThreadLocal 的角度去回答。

其实就是因为 PageHelper 方法使用了静态的 ThreadLocal 参数,分页参数和线程是绑定的:
在这里插入图片描述
如果我们写出下面这样的代码,就是不安全的用法:
在这里插入图片描述
这种情况下由于 sysAgent 存在 null 的情况,就会导致 PageHelper 生产了一个分页参数,但是没有被消费,这个参数就会一直保留在这个线程上,也就是放在线程的 ThreadLocal 里面。

当这个线程再次被使用时,就可能导致不该分页的方法去消费这个分页参数,这就产生了莫名其妙的分页。

上面这个代码,应该写成下面这个样子:
在这里插入图片描述
这种写法,就能保证安全。

核心思想就一句话:只要你可以保证在 PageHelper 方法调用后紧跟 MyBatis 查询方法,这就是安全的。

因为 PageHelper 在 finally 代码段中自动清除了 ThreadLocal 存储的对象。


3. Dubbo 的 RpcContext。

RpcContext 这个对象里面维护了两个 InternalThreadLocal,分别是存放 local 和 server 的上下文。

也就是我们说的增强版的 ThreadLocal:
在这里插入图片描述
作为一个 Dubbo 应用,它既可能是发起请求的消费者,也可能是接收请求的提供者。

每一次发起或者收到 RPC 调用的时候,上下文信息都会发生变化。

比如说:A 调用 B,B 调用 C。这个时候 B 既是消费者也是提供者。

那么当 A 调用 B,B 还是没调用 C 之前,RpcContext 里面保存的是 A 调用 B 的上下文信息。

当 B 开始调用 C 了,说明 A 到 B 之前的调用已经完成了,那么之前的上下文信息就应该清除掉。

这时 RpcContext 里面保存的应该是 B 调用 C 的上下文信息。否则会出现上下文污染的情况。

而这个上下文信息,就是维护在当前线程的 InternalThreadLocal 里面的。这个对象是在 ContextFilter 这个拦截器维护的。

ThreadLocal 在 Dubbo 里面的一个应用就是这样。

当然,还有很多很多其他的开源框架都使用了 ThreadLocal 。

可以说使用频率非常的高。

什么?你说你用的少?

那可不咋的,人家都给你封装好了,你当个黑盒,开箱即用。

其实你用了,只是你不知道而已。

三. 原理?

看过源码就不难回答。如下图(这张图最易懂),ThreadLocal的get和remove方法,只不过是一个使用的快捷方式。它的真正数据,是存在于线程中的一个叫做ThreadLocalMap的结构里。
一个ThreadLocal的值,会根据线程的不同,分散在N个线程中。所以获取ThreadLocal的Value,有两个步骤。

  • 第一步,根据线程获取Map
  • 第二部,根据自身从Map中获取值,所以它的this就是Map的Key
    在这里插入图片描述
    这没什么原理。这就是一个为了照顾编码习惯的数据结构。

四. 内存泄漏的问题?

严格来说,ThreadLocal没有内存泄漏问题。有的话,那就是你忘记执行remove方法。这是不正确使用引起的。
这和其他一些内存泄漏的问题是一致的,比如:

  • 流没有关闭
  • 连接没有断开
  • 滥用static map

为什么会有泄漏问题?
如果你不调用remove方法的话,ThreadLocal所对应的值,就会存在,一直到当前线程的销毁。
众所周知,线程的生命周期都比较长,加上现在普遍使用的线程池,会让线程的生命更加长。不remove,当然不会释放。这和Key,到底是不是弱引用,关系不大。
那这种情况,属不属于泄漏问题,是一个咬字眼的问题。面试的过程是探讨,并不一定要标准的答案。
比起内存泄漏问题,线程池所引起的数据错乱问题,更加应该引起关心。因为放在ThreadLocal的数据,肯定不会很大,泄漏顶多占用一点内存而已;而数据错乱,可是会引起业务Bug的。


InternalThreadLocal

一. 能干啥用的?

InternalThreadLocal 是 ThreadLocal 的增强版,所以他们的用途都是一样的,一言蔽之就是:传递信息。

二. 强在哪里?

先说结论。

答案其实就写在类的 javadoc 上:
在这里插入图片描述
InternalThreadLocal 是 ThreadLocal 的一个变种,当配合 InternalThread 使用时,具有比普通 Thread 更高的访问性能。

  • InternalThread 的内部使用的是数组,通过下标定位,非常的快。如果遇得扩容,直接搞一个扩大一倍的数组,然后copy 原数组,多余位置用指定对象填充,完事。

  • 而 ThreadLocal 的内部使用的是 hashCode 去获取值,多了一步计算的过程,而且用 hashCode 必然会遇到 hash 冲突的场景,ThreadLocal 还得去解决 hash 冲突,如果遇到扩容,扩容之后还得 rehash ,这可不得慢吗?

数据结构都不一样了,这其实就是这两个类的本质区别,也是 InternalThread 的性能在 Dubbo 的这个场景中比 ThreadLocal 好的根本原因。

而 InternalThread 这个设计思想是从 Netty 的 FastThreadLocal 中学来的。

首先,我们先搞个测试类:

public class InternalThreadLocalTest {
    
    

    private static InternalThreadLocal<Integer> internalThreadLocal_0 = new InternalThreadLocal<>();

    public static void main(String[] args) {
    
    
        new InternalThread(() -> {
    
    
            for (int i = 0; i < 5; i++) {
    
    
                internalThreadLocal_0.set(i);
                Integer value = internalThreadLocal_0.get();
                System.out.println(Thread.currentThread().getName()+":"+value);
            }
        }, "internalThread_have_set").start();

        new InternalThread(() -> {
    
    
            for (int i = 0; i < 5; i++) {
    
    
                Integer value = internalThreadLocal_0.get();
                System.out.println(Thread.currentThread().getName()+":"+value);
            }
        }, "internalThread_no_set").start();
    }
}

上面代码的运行结果是这样的:
在这里插入图片描述
由于 internalThread_no_set 这个线程没有调用 InternalThreadLocal 类的 set 方法,所以调用 get 方法输出为 null。

里面主要用到了 set、get 这一对方法。

下面借助 set 方法,带大家看看内部原理(先说一下,为了方便截图,我有可能会调整一下源码顺序):
      在这里插入图片描述
首先是判断了传进来的 value 是否是 null 或者是 UNSET,如果是则调用 remove 方法。

null 是好理解的。这个 UNSET 是个什么鬼?

根据 UNSET 能很容易的找到这个地方:
               在这里插入图片描述
原来是 InternalThreadLocalMap 初始化的时候会填充 UNSET 对象。

所以,如果 set 的对象是 UNSET,我们可以认为是需要把当前位置上的值替换为 UNSET,也就是 remove 掉。

而且,我们还看到了两个关键的信息:

  • 1.InternalThreadLocalMap 虽然名字叫做 Map ,但是它挂羊头卖狗肉,其实里面维护的是一个数组。

  • 2.数组初始化大小是 32。

接着我们回去看 else 分支的逻辑:
在这里插入图片描述
调用的是 InternalThreadLocalMap 对象的 get 方法。

而这个方法里面的两个 get 就有趣了。

能走到 fastGet 方法的,说明当前线程是 InternalThread 类,直接可以获取到类里面的 InternalThreadLocalMap。

如果走到 slowGet 了,则回退到原生的 ThreadLocal ,只是在原生的里面,我还是放的 InternalThreadLocalMap:
在这里插入图片描述
所以,其实线程上绑定的数据都是放到 InternalThreadLocalMap 里面的,不管你操作什么 ThreadLocal,实际上都是操作的 InternalThreadLocalMap。

那问题来了,你觉得一个叫做 fastGet ,一个叫做 slowGet。这个快慢,指的是 get 什么东西的快慢?
                        在这里插入图片描述
对咯,就是获取 InternalThreadLocalMap。

InternalThreadLocalMap 在 InternalThread 里面是一个变量维护的,可以直接通过 InternalThread.threadLocalMap() 获得:
在这里插入图片描述
标号为 ① 的地方是获取,标号为 ② 的地方是设置。

都是一步到位,操作起来非常的方便。

这是 fastGet。

而 slowGet 是从 ThreadLocal 中获取:
在这里插入图片描述
这里的 get ,就是原生 ThreadLocal 的 get 方法,一眼望去,就复杂多了:
            在这里插入图片描述
标号为 ① 的地方,首先计算 hash 值,然后拿着 hash 值去数组里面取数据。如果取出来的数据不是我们想要的数据,则到标号为 ② 的逻辑里面去。

那么我问你,除了这个位置上的值真的为 null 外,还有什么原因会导致我拿着计算出来的 hash 值去数组里面取数据取不到?
                在这里插入图片描述
就是看你熟不熟悉 ThreadLocal 对 hash 冲突的处理方式了。

那么这个问题稍微的升级一下就是:你知道哪些 hash 冲突的解决方案呢?

1.开放定址法。

2.链式地址法。

3.再哈希法。

4.建立公共溢出区。

我们非常熟悉的 HashMap 就是采用的链式地址法解决 hash 冲突。

而 ThreadLocal 用的就是开放定址法中的线性探测。

所谓线性探测就是,如果某个位置的值已经存在了,那么就在原来的值上往后加一个单位,直至不发生哈希冲突。
          在这里插入图片描述
上面的动图就是需要在一个长度为 7 的数组里面,再放一个经过 hash 计算后下标为 2 的数据,但是该位置上有值,也就是发生了 hash 冲突。

于是解决 hash 冲突的方法就是一次次的往后移,直到找到没有冲突的位置。

所以,当我们取值的时候如果发生了 hash 冲突也需要往后查询,这就是上面标号为 ③ 的 while 循环代码的其中一个目的。

当然它还顺便搞了一些其他的事情,就隐藏在 440 行的 expungeStaleEntry 方法里面。不是本文重点,就不多说了。

但是如果你不知道这个方法,你一定要去查阅一下相关的资料,有可能会在一定程度上改变你印象中的:用 ThreadLocal 会导致内存泄漏的风险。

至少,你可以知道 JDK 为了避免内存泄漏的问题,是做了自己的最大努力的。

好了,不扯远了,说回来。

从上面我们知道了,从 ThreadLocal 中获取 InternalThreadLocalMap 会经历如下步骤:

1.计算 hash 值。

2.判断通过 hash 值是否能直接获取到目标对象。

3.如果没有获取到目标对象则往后遍历,直至获取成功或者循环结束。

比从 InternalThread 里面获取 InternalThreadLocalMap 复杂多了。

现在你知道了 fastGet/slowGet 这个两个方法中的快慢,指的是从两个不同的 ThreadLocal 中获取 InternalThreadLocalMap 的操作的快慢。而快慢的根本原因是数据结构的差异。


FastThreadLocal

一. 能干啥用?

FastThreadLocal 并不是 JDK 自带的,而是在 Netty 中造的一个轮子,Netty 为什么要重复造轮子呢?
来看下它源码中的注释定义:

/**
 * A special variant of {@link ThreadLocal} that yields higher access performance when accessed from a
 * {@link FastThreadLocalThread}.
 * <p>
 * Internally, a {@link FastThreadLocal} uses a constant index in an array, instead of using hash code and hash table,
 * to look for a variable.  Although seemingly very subtle, it yields slight performance advantage over using a hash
 * table, and it is useful when accessed frequently.
 * </p><p>
 * To take advantage of this thread-local variable, your thread must be a {@link FastThreadLocalThread} or its subtype.
 * By default, all threads created by {@link DefaultThreadFactory} are {@link FastThreadLocalThread} due to this reason.
 * </p><p>
 * Note that the fast path is only possible on threads that extend {@link FastThreadLocalThread}, because it requires
 * a special field to store the necessary state.  An access by any other kind of thread falls back to a regular
 * {@link ThreadLocal}.
 * </p>
 *
 * @param <V> the type of the thread-local variable
 * @see ThreadLocal
 */
public class FastThreadLocal<V> {
    
    
    ...
}

FastThreadLocal 是一个特殊的 ThreadLocal 变体,当从线程类 FastThreadLocalThread 中访问 FastThreadLocalm时可以获得更高的访问性能。

二. 为什么快?

在 FastThreadLocal 内部,使用了索引常量代替了 Hash Code 和哈希表,源代码如下:

private final int index;

public FastThreadLocal() {
    
    
    index = InternalThreadLocalMap.nextVariableIndex();
}
public static int nextVariableIndex() {
    
    
    int index = nextIndex.getAndIncrement();
    if (index < 0) {
    
    
        nextIndex.decrementAndGet();
        throw new IllegalStateException("too many thread-local indexed variables");
    }
    return index;

FastThreadLocal 内部维护了一个索引常量 index,该常量在每次创建 FastThreadLocal 中都会自动+1,从而保证了下标的不重复性。

这要做虽然会产生大量的 index,但避免了在 ThreadLocal 中计算索引下标位置以及处理 hash 冲突带来的损耗,所以在操作数组时使用固定下标要比使用计算哈希下标有一定的性能优势,特别是在频繁使用时会非常显著,用空间换时间,这就是高性能 Netty 的巧妙之处。
要利用 FastThreadLocal 带来的性能优势,就必须结合使用 FastThreadLocalThread 线程类或其子类,因为 FastThreadLocalThread 线程类会存储必要的状态,如果使用了非 FastThreadLocalThread 线程类则会回到常规 ThreadLocal。

Netty 提供了继承类和实现接口的线程类:

  • FastThreadLocalRunnable
  • FastThreadLocalThread
    在这里插入图片描述
    Netty 也提供了 DefaultThreadFactory 工厂类,所有由 DefaultThreadFactory 工厂类创建的线程默认就是 FastThreadLocalThread 类型,来看下它的创建过程:
    在这里插入图片描述
    先创建 FastThreadLocalRunnable,再创建 FastThreadLocalThread,基友搭配,干活不累,一定要配合使用才“快”。

三. 实战体验

要使用 FastThreadLocal 就需要导入 Netty 的依赖了:

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.52.Final</version>
</dependency>

写一个测试小示例:

import io.netty.util.concurrent.DefaultThreadFactory;
import io.netty.util.concurrent.FastThreadLocal;

public class FastThreadLocalTest {
    
    

    public static final int MAX = 100000;

    public static void main(String[] args) {
    
    
        new Thread(() -> threadLocal()).start();
        new Thread(() -> fastThreadLocal()).start();
    }

    private static void fastThreadLocal() {
    
    
        long start = System.currentTimeMillis();
        DefaultThreadFactory defaultThreadFactory = new DefaultThreadFactory(FastThreadLocalTest.class);

        FastThreadLocal<String>[] fastThreadLocal = new FastThreadLocal[MAX];

        for (int i = 0; i < MAX; i++) {
    
    
            fastThreadLocal[i] = new FastThreadLocal<>();
        }

        Thread thread = defaultThreadFactory.newThread(() -> {
    
    
            for (int i = 0; i < MAX; i++) {
    
    
                fastThreadLocal[i].set("java: " + i);
            }

            System.out.println("fastThreadLocal set: " + (System.currentTimeMillis() - start));

            for (int i = 0; i < MAX; i++) {
    
    
                for (int j = 0; j < MAX; j++) {
    
    
                    fastThreadLocal[i].get();
                }
            }
        });
        thread.start();
        try {
    
    
            thread.join();
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }

        System.out.println("fastThreadLocal total: " + (System.currentTimeMillis() - start));
    }

    private static void threadLocal() {
    
    
        long start = System.currentTimeMillis();
        ThreadLocal<String>[] threadLocals = new ThreadLocal[MAX];

        for (int i = 0; i < MAX; i++) {
    
    
            threadLocals[i] = new ThreadLocal<>();
        }

        Thread thread = new Thread(() -> {
    
    
            for (int i = 0; i < MAX; i++) {
    
    
                threadLocals[i].set("java: " + i);
            }

            System.out.println("threadLocal set: " + (System.currentTimeMillis() - start));

            for (int i = 0; i < MAX; i++) {
    
    
                for (int j = 0; j < MAX; j++) {
    
    
                    threadLocals[i].get();
                }
            }
        });
        thread.start();
        try {
    
    
            thread.join();
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }

        System.out.println("threadLocal total: " + (System.currentTimeMillis() - start));
    }

}

结果输出:
在这里插入图片描述

可以看出,在大量读写面前,写操作的效率差不多,但读操作 FastThreadLocal 比 ThreadLocal 快的不是一个数量级,简直是秒杀 ThreadLocal 的存在。
当我把 MAX 值调整到 1000 时,结果输出:在这里插入图片描述

读写操作不多时,ThreadLocal 明显更胜一筹!
上面的示例是单线程测试多个 *ThreadLocal,即数组形式,另外,我也测试了多线程单个 *ThreadLocal,这时候 FastThreadLocal 效率就明显要落后于 ThreadLocal。。
最后需要说明的是,在使用完 FastThreadLocal 之后不用 remove 了,因为在 FastThreadLocalRunnable 中已经加了移除逻辑,在线程运行完时会移除全部绑定在当前线程上的所有变量。在这里插入图片描述

所以,使用 FastThreadLocal 导致内存溢出的概率会不会要低于 ThreadLocal?
不一定,因为 FastThreadLocal 会产生大量的 index 常量,所谓的空间换时间,所以感觉 FastThreadLocal 内存溢出的概率更大,但好在每次使用完都会自动 remove。

四. 总结

Netty 中的 FastThreadLocal 在大量频繁读写操作时效率要高于 ThreadLocal,但要注意结合 Netty 自带的线程类使用,这可能就是 Netty 为什么高性能的奥妙之一吧!

如果没有大量频繁读写操作的场景,JDK 自带的 ThreadLocal 足矣,并且性能还要优于 FastThreadLocal。


好了,今天的分享就到这里了,觉得有用,转发分享一下哦。

おすすめ

転載: blog.csdn.net/Number_oneEngineer/article/details/118481239