Java alibaba transmittable-thread-local 库:让 ThreadLocal 跨线程传播


#Java 笔记


了解该库之前,建议先看下:

源码地址: https://github.com/alibaba/transmittable-thread-local

可以简称为 TTL 。

maven 仓库地址: https://mvnrepository.com/artifact/com.alibaba/transmittable-thread-local

这个库解决了什么问题 ? ThreadLocal、InheritableThreadLocal 无法直接将值传播到新线程/线程池, 而该库可以解决这个问题。

引入依赖

gradle:

compile group: 'com.alibaba', name: 'transmittable-thread-local', version: '2.10.2'

maven:

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>transmittable-thread-local</artifactId>
    <version>2.10.2</version>
</dependency>

示例1:基本使用

import com.alibaba.ttl.TransmittableThreadLocal;

public class TtlTest {

    public static TransmittableThreadLocal<Integer> logId = new TransmittableThreadLocal<>();

    // 输出线程名、logId在当前线程的值
    public static void showLogId() {
        System.out.printf("%s : %s\n", Thread.currentThread().getName(), logId.get());
    }


    public static void main(String[] args) {
        showLogId();    // null

        logId.set(10);
        showLogId();    // 10

        logId.set(20);
        showLogId();    // 20

        logId.remove(); // 清除
        showLogId();    // null
    }
}

运行结果:

main : null
main : 10
main : 20
main : null

示例2:在多线程中使用

import com.alibaba.ttl.TransmittableThreadLocal;

public class TtlTest {

    public static TransmittableThreadLocal<Integer> logId = new TransmittableThreadLocal<>();

    // 输出线程名、logId在当前线程的值
    public static void showLogId() {
        System.out.printf("%s : %s\n", Thread.currentThread().getName(), logId.get());
    }


    public static void main(String[] args) throws InterruptedException {
        showLogId();    // null

        logId.set(10);
        showLogId();    // 10

        Thread t1 = new Thread(()->{
            showLogId();     // 10
            logId.set(20);
            showLogId();     // 20
        });

        Thread t2 = new Thread(()->{
            showLogId();   // 10
            logId.set(30);
            showLogId();   // 30
        });

        t1.start(); // 运行线程 t1
        t2.start(); // 运行线程 t2
        t1.join();  // 等待线程 t1 执行完
        t2.join();  // 等待线程 t2 执行完

        showLogId(); // 10
    }
}

执行结果:

main : null
main : 10
Thread-1 : 10
Thread-1 : 20
Thread-2 : 10
Thread-2 : 30
main : 10

可以看到 Thread-1 和 Thread-2 中的 logId的初始值都是 10,符合预期。

示例3:线程池错误的使用方式

import com.alibaba.ttl.TransmittableThreadLocal;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class TtlTest {

    public static TransmittableThreadLocal<Integer> logId = new TransmittableThreadLocal<>();

    // 输出线程名、logId在当前线程的值
    public static void showLogId() {
        System.out.printf("%s : %s\n", Thread.currentThread().getName(), logId.get());
    }


    public static void main(String[] args) throws InterruptedException {
        showLogId();    // null

        logId.set(10);
        showLogId();    // 10

        // 只有一个线程的线程池,该线程会复用
        ExecutorService executorService = Executors.newFixedThreadPool(1);
        // 提交task
        executorService.submit(() -> {
            showLogId();
            logId.set(20);
            showLogId();
        });

        // 再提交一个task
        executorService.submit(() -> {
            showLogId();
            logId.set(30);
            showLogId();
        });

        // 关闭线程池
        executorService.shutdown();
        // 等待至任务都执行完,最多等3秒
        executorService.awaitTermination(3, TimeUnit.SECONDS);

        showLogId();
    }
}

输出:

main : null
main : 10
pool-1-thread-1 : 10
pool-1-thread-1 : 20
pool-1-thread-1 : 20
pool-1-thread-1 : 30
main : 10

可以看到 logId 还是被线程池里的logId复用了。如果要 logId 不被复用,那么就要用 TTL 系统的包装类。

示例4:线程池下的使用方式 - 包装 Runnale

import com.alibaba.ttl.TransmittableThreadLocal;
import com.alibaba.ttl.TtlRunnable;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class TtlTest {

    public static TransmittableThreadLocal<Integer> logId = new TransmittableThreadLocal<>();

    // 输出线程名、logId在当前线程的值
    public static void showLogId() {
        System.out.printf("%s : %s\n", Thread.currentThread().getName(), logId.get());
    }


    public static void main(String[] args) throws InterruptedException {
        showLogId();    // null

        logId.set(10);
        showLogId();    // 10

        // 只有一个线程的线程池,该线程会复用
        ExecutorService executorService = Executors.newFixedThreadPool(1);
        // 提交task
        executorService.submit(TtlRunnable.get(() -> {
            showLogId();
            logId.set(20);
            showLogId();
        }));

        // 再提交一个task
        executorService.submit(TtlRunnable.get(() -> {
            showLogId();
            logId.set(30);
            showLogId();
        }));

        // 关闭线程池
        executorService.shutdown();
        // 等待至任务都执行完,最多等3秒
        executorService.awaitTermination(3, TimeUnit.SECONDS);

        showLogId();
    }
}

运行结果:

main : null
main : 10
pool-1-thread-1 : 10
pool-1-thread-1 : 20
pool-1-thread-1 : 10
pool-1-thread-1 : 30
main : 10

可以看到,两个 task 虽然都在同一个线程中依次执行,但是 logId 的初始值都是 10,符合预期。

示例5:线程池下的使用方式 - 包装线程池

import com.alibaba.ttl.TransmittableThreadLocal;
import com.alibaba.ttl.threadpool.TtlExecutors;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class TtlTest {

    public static TransmittableThreadLocal<Integer> logId = new TransmittableThreadLocal<>();

    // 输出线程名、logId在当前线程的值
    public static void showLogId() {
        System.out.printf("%s : %s\n", Thread.currentThread().getName(), logId.get());
    }


    public static void main(String[] args) throws InterruptedException {
        showLogId();    // null

        logId.set(10);
        showLogId();    // 10

        // 使用包装的线程池
        ExecutorService executorService = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(1));
        // 提交task
        executorService.submit(() -> {
            showLogId();
            logId.set(20);
            showLogId();
        });

        // 再提交一个task
        executorService.submit(() -> {
            showLogId();
            logId.set(30);
            showLogId();
        });

        // 关闭线程池
        executorService.shutdown();
        // 等待至任务都执行完,最多等3秒
        executorService.awaitTermination(3, TimeUnit.SECONDS);

        showLogId();
    }
}

运行结果:

main : null
main : 10
pool-1-thread-1 : 10
pool-1-thread-1 : 20
pool-1-thread-1 : 10
pool-1-thread-1 : 30
main : 10

可以看到,两个 task 虽然都在同一个线程中依次执行,但是 logId 的初始值都是 10,符合预期。

示例6: 使用 javaagent

使用 javaagent,就不用包装 Runnable 或者线程池了。但执行时需要指定 javaagent。

在 maven 仓库中下载 ttl 的jar包到本地。

代码如下:

import com.alibaba.ttl.TransmittableThreadLocal;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class TtlTest {

    public static TransmittableThreadLocal<Integer> logId = new TransmittableThreadLocal<>();

    // 输出线程名、logId在当前线程的值
    public static void showLogId() {
        System.out.printf("%s : %s\n", Thread.currentThread().getName(), logId.get());
    }


    public static void main(String[] args) throws InterruptedException {
        showLogId();    // null

        logId.set(10);
        showLogId();    // 10

        // 只有一个线程的线程池,该线程会复用
        ExecutorService executorService = Executors.newFixedThreadPool(1);
        // 提交task
        executorService.submit(() -> {
            showLogId();
            logId.set(20);
            showLogId();
        });

        // 再提交一个task
        executorService.submit(() -> {
            showLogId();
            logId.set(30);
            showLogId();
        });

        // 关闭线程池
        executorService.shutdown();
        // 等待至任务都执行完,最多等3秒
        executorService.awaitTermination(3, TimeUnit.SECONDS);

        showLogId();
    }
}

编译上面的源代码:

$ javac -cp ./transmittable-thread-local-2.10.2.jar TtlTest.java

-cp 换成 -classpath 也行。

运行时不指定 javaagent :

$ java -cp ./transmittable-thread-local-2.10.2.jar:. TtlTest
main : null
main : 10
pool-1-thread-1 : 10
pool-1-thread-1 : 20
pool-1-thread-1 : 20
pool-1-thread-1 : 30
main : 10

结果不符合预期。

运行时指定 javaagent:

$ java -cp ./transmittable-thread-local-2.10.2.jar:. -javaagent:./transmittable-thread-local-2.10.2.jar TtlTest
main : null
main : 10
pool-1-thread-1 : 10
pool-1-thread-1 : 20
pool-1-thread-1 : 10
pool-1-thread-1 : 30
main : 10

结果符合预期。



( 本文完 )