专栏名称: Java爱好者
分享android开发编程知识和相关技术应用
51好读  ›  专栏  ›  Java爱好者

这玩意比ThreadLocal叼多了,吓得我赶紧分享出来。

Java爱好者  · 公众号  ·  · 2020-12-16 09:48

正文

来自公众号: why技术

Dubbo的一次提交开始

故事得从前段时间翻阅 Dubbo 源码时,看到的一段代码讲起。

这段代码就是这个:

org.apache.dubbo.rpc.RpcContext

使用 InternalThreadLocal 提升性能。

相信作为一个程序猿,都会被 improve performance(提升性能)这样的字眼抓住眼球。

心里开始痒痒的,必须要一探究竟。

刚看到这段代码的时候,我就想:既然他是要提升性能,那说明之前的东西表现的不太好。

那之前的东西是什么?

经过长时间的推理、缜密的分析,我大胆的猜测道之前的东西就是:ThreadLocal。

来,带大家看一下:

果不其然,我真是太厉害了。

2018 年 5 月 15 日的提交:New threadLocal provides more performance. (#1745)

可以看到这次提交的后面跟了一个数字:1745。它对应一个 pr,链接如下:

https://github.com/apache/dubbo/pull/1745

在这个 pr 里面还是有很多有趣的东西的,出场人物一个比一个骚,文章的最后带大家看看,给大家分享一点用不到的知识。

能干啥用?

说 ThreadLocal 和 InternalThreadLocal 之前,还是先讲讲它们是干啥用的吧。

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

你想象你有一个场景,调用链路非常的长。当你在其中某个环节中查询到了一个数据后,最后的一个节点需要使用一下。

这个时候你怎么办?你是在每个接口的入参中都加上这个参数,传递进去,然后只有最后一个节点用吗?

可以实现,但是不太优雅。

你再想想一个场景,你有一个和业务没有一毛钱关系的参数,比如 traceId ,纯粹是为了做日志追踪用。

你加一个和业务无关的参数一路透传干啥玩意?

通常我们的做法是放在 ThreadLocal 里面,作为一个全局参数,在当前线程中的任何一个地方都可以直接读取。当然,如果你有修改需求也是可以的,视需求而定。

绝大部分的情况下,ThreadLocal 是适用于读多写少的场景中。

举三个框架源码中的例子,大家品一品。

第一个例子: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 才会生效。

附送一个知识点给你,不客气。

第二个例子:mybatis 的分页插件,PageHelper。

使用方法非常简单,从官网上截个图:

这里它为什么说:紧跟着的第一个 select 方法会被分页。

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

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

其实就是因为 PageHelper 方法使用了静态的 ThreadLocal 参数,分页参数和线程是绑定的:

如果我们写出下面这样的代码,就是不安全的用法:

这种情况下由于 param1 存在 null 的情况,就会导致 PageHelper 生产了一个分页参数,但是没有被消费,这个参数就会一直保留在这个线程上,也就是放在线程的 ThreadLocal 里面。

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

上面这个代码,应该写成下面这个样子:

这种写法,就能保证安全。

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

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

所以就算代码在进入 Executor 前发生异常,导致线程不可用的情况,比如常见的接口方法名称和 XML 中的不匹配,导致找不到 MappedStatement ,由于 finally 的自动清除,也不会导致 ThreadLocal 参数被错误的使用。

我看有的人为了保险起见这样去写:

怎么说呢,这个代码....

第三个例子: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的几个应用场景,那么这个 InternalThreadLocal 到底比 ThreadLocal 强在什么地方呢?

先说结论。

答案其实就写在类的 javadoc 上:

InternalThreadLocal 是 ThreadLocal 的一个变种,当配合 InternalThread 使用时,具有比普通 Thread 更高的访问性能。

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

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

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

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

本文主要聊聊 InternalThread ,但是我希望的是大家能学到这个类的思想,而不是用法。

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

public class InternalThreadLocalTest {

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

    public static void main(String[] args) {
        new InternalThread(() -> {
            for (int i = 0; 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                 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 冲突的解决方案呢?







请到「今天看啥」查看全文