ThreadLocal


#Java 笔记


ThreadLocal,线程本地变量,可以理解为一个线程内部的全局变量。但是变量名是多线程可以共用的。

对于 ThreadLocal 的原理,可以简单地理解为内部用了一个 Map,key是线程对象,value是值注意,实际不是这样实现的)。当前的线程对象很好拿,就是Thread.currentThread()

常见方法:

方法名 方法属性 描述
set 实例方法 设置值
get 实例方法 获取值
remove 实例方法 清空值。清空后,若再获取值,会重新初始化值。
withInitial 静态方法 指定如何初始化值,并生成 ThresholdLocal 对象。如果没有用该方法生成 ThresholdLocal 对象,那么 ThresholdLocal 对象的初始化值是 null。

基本使用

在一个线程中使用 ThreadLocal 。

public class ThreadLocalTest {

    private static ThreadLocal<Integer> logId = new ThreadLocal<>();

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

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

        logId.set(10);
        showLogId();

        logId.set(20);
        showLogId();

        logId.remove();
        showLogId();
    }
}

运行后输出:

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

main 是主线程的名字。可以看到 ThreadLocal 变量的初始值是 null 。

我们再看下 withInitial 方法的使用:

public class ThreadLocalTest {

    private static ThreadLocal<Integer> logId = ThreadLocal.withInitial(() -> {
        return 1; // 这里只是一个示例,实际业务场景中可能是从数据源(如MySQL)中取数据
    });;


    public static void showLogId() {
        System.out.printf("%s : %s\n", Thread.currentThread().getName(), logId.get());
    }

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

        logId.set(20);
        showLogId();

        logId.remove();
        showLogId();
    }
}

运行结果:

main : 1
main : 20
main : 1

在多线程中使用

在多个线程中使用 ThreadLocal 。下面的 logId、showLogId 来自上面的示例。

showLogId();

logId.set(10);
showLogId();

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

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

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

showLogId(); // 再看下当前线程的 logId 值

运行结果:

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

可以看到,在主线程、Thread-0、Thread-1三个线程中,logId 的初始值都是 null,在一个线程中对 logId 设值,不应影响另外一个线程中的 logId 值。也就是 logId 在不同线程中互相隔离的。

在线程池中使用可能遇到问题

如果线程池中线程会被复用,这时 ThreadLocal 的值也会被复用。

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

public class ThreadLocalTest {

    private static ThreadLocal<Integer> logId = new ThreadLocal<>();


    public static void showLogId() {
        System.out.printf("%s : %s\n", Thread.currentThread().getName(), logId.get());
    }

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

        logId.set(10);
        showLogId();

        // 只有一个线程的线程池,该线程会复用
        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 : null
pool-1-thread-1 : 20
pool-1-thread-1 : 20
pool-1-thread-1 : 30
main : 10

可以看出来,我们在线程池中提交了两个任务,这两个任务都在一个线程中执行,也就是线程被复用了,所以 ThreadLocal 类型的 logId 也被复用了。

这个示例没有区分出两个task,我们优化下:

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

public class ThreadLocalTest {

    private static ThreadLocal<Integer> logId = new ThreadLocal<>();

    private static AtomicLong taskId = new AtomicLong();  // 生成任务编号


    public static void showLogId() {
        System.out.printf("%s : %s\n", Thread.currentThread().getName(), logId.get());
    }

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

        logId.set(10);
        showLogId();

        // 只有一个线程的线程池,该线程会复用
        ExecutorService executorService = Executors.newFixedThreadPool(1);

        // 提交task
        executorService.submit(() -> {
            Thread.currentThread().setName("task-" + taskId.addAndGet(1));
            showLogId();
            logId.set(20);
            showLogId();
        });

        // 再提交一个task
        executorService.submit(() -> {
            Thread.currentThread().setName("task-" + taskId.addAndGet(1));
            showLogId();
            logId.set(30);
            showLogId();
        });

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

        showLogId();
    }
}

运行后输出:

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

可以看到,线程被复用时,ThreadLocal 也被复用了,这可能不符合我们的预期,那怎么办?

我们可以对 Runnable 进行改造,见下面的示例。

如何避免 ThreadLocal 在线程池中被复用

如何避免线程池中线程被复用而导致的 ThreadLocal 被复用的问题呢 ?一个简单的思路是,我们对 Runnable 进行改造。

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

public class ThreadLocalTest {

    private static ThreadLocal<Integer> logId = new ThreadLocal<>();

    private static AtomicLong taskId = new AtomicLong();  // 生成任务编号

    // 加强版 Runnable
    public static class EnhancedRunnable implements Runnable {

        private Runnable delegate;

        public EnhancedRunnable(Runnable task) {
            delegate = task; // 在当前线程执行
        }

        public static EnhancedRunnable of(Runnable task) {
            return new EnhancedRunnable(task);
        }

        @Override
        public void run() {  // 在另外一个线程执行
            logId.remove(); // 清理 logId 的值
            delegate.run();
        }
    }

    public static void showLogId() {
        System.out.printf("%s : %s\n", Thread.currentThread().getName(), logId.get());
    }

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

        logId.set(10);
        showLogId();

        // 只有一个线程的线程池,该线程会复用
        ExecutorService executorService = Executors.newFixedThreadPool(1);

        // 提交 EnhancedRunnable 类型的任务
        executorService.submit(EnhancedRunnable.of(() -> {
            Thread.currentThread().setName("task-" + taskId.addAndGet(1));
            showLogId();
            logId.set(20);
            showLogId();
        }));

        // 再提交一个 EnhancedRunnable 类型的任务
        executorService.submit(EnhancedRunnable.of(() -> {
            Thread.currentThread().setName("task-" + taskId.addAndGet(1));
            showLogId();
            logId.set(30);
            showLogId();
        }));

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

        showLogId();
    }
}

运行结果:

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

可以看到

如何将 Threadlocal 传递到另外一个线程

传递到另外一个线程有两个场景:

  1. 传递给当前线程创建的新线程(也可以叫做当前线程的子线程)
  2. 传递给其他线程创建的新线程

但都是一样的处理。

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

public class ThreadLocalTest {

    private static ThreadLocal<Integer> logId = new ThreadLocal<>();

    private static AtomicLong taskId = new AtomicLong();  // 生成任务编号

    // 加强版 Runnable
    public static class EnhancedRunnable implements Runnable {

        private Runnable delegate;
        private Integer logIdValue;

        // 初始化是在当前线程中执行
        public EnhancedRunnable(Runnable task) {
            delegate = task;
            logIdValue = logId.get();
        }

        public static EnhancedRunnable of(Runnable task) {
            return new EnhancedRunnable(task);
        }

        // 下面的逻辑是在另外一个线程中执行
        @Override
        public void run() {
            logId.set(logIdValue); // 将提交任务到本线程的线程logId值设置到本线程
            delegate.run();
            logId.remove(); // 清理 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();

        logId.set(10);
        showLogId();

        // 只有一个线程的线程池,该线程会复用
        ExecutorService executorService = Executors.newFixedThreadPool(1);

        // 提交任务
        executorService.submit(EnhancedRunnable.of(() -> {
            Thread.currentThread().setName("task-" + taskId.addAndGet(1));
            showLogId();
            logId.set(20);
            showLogId();
        }));

        // 再提交一个任务
        executorService.submit(EnhancedRunnable.of(() -> {
            Thread.currentThread().setName("task-" + taskId.addAndGet(1));
            showLogId();
            logId.set(30);
            showLogId();
        }));

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

        showLogId();
    }
}

执行结果:

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

可以看到,main线程在设置自己的logId为10之后,提交了两个任务到线程池,这两个任务在新线程执行时,logId的初始值都是10。

Java 本身提供了一个 ThreadLocal 的加强版实现:InheritableThreadLocal (InheritableThreadLocal)。可以让子线程继承父线程的值,但无法处理线程池中的复用问题。 alibaba 的 transmittable-thread-local 库 (Java alibaba transmittable-thread-local 库:让 ThreadLocal 跨线程传播)提供了子线程/线程池传递ThreadLocal的能力,设计的基本思路和上面示例类似。

ThreadLocal 原理

  • Thread 类 和 ThreadLocal 类都在 java.lang 包中。
  • ThreadLocal 本身不直接存储数据,它里面定义了 ThreadLocalMap 类 ,key是 ThreadLocal 对象,value是 ThreadLocal 对象对应的值。key和value被封装在 ThreadLocalMap.Entry 中,ThreadLocalMap.Entry 继承自 WeakReference,所以不会出现内存问题。
  • 每个Thread对象都有一个 ThreadLocalMap 类型的实例变量 threadLocals 。
  • 调用 ThreadLocal 变量(比如叫 logId)的get方法时,先拿到当前线程的 threadLocals 值。
    1. 若 threadLocals 为 null,则初始化该对象。
    2. 判断 threadLocals 中是否有 logId 为key 的数据;若有,则直接返回;若无,则 logId 和 对应的初始化值(一般是 null),放入threadLocals中,然后返回。
  • 调用 ThreadLocal 变量(比如叫 logId)的set方法(例如设值为 123)时,先拿到当前线程的 threadLocals 值。
    1. 若 threadLocals 为 null,则初始化该对象。
    2. 将 logId 为key,123 作为value,放入 threadLocals 中。


( 本文完 )