ThreadLocal & InheritableThreadLocal

这是一个坑位post。

有时间写一下 ThreadLocal 和 InheritableThreadLocal 的区别及应用。

ThreadLocal应用在@Transactional

InheritableThreadLocal应用在TraceId


来填坑了! 首先先上代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// demo1
public class Test {

public static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>();

public static void main(String args[]) {
threadLocal.set(new Integer(456));
Thread thread = new MyThread();
thread.start();
System.out.println("main = " + threadLocal.get());
}

static class MyThread extends Thread {
@Override
public void run() {
System.out.println("MyThread = " + threadLocal.get());
}
}
}

这是从网上随便找的一个例子。还有一个版本作为对比。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// demo2
public class Test {

public static ThreadLocal<Integer> threadLocal = new InheritableThreadLocal<Integer>();

public static void main(String args[]) {
threadLocal.set(new Integer(456));
Thread thread = new MyThread();
thread.start();
System.out.println("main = " + threadLocal.get());
}

static class MyThread extends Thread {
@Override
public void run() {
System.out.println("MyThread = " + threadLocal.get());
}
}
}

先说这两段代码的输出结果。 demo1的输出结果是456和null,而demo2的输出结果是456和456。 这两段代码输出结果的不同直接反应了ThreadLocalInheritableThreadLocal的区别。其实从类的名称上我们也能看出,前者是普通的线程局部变量类和后者在前者基础上加了线程的继承关系。也就是说,通过InheritableThreadLocal类我们可以在子线程中获取父线程的局部变量。为什么要这样设置呢?以下是我的猜测和理解。

在此之前,先说一下这两个类的原理。

为什么可以做到线程隔离的局部变量呢?

其实我们可以按照普通的多线程的思路先想一下,如果一段代码可以多线程进行执行,并且操作了某个局部变量或者全局变量,那么所有线程对这个变量的操作结果都是对其他线程可见的。这样就会有我们经常说的变量同步问题。多线程下的变量会因为多个线程同时操作导致变量出现混乱的情况。最简单的例子就是多个线程对一个int变量进行加一操作。如果不加任何同步限制措施的话,多线程高并发的情况下,大概率最终这个变量的结果会错误。这就是我们经常在多线程操作过程中遇到的同步问题。解决方法也是我们常用的代码块只能一个线程进入、volatile关键字、乐观锁等等。

既然有多个线程使用同一个变量场景存在,那么如果多个线程只想操作自己独有的变量怎么办?比如Spring中的事务处理。Spring事务处理过程中,可以分为三个阶段:

  1. 准备阶段:获取数据库连接、开启事务
  2. 业务逻辑处理
  3. 提交事务

事务的三个阶段最重要的要求是三部分必须是同一个数据库连接。而Spring支持声明式事务,所以需要保证对业务方法增强时,使用到的数据库连接保持不变。所以需要使用ThreadLocal

但是,ThreadLocal是怎么实现变量绑定线程的呢?其实这个原理也很好理解。每个线程需要持有自己独有的变量,那么我们就把变量内容绑定到线程里不就可以了嘛!Thread类中有一个ThreadLocalMap用于存储设置到线程中的数据。ThreadLocal暴露setget方法进行变量设置和获取。具体原理可以直接看源码,还是很好理解的。

ThreadLocal是只能获取当前线程设置的变量,而InheritableThreadLocal可以获取到当前线程父线程的变量。这又是什么实现的呢?其实Thread类中除了有保存当前线程变量的ThreadLocalMap外,还有一个保存父级线程变量的ThreadLocalMap。当一个线程创建时会复制父线程的局部变量到当前线程的inheritableThreadLocals里,这样每次执行ThreadLocalget方法后就可以获取到父线程的变量了。

原理理解起来很简单,但是还是建议多研究源代码。毕竟知道是一回事,能根据原理写出自己的代码又是另外一回事。

回到最初的问题。
这两个类有什么作用,我们应该怎么用这两个功能。

上面已经提到了ThreadLocal在Spring事务中的应用,这是日常工作中比较常见的一种情况。只要遇到需要线程隔离变量的情况,其实就可以考虑ThreadLocalInheritableThreadLocal。另外还有一种应用场景是traceId的创建和传递。

为了记录一个请求中传了什么数据、处理得到了什么结果、处理过程中出现了什么错误,我们需要借助日志把一个请求的所有处理信息和上下文信息记录下来,以便问题排查、解决。traceId就可以把一个请求链路中所有的信息进行统一标识、记录。后端服务在处理数据和业务逻辑的过程中,会调用不同类的不同方法,就会产生长长的调用链,每个调用节点可能都会产生日志。通过对每个调用节点的日志打上traceId的标签就可以把一个请求链路上的所有日志进行标识。那么应该怎么打这个标签呢?

对于一个Spring服务,一个请求往往都是使用一个线程进行处理,并把处理结果返回给调用方。所以,结合前面的了解,我们就可以想到使用ThreadLocal来保存traceId,在打印日志时取出,并拼接到日志中,就可以保证每个请求的traceId不同。

当然,如果一个请求中使用到了多线程来处理数据,并打印日志。那么使用ThreadLocal就不太行了,这时InheritableThreadLocal就可以出马了。

日常工作中,需要写类似的代码的情况可能比较少,但是应该提前准备好,理解技术的原理和使用场景,在需要使用到的时候就可以得心应手了。