# 5.3 线程关联

# 5.3.1 线程上下文丢失

ThreadLocal能够很好的解决线程内部的上下文传递问题,但是对于使用多线程的异步场景,线程上下文会丢失。 下面的代码,在主线程中设置线程变量,然后启动一个子线程,在子线程中获取线程变量的值。

public class ThreadLocalDemo {

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

    public static void main(String[] args) {
        // 设置线程变量的值 (main线程)
        context.set(1000);

        // 从线程变量中取出值
        Integer ctx = context.get();
        System.out.println("ctx= " + ctx);
        
        // thread线程是main线程创建的子线程
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                Integer ctx = context.get();
                System.out.println("ctx= " + ctx);
            }
        });

        thread.start();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

输出结果如下,可以看到子线程无法获取主线程设置的线程变量。

ctx= 1000
ctx= null
1
2

从线程变量的名称和作用来看,这个子线程获取为空是符合预期的,但是从线程上下文传递的功能角度来看,却是不满足需求的。 于是Java官方又提供了ThreadLocal的子类InheritableThreadLocal来解决创建新线程时的上下文传递丢失的问题。

# 5.3.2 InheritableThreadLocal

使用TheadLocal时,子线程访问不了父线程的本地变量,InheritableThreadLocal很好的解决了该问题。 InheritableThreadLocal源码如下。

public class InheritableThreadLocal<T> extends ThreadLocal<T> {

    // 接收父线程本地变量的值
    // 这个方法在父线程创建子线程时调用
    protected T childValue(T parentValue) {
        // 这里是直接返回原值
        return parentValue;
    }
    
    // 使用inheritableThreadLocals保存线程变量
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

    // 初始化inheritableThreadLocals
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

不同于ThreadLocal,在使用InheritableThreadLocal对象时,变量保存在inheritableThreadLocals中。 下面是Thread类中两个变量的定义。

ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
1
2

再来看下在线程创建时如何实现线程变量的copy过程。

private Thread(ThreadGroup g, Runnable target, String name,
                long stackSize, AccessControlContext acc,
                boolean inheritThreadLocals) {
     this.name = name;

     Thread parent = currentThread();
     // 安全、校验等代码省略...    
     
     // 线程常规的初始化动作
     this.group = g;
     this.daemon = parent.isDaemon();
     this.priority = parent.getPriority();
     this.target = target;
     setPriority(priority);
     
     // 线程变量的map拷贝
     if (inheritThreadLocals && parent.inheritableThreadLocals != null){
         this.inheritableThreadLocals =
             ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
     }
     
     this.stackSize = stackSize;

     this.tid = nextThreadID();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

线程变量的map拷贝在ThreadLocal.createInheritedMap中,实际是创建一个新的map并将值复制一份。

private ThreadLocalMap(ThreadLocalMap parentMap) {
    Entry[] parentTable = parentMap.table;
    int len = parentTable.length;
    setThreshold(len);
    table = new Entry[len];
    
    // 遍历父线程的table
    for (Entry e : parentTable) {
        if (e != null) {
            ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
            if (key != null) {
                // 赋值
                Object value = key.childValue(e.value);
                Entry c = new Entry(key, value);
                int h = key.threadLocalHashCode & (len - 1);
                while (table[h] != null)
                    h = nextIndex(h, len);
                table[h] = c;
                size++;
            }
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 5.3.3 transmittable-thread-local

JDK的InheritableThreadLocal类可以完成父线程到子线程的值传递。 对于线程池场景,线程由线程池创建好,并且线程是池化起来反复使用的, 这时父子线程关系的ThreadLocal值传递已经没有意义,应用需要的实际上是把任务提交给线程池时的ThreadLocal值传递到任务执行时。

TransmittableThreadLocal(TTL) 是阿里巴巴开源的项目,在使用线程池等会池化复用线程的执行组件情况下, 提供ThreadLocal值的传递功能,解决异步执行时上下文传递的问题。

TransmittableThreadLocal继承InheritableThreadLocal,使用方式也类似。 相比InheritableThreadLocal,添加了protected的transmitteeValue()方法, 用于定制任务提交给线程池时的ThreadLocal值传递到任务执行时的传递方式。

# 5.3.3.1 简单使用

  • 父线程给子线程传递值
TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();

// =====================================================

// 在父线程中设置
context.set("value-set-in-parent");

// =====================================================

// 在子线程中可以读取,值是"value-set-in-parent"
String value = context.get();
1
2
3
4
5
6
7
8
9
10
11

这其实是InheritableThreadLocal的功能,可以使用InheritableThreadLocal来完成。

# 5.3.3.2 线程池中传递值

  • 修饰Runnable和Callable
TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();

// =====================================================

// 在父线程中设置
context.set("value-set-in-parent");

Runnable task = new RunnableTask();
// 额外的处理,生成修饰了的对象ttlRunnable
Runnable ttlRunnable = TtlRunnable.get(task);
executorService.submit(ttlRunnable);

// =====================================================

// Task中可以读取,值是"value-set-in-parent"
String value = context.get();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

上面演示了Runnable,Callable的处理类似。

TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();

// =====================================================

// 在父线程中设置
context.set("value-set-in-parent");

Callable call = new CallableTask();
// 额外的处理,生成修饰了的对象ttlCallable
Callable ttlCallable = TtlCallable.get(call);
executorService.submit(ttlCallable);

// =====================================================

// Call中可以读取,值是"value-set-in-parent"
String value = context.get();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  • 修饰线程池

省去每次Runnable和Callable传入线程池时的修饰,这个逻辑可以在线程池中完成。 例子如下:

ExecutorService executorService = ...
// 额外的处理,生成修饰了的对象executorService
executorService = TtlExecutors.getTtlExecutorService(executorService);

TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();

// =====================================================

// 在父线程中设置
context.set("value-set-in-parent");

Runnable task = new RunnableTask();
Callable call = new CallableTask();
executorService.submit(task);
executorService.submit(call);

// =====================================================

// Task或是Call中可以读取,值是"value-set-in-parent"
String value = context.get();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  • 使用Java Agent来修饰JDK线程池实现类

相比于SDK方式,这种方式,实现线程池上下文的传递是透明的,业务代码中没有修饰Runnable或是线程池的代码,即可以做到应用代码无侵入。

使用需要在应用启动参数中增加一个premain的agent,在应用启动之前修改线程的字节码,接入方式如下:

java -javaagent:path/to/transmittable-thread-local-2.x.y.jar springboot-application.jar
1

需要注意的是,如果有多个JavaAgent,需要将transmittable的Agent参数放到其他Agent参数之前。