大纲
ThreadLocal 的介绍
- (1) 在 Java 中,ThreadLocal 是为每个线程独立存储变量的机制。它使得每个线程都能拥有一个独立于其他线程的变量副本,这样可以避免线程间的变量共享,从而保证线程安全。ThreadLocal 实例通常是类中的私有静态字段(属性),使用它的目的是希望将状态(例如,用户 ID 或者事务 ID)与线程关联起来。
- (2) ThreadLocal 实现了每一个线程都有自己专属的本地变量副本,主要解决了让每个线程都可以绑定自己的变量值。通过调用 ThreadLocal 的
get()
和 set()
方法,获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。如果不使用 ThreadLocal 或者其他手段(比如加锁)来控制共享变量的并发修改,那么就会出现线程安全问题,其根本原因在于 Java 内存模型(JMM) 的结构。
总结
- 每个线程都有自己的变量副本,且该变量副本只允许当前线程自己使用。
- 既然其他线程不可以使用,那就不存在多线程间数据共享的问题(线程安全问题)。
- ThreadLocal 要求统一设置变量的初始化值,但是每个线程对这个值的修改都是互相独立的,互不影响。
ThreadLocal 的作用
- 线程隔离:
- ThreadLocal 为每个线程提供了独立的变量副本,每个线程都可以独立地读取和修改自己的变量副本,互不影响。这样可以实现线程间的数据隔离,避免了多线程之间共享变量带来的并发访问问题。
- 线程上下文传递:
- 在多线程应用程序中,有时需要在线程之间传递一些上下文信息,例如用户身份认证信息、数据库连接等。使用 ThreadLocal 可以方便地在不同的线程中共享这些上下文信息,而不需要显式地将它们作为参数传递给每个方法或对象。
- 线程安全性:
- 由于每个线程都拥有自己的变量副本,因此不需要使用同步机制来保护线程局部变量。这可以减少对锁的需求,提高程序的并发性能。
- 性能提升:
- 相比于使用全局变量或共享变量,使用 ThreadLocal 可以避免线程间的竞争和同步开销。每个线程独立地操作自己的变量副本,不会出现线程冲突,从而提高了程序的并发性能。
ThreadLocal 的缺点
- 内存泄漏:
- 由于 ThreadLocal 内部使用了 ThreadLocalMap 来存储数据,而 ThreadLocalMap 的键是 ThreadLocal 的弱引用,值是强引用。当线程长时间运行且没有及时清除 ThreadLocal 变量时,可能导致内存泄漏。虽然 ThreadLocal 的键可以被垃圾回收,但值仍然会被线程引用,无法被回收。
- 线程复用问题:
- 在线程池中,线程是被复用的。如果在某个线程上设置了 ThreadLocal 变量,但没有及时清除,当这个线程被再次使用时,可能会继承之前任务的状态,导致不可预测的行为。因此,需要在适当的时候手动调用
remove()
方法清理数据,这增加了使用的复杂度。
ThreadLocal 的适用场景
- 数据库连接管理:
- 在多线程环境下,为每个线程提供一个独立的数据库连接实例,避免多个线程共享同一个连接,从而防止并发问题和数据一致性问题。
- 会话管理:
- 在 Web 应用中,为每个线程提供独立的会话信息存储,可以避免线程间共享会话信息。
- 用户信息存储:
- 在认证系统中,为每个线程提供独立的用户身份信息存储,确保用户请求的独立性和安全性。
- 事务管理
- 在事务处理中,可以使用 ThreadLocal 来保存事务相关的信息,确保每个线程的事务处理独立。
- 日志追踪:
- 在分布式系统中,通过 ThreadLocal 保存每个请求的唯一标识符(如 Trace ID),可以实现日志的全链路追踪,便于问题定位。
ThreadLocal 的核心 API
ThreadLocal 的使用案例
本案例将模拟实现以下业务场景:
- 门店里有 3 个销售员卖车,求 3 个销售员的总销售额。
- 门店里有 3 个销售员卖车,求 3 个销售员的个人销售额。
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| public class ThreadLocalDemo {
public static void main(String[] args) throws InterruptedException { final int employee = 3; CountDownLatch countDownLatch = new CountDownLatch(employee);
Car car = new Car();
for (int i = 1; i <= employee; i++) { new Thread(() -> { try { for (int j = 1; j <= new Random().nextInt(3) + 1; j++) { car.updateSaleTotal(); car.updateSalePersonal(); } System.out.println(Thread.currentThread().getName() + " 号销售员的销售额:" + car.salePersonal.get()); } finally { car.salePersonal.remove(); countDownLatch.countDown(); } }, String.valueOf(i)).start(); }
countDownLatch.await();
System.out.println("本门店总销售额:" + car.getSaleTotal()); }
private static class Car {
@Getter private int saleTotal;
public ThreadLocal<Integer> salePersonal = ThreadLocal.withInitial(() -> 0);
public void updateSalePersonal() { int newTotal = salePersonal.get() + 1; salePersonal.set(newTotal); }
public synchronized void updateSaleTotal() { saleTotal++; }
}
}
|
程序运行的输出结果:
1 2 3 4
| 1 号销售员的销售额:1 2 号销售员的销售额:3 3 号销售员的销售额:2 本门店总销售额:6
|
ThreadLocal 的使用问题
内存泄漏问题
ThreadLocal
会发生内存泄漏的主要原因在于 ThreadLocal
的实现机制。每个线程都是一个 Thread
对象,而这个对象内部包含了一个 ThreadLocalMap
,用于存储 ThreadLocal
变量。这些变量实际上是存储在 ThreadLocalMap
中的键值对,其中键是 ThreadLocal
的弱引用,而值是线程局部变量的值。- 当一个
ThreadLocal
变量不再使用,并且没有强引用指向它时,垃圾回收器会回收这个 ThreadLocal
对象。然而,由于 ThreadLocalMap
的键是弱引用,但值却是强引用,因此即使 ThreadLocal
本身会被回收,值依然会被保留在 ThreadLocalMap
中,导致值无法被垃圾回收,从而引发内存泄漏。 - 内存泄漏最典型的场景是长生命周期的线程(如线程池中的线程)持有
ThreadLocal
变量,而 ThreadLocal
变量的生命周期较短。当 ThreadLocal
被回收后,它对应的值仍然保存在 ThreadLocalMap
中,从而导致值无法被垃圾回收。
线程池的线程复用问题
在线程池中,线程是会被复用的。如果在某个线程上设置了 ThreadLocal 变量,但没有及时清除,当这个线程被再次使用时,可能会继承之前任务的状态(变量值),导致不可预测的行为(比如内存泄漏、业务逻辑出错)。因此,当使用完 ThreadLocal
变量后,需要在适当的时候手动调用其 remove()
方法来清除当前线程的 ThreadLocal
变量。
阿里巴巴开发手册
线程池的错误使用
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| public class ThreadLocalAndThreadPoolDemo {
public static void main(String[] args) { MyData myData = new MyData(); ExecutorService threadPool = Executors.newFixedThreadPool(3);
try { for (int i = 1; i <= 10; i++) { final int index = i; threadPool.submit(() -> { Integer beforeValue = myData.threadLocal.get(); myData.add(); Integer afterValue = myData.threadLocal.get(); System.out.println( Thread.currentThread().getName() + "\t工作窗口\t" + "受理第: " + index + " 个顾客," + "beforeValue: " + beforeValue + "\t" + "afterValue: " + afterValue); }); } } catch (Exception e) { e.printStackTrace(); } finally { threadPool.shutdown(); } }
private static class MyData {
ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
public void add() { threadLocal.set(threadLocal.get() + 1); }
}
}
|
程序运行的输出结果:
1 2 3 4 5 6 7 8 9 10
| pool-1-thread-3 工作窗口 受理第: 3 个顾客,beforeValue: 0 afterValue: 1 pool-1-thread-1 工作窗口 受理第: 1 个顾客,beforeValue: 0 afterValue: 1 pool-1-thread-2 工作窗口 受理第: 2 个顾客,beforeValue: 0 afterValue: 1 pool-1-thread-3 工作窗口 受理第: 4 个顾客,beforeValue: 1 afterValue: 2 pool-1-thread-1 工作窗口 受理第: 5 个顾客,beforeValue: 1 afterValue: 2 pool-1-thread-3 工作窗口 受理第: 7 个顾客,beforeValue: 2 afterValue: 3 pool-1-thread-2 工作窗口 受理第: 6 个顾客,beforeValue: 1 afterValue: 2 pool-1-thread-3 工作窗口 受理第: 9 个顾客,beforeValue: 3 afterValue: 4 pool-1-thread-1 工作窗口 受理第: 8 个顾客,beforeValue: 2 afterValue: 3 pool-1-thread-2 工作窗口 受理第: 10 个顾客,beforeValue: 2 afterValue: 3
|
从上面的输出结果可以看到,每个工作窗口从第二次为客户办理业务时,都会读取到上一个客户的信息(beforeValue
),这就可能会影响后续的业务逻辑和造成内存泄漏等问题。
线程池的正确使用
在同时使用 ThreadLocal 与线程池时,每个线程执行完任务后,都必须在 try-finally
代码块内调用 ThreadLocal 的 remove()
方法,以此清理自定义的 ThreadLocal 变量,否则可能会影响后续的业务逻辑和造成内存泄漏等问题。
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| package com.java.interview.thread;
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors;
public class ThreadLocalAndThreadPoolDemo {
public static void main(String[] args) { MyData myData = new MyData(); ExecutorService threadPool = Executors.newFixedThreadPool(3);
try { for (int i = 1; i <= 10; i++) { final int index = i; threadPool.submit(() -> { try { Integer beforeValue = myData.threadLocal.get(); myData.add(); Integer afterValue = myData.threadLocal.get(); System.out.println( Thread.currentThread().getName() + "\t工作窗口\t" + "受理第: " + index + " 个顾客," + "beforeValue: " + beforeValue + "\t" + "afterValue: " + afterValue); } finally { myData.threadLocal.remove(); } }); } } catch (Exception e) { e.printStackTrace(); } finally { threadPool.shutdown(); } }
private static class MyData {
ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
public void add() { threadLocal.set(threadLocal.get() + 1); }
}
}
|
程序运行的输出结果:
1 2 3 4 5 6 7 8 9 10
| pool-1-thread-2 工作窗口 受理第: 2 个顾客,beforeValue: 0 afterValue: 1 pool-1-thread-1 工作窗口 受理第: 1 个顾客,beforeValue: 0 afterValue: 1 pool-1-thread-3 工作窗口 受理第: 3 个顾客,beforeValue: 0 afterValue: 1 pool-1-thread-1 工作窗口 受理第: 4 个顾客,beforeValue: 0 afterValue: 1 pool-1-thread-2 工作窗口 受理第: 5 个顾客,beforeValue: 0 afterValue: 1 pool-1-thread-1 工作窗口 受理第: 6 个顾客,beforeValue: 0 afterValue: 1 pool-1-thread-2 工作窗口 受理第: 7 个顾客,beforeValue: 0 afterValue: 1 pool-1-thread-1 工作窗口 受理第: 8 个顾客,beforeValue: 0 afterValue: 1 pool-1-thread-3 工作窗口 受理第: 9 个顾客,beforeValue: 0 afterValue: 1 pool-1-thread-2 工作窗口 受理第: 10 个顾客,beforeValue: 0 afterValue: 1
|
父子线程无法共享传递问题
ThreadLocal 的相关类
ThreadLocal
- 由 JDK 提供,不支持父子线程共享传递 ThreadLocal 变量。
- 也就是说,当在父线程中设置了一个
ThreadLocal
变量,然后启动一个子线程时,子线程会有自己独立的 ThreadLocalMap
,这意味着父线程中的 ThreadLocal
变量对子线程不可见。
InheritableThreadLocal
- 由 JDK 提供,属于 ThreadLocal 的扩展版本,允许子线程继承父线程的 ThreadLocal 变量。
- 在子线程继承后,子线程和父线程的 ThreadLocal 变量是互相独立的。换言之,如果在子线程启动之后,父线程修改了 ThreadLocal 变量的值,子线程中的变量值不会同步更新,反之亦然。当遇到线程池的线程复用时,会存在 ThreadLocal 变量无法传递的问题。
TransmittableThreadLocal
- 阿里巴巴开源的 ThreadLocal 扩展版本,解决了 InheritableThreadLocal 在使用线程池时(线程复用)变量不传递的问题,特别适用于分布式系统中的跨线程数据传递。
- 在使用 TransmittableThreadLocal 之前,需要引入以下 Maven 依赖 后才能够使用,可以解决线程池中上下文传递问题,推荐使用。
1 2 3 4 5
| <dependency> <groupId>com.alibaba</groupId> <artifactId>transmittable-thread-local</artifactId> <version>2.14.5</version> </dependency>
|
父子线程共享传递问题
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| @Slf4j public class ThreadLocalDemo {
public static void main(String[] args) { ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> null);
threadLocal.set(Thread.currentThread().getName() + "-Java"); log.info("major: {}", threadLocal.get()); System.out.println();
new Thread(() -> { log.info("major: {}", threadLocal.get()); threadLocal.set(Thread.currentThread().getName() + "-Vue"); log.info("major: {}", threadLocal.get()); }, "thread-1").start(); System.out.println();
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
new Thread(() -> { log.info("major: {}", threadLocal.get()); threadLocal.set(Thread.currentThread().getName() + "-Flink"); log.info("major: {}", threadLocal.get()); }, "thread-2").start(); System.out.println();
CompletableFuture.supplyAsync(() -> { log.info("major: {}", threadLocal.get()); threadLocal.set(Thread.currentThread().getName() + "-MySQL"); log.info("major: {}", threadLocal.get()); return null; }); System.out.println();
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } }
}
|
程序执行的输出结果:
1 2 3 4 5 6 7 8 9 10
| 15:46:10.890 [main] INFO com.java.interview.thread.ThreadLocalDemo - major: main-Java
15:46:10.898 [thread-1] INFO com.java.interview.thread.ThreadLocalDemo - major: null 15:46:10.900 [thread-1] INFO com.java.interview.thread.ThreadLocalDemo - major: thread-1-Vue
15:46:11.900 [thread-2] INFO com.java.interview.thread.ThreadLocalDemo - major: null 15:46:11.901 [thread-2] INFO com.java.interview.thread.ThreadLocalDemo - major: thread-2-Flink
15:46:11.911 [ForkJoinPool.commonPool-worker-115] INFO com.java.interview.thread.ThreadLocalDemo - major: null 15:46:11.912 [ForkJoinPool.commonPool-worker-115] INFO com.java.interview.thread.ThreadLocalDemo - major: ForkJoinPool.commonPool-worker-115-MySQL
|
从上面的输出结果可以看到,子线程无法访问父线程的 ThreadLocal 变量,也就是说 ThreadLocal 变量默认不支持在父子线程中传递。
父子线程共享传递解决
InheritableThreadLocal 使用
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 26 27 28
| @Slf4j public class ThreadLocalDemo {
public static void main(String[] args) { InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal();
inheritableThreadLocal.set(Thread.currentThread().getName() + "-Java"); log.info("major: {}", inheritableThreadLocal.get());
new Thread(() -> { log.info("major: {}", inheritableThreadLocal.get()); }, "thread1").start();
new Thread(() -> { log.info("major: {}", inheritableThreadLocal.get()); }, "thread2").start();
new Thread(() -> { log.info("major: {}", inheritableThreadLocal.get()); }, "thread3").start(); }
}
|
程序执行的输出结果:
1 2 3 4
| 15:59:36.338 [main] INFO com.java.interview.thread.ThreadLocalDemo - major: main-Java 15:59:36.345 [thread1] INFO com.java.interview.thread.ThreadLocalDemo - major: main-Java 15:59:36.346 [thread2] INFO com.java.interview.thread.ThreadLocalDemo - major: main-Java 15:59:36.347 [thread3] INFO com.java.interview.thread.ThreadLocalDemo - major: main-Java
|
从上面的输出结果可以看到,使用 InheritableThreadLocal 后,子线程可以继承父线程的 ThreadLocal 变量。
特别注意
当 InheritableThreadLocal 遇到线程池,会存在问题。在使用线程池时,InheritableThreadLocal 虽然可以让子线程继承父线程的 ThreadLocal 变量,但是一旦发生线程复用,修改父线程的 ThreadLocal 变量不会影响到子线程,反之亦然。
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| @Slf4j public class ThreadLocalDemo {
public static void main(String[] args) { InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal();
inheritableThreadLocal.set(Thread.currentThread().getName() + "-Java"); log.info("major: {}", inheritableThreadLocal.get());
ExecutorService threadPool = Executors.newFixedThreadPool(1); threadPool.execute(() -> { log.info("ThreadPool 第 1 次获取值,major: {}", inheritableThreadLocal.get()); });
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println();
inheritableThreadLocal.set(Thread.currentThread().getName() + "-Vue,已经修改了O(∩_∩)O"); log.info("major: {}", inheritableThreadLocal.get());
threadPool.execute(() -> { log.info("ThreadPool 第 2 次获取值,major: {}", inheritableThreadLocal.get()); });
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
threadPool.shutdown(); }
}
|
程序执行的输出结果:
1 2 3 4 5
| 16:19:13.993 [main] INFO com.java.interview.thread.ThreadLocalDemo - major: main-Java 16:19:14.001 [pool-1-thread-1] INFO com.java.interview.thread.ThreadLocalDemo - ThreadPool 第 1 次获取值,major: main-Java
16:19:15.002 [main] INFO com.java.interview.thread.ThreadLocalDemo - major: main-Vue,已经修改了O(∩_∩)O 16:19:15.003 [pool-1-thread-1] INFO com.java.interview.thread.ThreadLocalDemo - ThreadPool 第 2 次获取值,major: main-Java
|
从上面的输出结果可以看到,在使用线程池时,InheritableThreadLocal 虽然可以让子线程继承父线程的 ThreadLocal 变量,但是一旦发生线程复用,子线程将无法实时感知到父线程的 ThreadLocal 变量的变化。简而言之,当 InheritableThreadLocal 遇到线程池,如果执行任务的线程是新创建的,那么子线程可以实时访问到父线程的 ThreadLocal 变量;如果执行任务的线程是复用的,那么子线程无法实时感知到父线程的 ThreadLocal 变量的变化。为了解决这个问题,需要使用 TransmittableThreadLocal 类,它是阿里巴巴开源的一个用于解决线程池中上下文传递问题的工具类。
TransmittableThreadLocal 使用
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| @Slf4j public class ThreadLocalDemo {
public static void main(String[] args) { TransmittableThreadLocal<String> transmittableThreadLocal = new TransmittableThreadLocal<>();
transmittableThreadLocal.set(Thread.currentThread().getName() + "-Java"); log.info("major: {}", transmittableThreadLocal.get());
ExecutorService threadPool = Executors.newSingleThreadExecutor(); threadPool = TtlExecutors.getTtlExecutorService(threadPool);
threadPool.execute(() -> { log.info("ThreadPool 第 1 次获取值,major: {}", transmittableThreadLocal.get()); });
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println();
transmittableThreadLocal.set(Thread.currentThread().getName() + "-Vue,已经修改了O(∩_∩)O"); log.info("major: {}", transmittableThreadLocal.get());
threadPool.execute(() -> { log.info("ThreadPool 第 2 次获取值,major: {}", transmittableThreadLocal.get()); });
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
threadPool.shutdown(); }
}
|
程序执行的输出结果:
1 2 3 4 5
| 16:42:35.703 [main] INFO com.java.interview.thread.ThreadLocalDemo - major: main-Java 16:42:35.726 [pool-1-thread-1] INFO com.java.interview.thread.ThreadLocalDemo - ThreadPool 第 1 次获取值,major: main-Java
16:42:36.726 [main] INFO com.java.interview.thread.ThreadLocalDemo - major: main-Vue,已经修改了O(∩_∩)O 16:42:36.728 [pool-1-thread-1] INFO com.java.interview.thread.ThreadLocalDemo - ThreadPool 第 2 次获取值,major: main-Vue,已经修改了O(∩_∩)O
|
从上面的输出结果可以看到,在使用线程池时,TransmittableThreadLocal 可以让子线程访问到父线程的 ThreadLocal 变量,即使是在线程池中发生线程复用,子线程也可以实时感知到父线程的 ThreadLocal 变量的变化。
ThreadLocal 底层原理浅析
(1) ThreadLocal 是 Java 中用于实现线程本地存储的一种机制,它使得每个线程都能拥有一个独立于其他线程的变量副本。这样可以避免线程间的变量共享,从而保证线程安全。
(2) ThreadLocal 底层是通过 ThreadLocalMap 来实现的。每个线程对象(Thread)都有一个名为 threadLocals
的成员变量,这个变量的类型是 ThreadLocal.ThreadLocalMap
;而 ThreadLocalMap 是一个定制的 HashMap,它的值是线程要存储的变量,键是弱引用(WeakReference)的 ThreadLocal 对象,这样可以防止内存泄漏,当 ThreadLocal 对象没有强引用时,GC 可以回收这些对象。
(3) 如果在线程池中使用 ThreadLocal,那么可能会造成内存泄漏。因为当 ThreadLocal 对象使用完之后,应该要把设置的键值对,也就是 Entry 对象进行回收;但线程池中的线程是不会被回收的(线程复用),而且线程对象(Thread)是通过强引用指向 ThreadLocalMap 的,ThreadLocalMap 也是通过强引用指向 Entry 对象 的,Entry 对象也是通过强引用指向值对象的;所以线程不被回收,Entry 对象也就不会被回收,那么 Entry 对象的值也不会回收,从而导致内存泄漏。解决办法是,在使用完 ThreadLocal 对象之后,手动调用 ThreadLocal 的 remove()
方法,手动清理 Entry 对象。
(4) ThreadLocal 经典的使用场景就是连接管理,比如数据库连接管理。每个线程持有一个数据库连接,该数据库连接对象可以在不同的方法之间进行传递,但线程之间不共享同一个数据库连接。