为什么在 Java 中需要使用 ThreadLocal?

Sherwin.Wei Lv7

为什么在 Java 中需要使用 ThreadLocal?

回答重点

因为在多线程编程中,多个线程可能会同时访问和修改共享变量,导致线程安全问题。ThreadLocal 提供了一种简单的解决方案,使每个线程都有自己的独立变量副本,避免了多线程间的变量共享和竞争,从而解决了线程安全问题。

与通过加锁、同步块等传统方式来保证线程安全相比。ThreadLocal 不需要对变量访问进行同步,减少了上下文切换、锁竞争的性能损耗。

扩展知识

常见应用场景

  • 数据库连接管理:每个线程拥有自己的数据库连接,避免了多个线程共享同一个连接导致的线程安全问题。
  • 用户上下文管理:在处理用户请求时,每个线程拥有独立的用户上下文(如用户ID、Session信息),在并发环境中确保正确的用户数据。

ThreadLocal 的原理

ThreadLocal 通过为每个线程创建一个独立的变量副本来实现线程本地化存储。ThreadLocal 实际上是为每个线程创建了一个 ThreadLocalMap,而 ThreadLocalMap 是每个线程内部持有的结构。

ThreadLocalMap 的键是 ThreadLocal 对象,而值则是线程独立的变量副本。当线程访问 ThreadLocal.get() 时,它会根据当前线程在自己的 ThreadLocalMap 中找到对应的变量副本。

以下是一个简化的访问流程:

  • 线程A访问 ThreadLocal.get() 时,从 ThreadLocalMap 中找到与该 ThreadLocal 对象对应的值。
  • 线程B访问 ThreadLocal.get() 时,它有自己独立的 ThreadLocalMap,获取的是与其自身相关的值,互不干扰。

ThreadLocal 通俗理解

最近不是开放三胎政策嘛,假设你有三个孩子。

现在你带着三个孩子出去逛街,路过了玩具店,三个孩子都看中了一款变形金刚。

所以你买了一个变形金刚,打算让三个孩子轮着玩。

回到家你发现,孩子因为这个玩具吵架了,三个都争着要玩,谁也不让着谁。

这时候怎么办呢?你可以去拉架,去讲道理,说服孩子轮流玩,但这很累。

所以一个简单的办法就是出去再买两个变形金刚,这样三个孩子都有各自的变形金刚,世界就暂时得到了安宁。

映射到我们今天的主题,变形金刚就是共享变量,孩子就是程序运行的线程。有多个线程(孩子),争抢同一个共享变量(玩具),就会产生冲突,而程序的解决办法是加锁(父母说服,讲道理,轮流玩),但加锁就意味着性能的消耗(父母比较累)。

所以有一种解决办法就是避免共享(让每个孩子都各自拥有一个变形金刚),这样线程之间就不需要竞争共享变量(孩子之间就不会争抢)。

所以为什么需要 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
public class ThreadLocalExample {
// 定义一个 ThreadLocal,用来保存每个线程独立的变量副本
private static final ThreadLocal<Integer> threadLocalCounter = ThreadLocal.withInitial(() -> 0);

public static void main(String[] args) {
// 创建三个线程,每个线程都会有自己独立的变量副本
Thread t1 = new Thread(() -> {
incrementAndPrint();
});

Thread t2 = new Thread(() -> {
incrementAndPrint();
});

Thread t3 = new Thread(() -> {
incrementAndPrint();
});

// 启动线程
t1.start();
t2.start();
t3.start();
}

private static void incrementAndPrint() {
for (int i = 0; i < 5; i++) {
int currentValue = threadLocalCounter.get();
threadLocalCounter.set(currentValue + 1);
System.out.println(Thread.currentThread().getName() + " : " + threadLocalCounter.get());
}
}
}

在上面的例子中,三个线程各自操作 ThreadLocal 提供的变量副本,互不干扰,解决了多线程之间的共享数据竞争问题。

Comments