八股-多线程
多线程相关面试题
线程和进程的区别
进程是正在运行程序的实例,其中包含线程,各线程执行不同任务。
不同的进程拥有独立的内存空间,而在同一进程内的所有线程可以共享内存空间。
线程更为轻量,其上下文切换成本通常低于进程上下文切换(上下文切换指从一线程切换至另一线程)
并行和并发的区别
现在,多核 CPU 已成为主流。在多核 CPU 的架构下:
- 并发指的是同一时间应对多件事情的能力,多个线程轮流使用一个或多个 CPU 核心
- 并行则是指同一时间动手做多件事情的能力,例如,4 核 CPU 可同时执行 4 个线程
项目 | 并发(Concurrency) | 并行(Parallelism) |
---|---|---|
定义 | 同一时间段内管理多个任务,交替执行 | 同一时间点真正同时执行多个任务 |
时间维度 | 宏观同时,微观串行 | 微观和宏观都同时 |
依赖硬件 | 不一定依赖多核处理器 | 通常需要多核或多处理器 |
目标 | 提高资源利用率,提高程序响应性 | 提高执行速度,缩短执行时间 |
举例 | 单核 CPU 切换线程 | 多核 CPU 同时运行多个线程 |
单核 CPU
- 单核 CPU 下线程实际还是串行执行的
- 操作系统中有一个组件叫做任务调度器,将 CPU 的时间片(windows 下时间片最小约为 15 毫秒)分给不同的程序使用,只是由于 CPU 在线程间(时间片很短)的切换非常快,人类感觉是同时运行的
- 总结为一句话就是:微观串行,宏观并行
- 一般会将这种线程轮流使用 CPU 的做法称为并发(concurrent)
多核 CPU
每个核(core)均可调度运行线程,此时线程可并行执行
🧵 Java 中创建线程的四种方式
编号 | 方式 | 是否推荐 | 特点 |
---|---|---|---|
1 | 继承 Thread 类 | ❌ 不推荐 | 扩展性差,不能继承其他类 |
2 | 实现 Runnable 接口 | ✅ 推荐 | 更灵活,可配合线程池使用 |
3 | 实现 Callable 接口 +FutureTask | ✅ 推荐 | 有返回值,可处理异常 |
4 | 使用线程池 ExecutorService | ✅✅ 强烈推荐 | 高效可控,避免资源浪费 |
✅ 方式一:继承 Thread 类(不推荐)
class MyThread extends Thread {
@Override
public void run() {
System.out.println("线程运行中:" + Thread.currentThread().getName());
}
}
public class Main {
public static void main(String[] args) {
new MyThread().start(); // 启动线程
}
}
缺点:
- Java 单继承,继承了 Thread 就不能继承其他类了
- 不利于解耦,灵活性差
✅ 方式二:实现 Runnable 接口(推荐)
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("线程运行中:" + Thread.currentThread().getName());
}
}
public class Main {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable());
t.start();
}
}
优点:
- 避免了单继承限制
- 任务和线程解耦,适用于线程池等高级用法
✅ 方式三:实现 Callable 接口 +FutureTask(推荐)
import java.util.concurrent.*;
class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
return "线程执行完成,返回结果";
}
}
public class Main {
public static void main(String[] args) throws Exception {
Callable<String> callable = new MyCallable();
FutureTask<String> futureTask = new FutureTask<>(callable);
new Thread(futureTask).start();
// 获取返回值(阻塞)
String result = futureTask.get();
System.out.println(result);
}
}
优点:
- 可以获取线程执行结果
- 可以处理异常
- 是创建任务类线程(带返回值)最标准方式
✅ 方式四:使用线程池(强烈推荐)
import java.util.concurrent.*;
public class Main {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.execute(() -> {
System.out.println("使用线程池执行任务:" + Thread.currentThread().getName());
});
executor.shutdown(); // 关闭线程池
}
}
优点:
- 线程复用,性能高
- 可控制最大并发数,避免创建过多线程
- 支持任务调度、延迟执行等高级功能
🏁 总结对比
创建方式 | 是否有返回值 | 是否可重用 | 是否推荐 |
---|---|---|---|
Thread | ✖ | ✖ | ❌ |
Runnable | ✖ | ✔ | ✅ |
Callable + FutureTask | ✔ | ✔ | ✅ |
ExecutorService(线程池) | ✔ | ✔ | ✅✅ |
runable 和 callable 的区别
Runnable 和 Callable 是 Java 中用于封装任务的两个接口,它们都可以用于多线程编程,但在功能上有明显的区别
特性 | Runnable | Callable |
---|---|---|
定义位置 | java.lang.Runnable | java.util.concurrent.Callable |
方法名 | run() | call() |
是否有返回值 | ❌ 无 | ✅ 有 |
是否能抛出异常 | ❌ 不能抛出检查异常(只能捕获) | ✅ 可以抛出检查异常 |
使用方式 | Thread、ExecutorService.execute() | FutureTask、ExecutorService.submit() |
线程池兼容性 | ✔️ | ✔️(需配合 submit()) |
使用场景 | 适合的接口 |
---|---|
只需要执行任务,不需要结果 | Runnable |
需要返回结果 / 抛出异常 | Callable |
在线程池中执行,想拿结果 | Callable + Future |
⚠️ 注意事项
- Runnable 与 Callable 都不是线程,它们只是任务,线程是由 Thread 或 ExecutorService 启动的
- 如果你只用 Runnable,无法捕获任务内部的受检异常,也不能获得执行结果
- 如果你想让线程执行完返回一个值,建议用 Callable + FutureTask 或 ExecutorService.submit()
start 和 run 的区别
start()
启动一个新的线程,并自动调用 run()
run()
是线程执行的具体任务,但直接调用仅是普通方法调用,不会启动新线程
调用 run()
不会触发多线程,而是由当前线程顺序执行
start()
只能启动一次
线程的状态
状态:
- 新建(NEW)
- 可运行(RUNNABLE)
- 阻塞(BLOCKED)
- 等待(WAITING)
- 时间等待(TIMED_WAITING)
- 终止(TERMINATED)
变化:
创建线程对象是新建状态
调用了 start()方法转变为可执行状态
线程获取到了 CPU 的执行权,执行结束是终止状态
在可执行状态的过程中,如果没有获取 CPU 的执行权,可能会切换其他状态
- 如果没有获取锁(synchronized 或 lock)进入阻塞状态,获得锁再切换为可执行状态
- 如果线程调用了 wait()方法进入等待状态,其他线程调用 notify()唤醒后可切换为可执行状态
- 如果线程调用了 sleep(50)方法,进入计时等待状态,到时间后可切换为可执行状态
public enum State {
/**
* 线程尚未启动的状态。
*/
NEW,
/**
* 可运行线程的状态。处于可运行状态的线程正在Java虚拟机中执行,但可能正在等待来自操作系统的其他资源,
* 例如处理器。
*/
RUNNABLE,
/**
* 等待监视器锁的线程状态。
* 处于阻塞状态的线程正在等待监视器锁以进入同步块/方法或在调用
* {@link Object#wait() Object.wait}后重新进入同步块/方法。
*/
BLOCKED,
/**
* 等待线程的状态。
* 线程由于调用以下方法之一而处于等待状态:
* <ul>
* <li>{@link Object#wait() Object.wait} 无超时</li>
* <li>{@link #join() Thread.join} 无超时</li>
* <li>{@link LockSupport#park() LockSupport.park}</li>
* </ul>
*
* <p>处于等待状态的线程正在等待另一个线程执行特定操作。
*
* 例如,调用了 {@code Object.wait()} 的线程正在等待另一个线程调用
* {@code Object.notify()} 或 {@code Object.notifyAll()}。
* 调用了 {@code Thread.join()} 的线程正在等待指定线程终止。
*/
WAITING,
/**
* 具有指定等待时间的等待线程状态。
* 线程由于调用以下方法之一并指定了正等待时间而处于定时等待状态:
* <ul>
* <li>{@link #sleep Thread.sleep}</li>
* <li>{@link Object#wait(long) Object.wait} 带超时</li>
* <li>{@link #join(long) Thread.join} 带超时</li>
* <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
* <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
* </ul>
*/
TIMED_WAITING,
/**
* 已终止线程的状态。
* 线程已完成执行。
*/
TERMINATED;
}

如何保证线程按顺序执行
在 Java 中,如果你想让线程 T1 → T2 → T3 按顺序执行(即:T1 执行完了才能执行 T2,T2 执行完了才能执行 T3),有多种实现方式。下面我会从简单到进阶讲解 5 种常见方法
✅ 方式一:使用 Thread.join()(最常用、简单)
public class ThreadOrderJoin {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> System.out.println("T1"));
Thread t2 = new Thread(() -> System.out.println("T2"));
Thread t3 = new Thread(() -> System.out.println("T3"));
t1.start();
t1.join(); // 等 T1 执行完再执行 T2
t2.start();
t2.join(); // 等 T2 执行完再执行 T3
t3.start();
}
}
🟢 输出保证顺序:T1 → T2 → T3
✅ 方式二:使用 ExecutorService 串行线程池(单线程)
import java.util.concurrent.*;
public class ThreadOrderExecutor {
public static void main(String[] args) {
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(() -> System.out.println("T1"));
executor.submit(() -> System.out.println("T2"));
executor.submit(() -> System.out.println("T3"));
executor.shutdown();
}
}
🟢 优雅高效,线程按顺序执行,不需要手动控制
✅ 方式三:使用 CountDownLatch(适合多个线程控制依赖)
import java.util.concurrent.CountDownLatch;
public class ThreadOrderLatch {
public static void main(String[] args) {
CountDownLatch latch1 = new CountDownLatch(1); // 控制 T2
CountDownLatch latch2 = new CountDownLatch(1); // 控制 T3
Thread t1 = new Thread(() -> {
System.out.println("T1");
latch1.countDown();
});
Thread t2 = new Thread(() -> {
try {
latch1.await(); // 等 T1 执行完
System.out.println("T2");
latch2.countDown();
} catch (InterruptedException e) {}
});
Thread t3 = new Thread(() -> {
try {
latch2.await(); // 等 T2 执行完
System.out.println("T3");
} catch (InterruptedException e) {}
});
t3.start();
t2.start();
t1.start();
}
}
🟢 输出顺序保证,但线程启动顺序不影响实际执行顺序
✅ 方式四:使用 wait()/notify()(略复杂,控制更细粒度)
这种方式需要加锁,控制线程之间通信,不建议初学者使用,略过也没关系,除非你在面试中被要求
✅ 方式五:使用 Lock+Condition(进阶版)
使用 ReentrantLock 和 Condition 可以更精准地控制线程顺序,但代码更复杂,适合生产级控制流程。一般只在需要循环执行线程顺序时使用
🏁 总结推荐
方法 | 难度 | 线程顺序保障 | 推荐场景 |
---|---|---|---|
join() | ⭐ 简单 | ✅ | 少量线程顺序控制 |
串行线程池 | ⭐ 简单 | ✅ | 提交任务有顺序要求 |
CountDownLatch | ⭐⭐ 中等 | ✅ | 控制多个线程依赖关系 |
wait/notify | ⭐⭐⭐ | ✅ | 高并发自定义控制 |
Lock/Condition | ⭐⭐⭐ | ✅ | 多线程顺序循环控制 |
notify() 和 notifyAll() 有什么区别
notifyAll:
唤醒所有处于 wait
状态的线程
notify:
仅随机唤醒一个处于 wait
状态的线程
wait 和 sleep 的区别
🧠 面试回答模板(简洁版)
wait() 是 Object 的方法,用于线程间通信,会释放锁;必须在 synchronized 代码块中使用
sleep() 是 Thread 的方法,用于线程休眠,不会释放锁,不需要同步块配合
特性 | wait() | sleep() |
---|---|---|
所在类 | Object 类 | Thread 类 |
是否是静态方法 | ❌ 不是 | ✅ 是(Thread.sleep()) |
是否需要加锁(synchronized) | ✅ 必须在同步块/方法中使用 | ❌ 不需要 |
是否释放锁 | ✅ 会释放锁 | ❌ 不会释放锁 |
是否抛异常 | ✅InterruptedException | ✅InterruptedException |
典型用途 | 线程间通信/等待唤醒机制 | 暂停线程(休眠)一段时间 |
唤醒方式 | 需配合 notify()/notifyAll() | 到时间自动恢复 |
是否常用于协作 | ✅ 是 | ❌ 否 |
如何停止一个正在运行的线程
使用退出标志,使线程正常退出,也就是当 run 方法完成后线程终止
使用 stop 方法强行终止(不推荐,方法已作废)
使用 interrupt 方法中断线程
- 打断阻塞的线程(sleep,wait,join)的线程,线程会抛出 InterruptedException 异常
- 打断正常的线程,可以根据打断状态来标记是否退出线程
停止方式 | 是否推荐 | 是否安全 | 是否优雅退出 | 是否可控 |
---|---|---|---|---|
标志位控制 | ✅✅ | ✅ | ✅ | ✅ |
interrupt 中断 | ✅ | ✅ | ✅ | ✅ |
stop() 强制终止 | ❌ | ❌ | ❌ | ❌ |
class MyThread extends Thread {
private volatile boolean running = true;
public void stopThread() {
running = false;
}
@Override
public void run() {
while (running) {
System.out.println("线程运行中...");
}
System.out.println("线程已安全退出");
}
}
MyThread t = new MyThread();
t.start();
Thread.sleep(1000);
t.stopThread(); // 通知线程停止
class MyThread extends Thread {
@Override
public void run() {
try {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("线程执行中...");
Thread.sleep(1000); // 休眠中可被中断
}
} catch (InterruptedException e) {
System.out.println("线程被中断了");
}
System.out.println("线程退出");
}
}
MyThread t = new MyThread();
t.start();
Thread.sleep(3000);
t.interrupt(); // 中断线程
Thread t = new Thread(() -> {
while (true) {
System.out.println("线程运行中...");
}
});
t.start();
Thread.sleep(1000);
t.stop(); // ⚠️ 强制停止线程(已弃用)
(TODO review JVM)synchronized 关键字的底层原理
Monitor
Monitor 被翻译为监视器,是由 jvm 提供,c++ 语言实现

Owner:存储当前获取锁的线程,仅能有一个线程可以获取
EntryList:关联未抢到锁的线程,处于阻塞(Blocked) 状态的线程
WaitSet:关联调用了 wait
方法的线程,处于等待(Waiting) 状态的线程
monitor 是重量级锁



轻量级锁
在众多情境下,java 程序运行时,同步块内的代码通常不存在竞争现象,各线程交替执行同步块内的代码。在这种情况下,使用重量级锁显得没有必要。因此,JVM 引入了轻量级锁的概念
JMM Java 内存模型
JMM(Java Memory Model,Java 内存模型)定义了共享内存中多线程程序读写操作的行为规范,通过这些规则来规范对内存的读写操作,从而保证指令的正确性
MM 将内存分为两部分:一部分是私有线程的工作区域(工作内存),另一部分是所有线程共享的区域(主内存)
线程间相互隔离,线程间的交互需要通过主内存来实现
CAS

CAS 底层依赖于一个 Unsafe
类来直接调用操作系统底层的 CAS 指令
ReentrantLock
中的一段 CAS 代码:
protected final boolean compareAndSetState(int expect, int update) {
return STATE.compareAndSet(this, expect, update);
}
-
this
:当前值 -
expect
:期望的值 -
update
:更新后的值
乐观锁、悲观锁
CAS(Compare-And-Swap)基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗
synchronized
基于悲观锁的思想:最悲观的估计,得防着其他线程来修改共享变量,我上了锁,你们都别想改;我改完了解开锁,你们才有机会
Volatile
作用:
- 保证线程之间的可见性
- 防止指令重排序
保证可见性
方案一:在程序运行时加入 -Xint
参数,表示禁用即时编译器。此方法不推荐,因得不偿失(其他程序亦需使用)
方案二:在修饰 stop
变量时使用 volatile
关键字,以此向 JIT(即时编译器)指示,不要对被 volatile
修饰的变量进行优化
禁止指令重排序
使用 volatile
关键字修饰共享变量,会在读写该共享变量时引入不同的屏障,从而阻止其他读写操作跨越这些屏障,实现阻止指令重排序的效果
Volatile 使用技巧:
将 volatile
修饰的变量置于代码的最后位置进行写操作。
将 volatile
修饰的变量置于代码的最开始位置进行读操作
AQS
全称是 AbstractQueuedSynchronizer,即抽象队列同步器。它是构建锁或其他同步组件的基础框架
AQS 和 Synchronized 的区别
特性 | synchronized | AQS |
---|---|---|
实现方式 | 关键字,c++ 语言实现 | java 语言实现 |
锁类型 | 悲观锁,自动释放锁 | 悲观锁,手动开启和关闭 |
锁竞争激烈时 | 都是重量级锁,性能差 | 提供了多种解决方案 |
AQS 常见实现类:
ReentrantLock 阻塞式锁
Semaphore 信号量
CountDownLatch 倒计时锁
AQS 基本工作机制
多个线程抢锁为了保证原子性使用的 CAS 判断
里面维护了一个双向链表实现的 FIFO 队列,锁释放后唤醒 head

可以实现公平锁也可以实现非公平锁
新的线程与队列中的线程共同争夺资源,此为非公平锁
新的线程加入队列等待,仅允许队列中的头部(head)线程获取锁,这属于公平锁
ReentrantLock 的实现原理
ReentrantLock 表示一种支持重新进入的锁。调用 lock 方法获取锁之后,再次调用 lock,不会再阻塞
ReentrantLock 主要利用 CAS(Compare-And-Swap)与 AQS(AbstractQueuedSynchronizer)队列来实现
该锁支持公平锁和非公平锁功能。在提供的构造器中,无参默认设置为非公平锁,也可以通过传参来设置为公平锁
ReentrantLock,翻译过来是可重入锁,相较于 synchronized,具备以下特点:
- 可中断
- 可设置超时时间
- 可设置公平锁
- 支持多个条件变量
- 与 synchronized 一样,均支持重入
ReentrantLock 主要利用 CAS(Compare-And-Swap)与 AQS(AbstractQueuedSynchronizer)队列来实现。它支持公平锁与非公平锁,两者的实现方式类似
构造方法接受一个可选的公平参数(默认为非公平锁)。当设置为 true
时,表示为公平锁;否则为非公平锁。通常情况下,公平锁的效率往往不如非公平锁,尤其在多线程访问时,公平锁会表现出较低的吞吐量
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
abstract static class Sync extends AbstractQueuedSynchronizer {
}

线程在抢夺锁之后,采用 CAS(Compare-And-Swap)技术来修改 state
状态。若修改状态成功,则将 exclusiveOwnerThread
属性指向当前线程,从而实现锁的获取
若修改状态失败,线程将进入双向队列中等待。其中,head
指向队列头部,tail
指向队列尾部
当 exclusiveOwnerThread
属性为 null
时,系统将唤醒在双向队列中等待的线程
在公平锁的实现中,锁的获取遵循先到先得的原则;而非公平锁则允许不在队列中的线程也能抢夺锁
synchronized 和 lock 的区别
语法层面
synchronized
是关键字,其在 JVM 中的源码是用 C++ 语言实现的
Lock
是接口,其源码由 JDK 提供,并使用 Java 语言实现
使用 synchronized
时,退出同步代码块锁会自动释放;而使用 Lock
时,则需要手动调用 unlock
方法释放锁
功能层面
二者均属于悲观锁,且都具备基本的互斥、同步、锁重入功能Lock
提供了 synchronized
所不具备的多项功能,例如公平锁、可中断、可超时、多条件变量等
Lock
拥有适用于不同场景的实现,如 ReentrantLock
、ReentrantReadWriteLock
(读写锁)等
性能层面
在没有竞争的情况下,synchronized 做了许多优化,例如偏向锁、轻量级锁,性能表现尚可
在竞争激烈的环境中,Lock 的实现通常会提供更优的性能
死锁产生的条件
死锁的产生通常需要同时满足以下四个必要条件(即“死锁的四个必要条件”):
- 互斥条件(Mutual Exclusion)
至少有一个资源处于被占用状态,即某个资源一次只能被一个进程占用。如果其他进程请求该资源,只能等待 - 占有且等待(Hold and Wait)
一个进程至少已经占有了一个资源,同时又提出新的资源请求,而该资源已被其他进程占用,此时该进程会阻塞,但又不释放它已占有的资源 - 不剥夺条件(No Preemption)
进程已获得的资源在未使用完之前,不能被系统强行剥夺,只能在进程自己使用完后主动释放 - 循环等待条件(Circular Wait)
存在一种进程资源的循环等待关系,例如:
进程 P1 等待 P2 占有的资源,
P2 等待 P3 占有的资源,
……
最后 Pn 又等待 P1 占有的资源,形成一个环路
只要上述四个条件同时成立,就可能产生死锁
预防死锁的方法,就是设法破坏其中至少一个条件。例如:
- 破坏“占有且等待”:要求进程在请求资源前必须释放已占有的资源
- 破坏“循环等待”:对资源进行统一编号,进程申请资源必须按照升序编号进行等
如何进行死锁诊断
当程序出现死锁现象时,我们可以利用 JDK 自带的工具:jps
和 jstack
-
jps
:输出虚拟机(VM)中运行的进程状态信息 -
jstack
:查看 Java 进程内线程的堆栈信息
在 Java 中,死锁诊断主要可以通过以下几种方式进行:
使用线程转储(Thread Dump)分析
最常见、最直接的方法
1. 使用 jstack 命令
jstack <Java进程的PID>
- 可以查看所有线程的栈信息
- 若存在死锁,jstack 会自动在输出中提示,如:
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x0000000012345678 (object A), which is held by "Thread-2"
"Thread-2":
waiting to lock monitor 0x0000000098765432 (object B), which is held by "Thread-1"
2. 如何获取进程 PID?
jps
或者:
ps -ef | grep java
程序内主动检测(编程方式)
使用 java.lang.management 包内的 线程管理 API:
示例代码(检测死锁线程):
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
public class DeadlockDetector {
public static void main(String[] args) {
ThreadMXBean mbean = ManagementFactory.getThreadMXBean();
long[] deadlockedThreadIds = mbean.findDeadlockedThreads(); // Java 5+
if (deadlockedThreadIds != null) {
ThreadInfo[] infos = mbean.getThreadInfo(deadlockedThreadIds);
System.out.println("发现死锁线程:");
for (ThreadInfo info : infos) {
System.out.println(info.getThreadName());
System.out.println(info);
}
} else {
System.out.println("未发现死锁。");
}
}
}
你可以将此逻辑做成一个定时任务,在生产环境中定期检测
使用可视化工具诊断
1. VisualVM
- 免费,Java 自带
- 可以实时查看线程状态
- 发现死锁时,会在 “Threads” 面板中标红
2. JConsole
- JDK 自带
- 功能与 VisualVM 类似,较轻量
3. 其他高级工具
- YourKit、JProfiler、Arthas(热修诊断利器)
ConcurrentHashMap
底层数据结构:
- JDK1.7 底层采用分段的数组与链表实现
- JDK1.8 采用的数据结构与 HashMap 1.8 的结构相同,即数组 + 链表/红黑二叉树
加锁的方式:
- JDK1.7 采用 Segment 分段锁,底层使用 ReentrantLock
- JDK1.8 采用 CAS(Compare-And-Swap)操作添加新节点,并使用 synchronized 锁定链表或红黑二叉树的首节点。与 Segment 分段锁相比,这种锁定方式粒度更细,性能更优
线程安全的 HashMap
底层数据结构:
- JDK1.7 底层采用分段的数组加链表实现
- JDK1.8 采用的数据结构与 HashMap 1.8 的结构相同,即数组加链表/红黑二叉树

在 JDK1.8 中,摒弃了 Segment 臃肿的设计,其数据结构与 HashMap 相同:数组 + 红黑树 + 链表。采用 CAS(Compare-And-Swap)与 Synchronized 机制来确保并发安全并实现
- CAS 控制数组节点添加
-
synchronized
仅锁定当前链表或红黑二叉树的首节点,只要哈希不冲突,便不会引发并发问题,效率得到提升
导致并发程序出现问题的根本原因(Java 程序中怎么保证多线程的执行安全)
Java 并发程序三大特征
原子性
定义: 一个线程在 CPU 中操作不可暂停,也不可中断,要么执行完成,要么不执行
解决:
-
synchronized
:同步加锁 - JUC(Java Util Concurrency)里面的
lock
:加锁
int ticketNum = 10;
public synchronized void getTicket(){
if(ticketNum <= 0){
return ;
}
System.out.println(Thread.currentThread().getName()+"抢到一张票,剩余:"+ticketNum);
// 非原子性操作
ticketNum--;
}
public static void main(String[] args) {
TicketDemo demo = new TicketDemo();
for(int i=0;i<20;i++){
new Thread(demo::getTicket).start();
}
}
内存可见性
定义: 让一个线程对共享变量的修改对另一个线程可见
解决:
- synchronized
- volatile(常用)
- lock
public class VolatileDemo {
private static boolean flag = false;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (!flag) {
}
System.out.println("第一个线程执行完毕...");
}).start();
Thread.sleep(100);
new Thread(() -> {
flag = true;
System.out.println("第二线程执行完毕...");
}).start();
}
}
有序性
前提:指令重排
处理器为了提升程序运行效率,可能会对输入代码进行优化。它不保证程序中各个语句的执行先后顺序与代码中的顺序一致,但会确保程序最终执行结果与代码顺序执行的结果保持一致
解决:volatile
线程池的核心参数
在 Java 中使用线程池(如 ThreadPoolExecutor)时,其构造函数包含 7 个核心参数
📌 1. corePoolSize (核心线程数)
- 线程池中始终保留的线程数,即使它们处于空闲状态
- 当有新任务时,如果当前线程数少于 corePoolSize,即使有空闲线程,也会新建线程处理
📌 2. maximumPoolSize (最大线程数)
- 线程池中允许创建的最大线程数
- 当任务超过核心线程数,且任务队列满了,就会尝试新建线程直到 maximumPoolSize
📌 3. keepAliveTime (线程空闲存活时间)
- 非核心线程在空闲状态下最多等待任务的时间
- 超过这个时间还没有新任务,这些线程将被终止
📌 4. TimeUnit (时间单位)
- keepAliveTime 的时间单位
- 可选值:TimeUnit.SECONDS、MILLISECONDS、MINUTES 等
📌 5. BlockingQueue<Runnable> (任务队列)
存放等待执行任务的阻塞队列
常见实现:
- ArrayBlockingQueue:有界队列
- LinkedBlockingQueue:可选择容量或默认无限大
- SynchronousQueue:不存储任务,直接提交给线程
- PriorityBlockingQueue:带优先级的队列
📌 6. ThreadFactory (线程工厂)
- 自定义线程的创建方式
- 常用于设置线程名、是否为守护线程等
- 如果不指定,默认使用 Executors.defaultThreadFactory()
📌 7. RejectedExecutionHandler (拒绝策略)
当任务队列满且线程数达到最大值时,如何处理新提交的任务
常见实现:
- AbortPolicy(默认):抛出异常
- CallerRunsPolicy:由提交任务的线程执行
- DiscardPolicy:直接丢弃任务
- DiscardOldestPolicy:丢弃队列中最早的任务,尝试再次提交
示例构造方法
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // corePoolSize
5, // maximumPoolSize
60, // keepAliveTime
TimeUnit.SECONDS, // timeUnit
new ArrayBlockingQueue<>(100), // workQueue
Executors.defaultThreadFactory(), // threadFactory
new ThreadPoolExecutor.AbortPolicy() // handler
);

线程池中常见的阻塞队列
在线程池(如 ThreadPoolExecutor)中,任务队列的类型会极大影响线程池的行为和性能。Java 中常见的 阻塞队列(BlockingQueue) 类型有以下几种,每种都有不同的应用场景:
🔸 1. ArrayBlockingQueue (数组阻塞队列)
✅ 特点:
- 有界队列(必须指定容量)
- 使用数组结构实现,按 FIFO(先进先出)排序
- 线程安全,适合生产者-消费者模型
📌 使用示例:
BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(100);
🚀 适用场景:
- 任务量可控,系统不允许无限提交任务
- 稳定、容易预测资源使用
🔸 2. LinkedBlockingQueue (链表阻塞队列)
✅ 特点:
- 可指定或不指定容量(默认容量为 Integer.MAX_VALUE)
- 使用链表结构实现,FIFO
- 可以实现更高的吞吐量(与 ArrayBlockingQueue 相比)
📌 使用示例:
BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(); // 默认容量非常大
⚠️ 注意:
- 如果不指定容量,可能会导致任务堆积,容易引发 OOM(内存溢出)
LinkedBlockingQueue vs ArrayBlockingQueue
特性 | LinkedBlockingQueue | ArrayBlockingQueue |
---|---|---|
默认容量 | 默认无界,支持有界 | 强制有界 |
底层数据结构 | 底层是链表 | 底层是数组 |
节点添加方式 | 是懒惰的,创建节点的时候添加数据 | 提前初始化 Node 数组 |
入队操作 | 入队会生成新 Node | Node 需要是提前创建好的 |
锁的使用 | 两把锁(头尾) | 一把锁 |

🔸 3. SynchronousQueue (同步移交队列)
✅ 特点:
- 容量为 0 的队列,不保存任务
- 每个 put 操作都必须等待一个 take 操作(任务必须直接交给线程执行)
- 高并发时性能极好
📌 使用示例:
BlockingQueue<Runnable> queue = new SynchronousQueue<>();
🚀 适用场景:
- 提交任务速度快,希望直接交给线程执行(如缓存线程池 Executors.newCachedThreadPool() 就使用它)
- 系统对延迟敏感,不想排队
🔸 4. PriorityBlockingQueue (优先级阻塞队列)
✅ 特点:
- 按任务优先级排序(非 FIFO)
- 需要任务实现 Comparable 接口或使用 Comparator 自定义排序规则
- 无界队列
📌 使用示例:
BlockingQueue<Runnable> queue = new PriorityBlockingQueue<>();
🚀 适用场景:
- 某些任务比其他任务更紧急
- 如:事件调度、优先处理高价值请求
🔸 5. DelayQueue (延迟队列)🚫(不常用于线程池)
✅ 特点:
- 基于优先级队列,只有到达指定延时时间的任务才会出队
- 任务需实现 Delayed 接口
📌 适用场景:
- 定时任务处理,如:延迟消息、订单超时处理
- 不适用于 ThreadPoolExecutor,但适用于定时调度场景
总结对比表:
队列类型 | 有界/无界 | 顺序 | 是否保存任务 | 应用场景 |
---|---|---|---|---|
ArrayBlockingQueue | 有界 | FIFO | 是 | 控制任务堆积,资源受限系统 |
LinkedBlockingQueue | 默认无界 | FIFO | 是 | 高吞吐,适用于任务量不易估计的系统 |
SynchronousQueue | 无缓冲 | N/A | 否 | 直接提交任务,适合高并发短任务 |
PriorityBlockingQueue | 默认无界 | 按优先级 | 是 | 优先级任务处理 |
DelayQueue | 默认无界 | 按时间 | 是 | 延迟执行任务 |
如何确定核心线程数
确定线程池的 核心线程数(corePoolSize) 是性能优化的关键。这个数值的选择应依据你的 任务类型(CPU 密集 or IO 密集) 以及系统资源情况来设定
① 高并发、任务执行时间短 →(CPU 核数 +1),减少线程上下文切换
② 并发不高、任务执行时间长
- IO 密集型任务 →(CPU 核数*2+1)
- 计算密集型任务 →(CPU 核数 +1)
③ 并发高、业务执行时间长,解决此类任务的关键不在于线程池,而在于整体架构设计。首先,检查业务中某些数据是否可以缓存是第一步;其次,增加服务器是第二步。至于线程池的设置,可参考(2)
✅ 一、根据任务类型来定
🔹 1. CPU 密集型任务
📌 特点:
- 任务主要消耗 CPU,如大规模计算、图像处理、加密解密等
- 线程越多反而增加上下文切换开销
✅ 推荐公式:
核心线程数 = CPU 核心数 + 1
💡 示例:
int corePoolSize = Runtime.getRuntime().availableProcessors() + 1;
🔹 2. IO 密集型任务
📌 特点:
- 任务中大量时间用于等待,如网络请求、磁盘读写、数据库访问
- CPU 并不总是忙,可以多开一些线程来“覆盖等待时间”
✅ 推荐公式:
核心线程数 = CPU 核心数 × 2(或更高)
更精细的估算(理想化模型):
核心线程数 = CPU 核心数 ×(1 + 平均等待时间 / 平均计算时间)
🔹 3. 混合型任务
📌 特点:
- 同时存在 CPU 和 IO 操作
- 推荐将任务拆分为不同线程池处理
- 或者以 IO 密集的估算方式为准(倾向于资源可伸缩)
✅ 二、考虑其他影响因素
因素 | 说明 |
---|---|
系统的内存资源 | 核心线程数过多可能造成内存压力甚至 OOM |
响应时间要求 | 实时性高可以适当提升线程数 |
并发任务数量 | 如果短时间内任务并发量很大,考虑适当加线程数 |
服务类型 | Web 应用通常 IO 密集,适合开多线程;计算服务适合限制线程数 |
✅ 三、线程池创建实用模板
// 查看 CPU 核心数
int cpuCore = Runtime.getRuntime().availableProcessors();
int corePoolSize = cpuCore * 2;
int maxPoolSize = corePoolSize * 2;
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize,
maxPoolSize,
60L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
✅ 四、监控和调优
- 使用 JMX、VisualVM、Arthas、Prometheus 等工具监控线程池运行情况
- 实时观察活跃线程数、等待队列长度、任务执行时间
- 根据业务波动定期调整核心线程数是非常推荐的实践
线程池的种类
Java 中的线程池主要通过 Executors 工具类进行创建,常见的线程池类型有 5 种,分别适用于不同的业务场景
✅ 1. newFixedThreadPool —— 固定线程数线程池
📌 特点:
- 创建一个固定数量的线程池
- 多余的任务会在 队列中等待
- 所有线程处于空闲也不会被回收
🔧 示例:
ExecutorService executor = Executors.newFixedThreadPool(5);
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
🚀 适用场景:
- 稳定处理量,如:日志处理、数据库连接、后台任务等
- 并发数量可预估
✅ 2. newCachedThreadPool —— 可缓存线程池
📌 特点:
- 线程数不固定,可根据需要创建新线程
- 线程空闲 60 秒 后自动销毁
- 使用 SynchronousQueue(不存储任务,直接交由线程处理)
🔧 示例:
ExecutorService executor = Executors.newCachedThreadPool();
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
🚀 适用场景:
- 短期大量并发任务
- 对响应速度要求高,任务执行时间短
⚠️ 注意:
- 无界线程池,若任务无限增长可能导致 OOM
✅ 3. newSingleThreadExecutor —— 单线程线程池
📌 特点:
- 单个线程串行执行任务
- 保证任务顺序执行,不会并发
- 若线程异常退出,会自动创建新线程保持单线程执行
🔧 示例:
ExecutorService executor = Executors.newSingleThreadExecutor();
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
🚀 适用场景:
- 串行任务处理,如:顺序日志写入、事务执行、定时持久化等
- 多线程有并发风险时采用
✅ 4. newScheduledThreadPool —— 定时线程池
📌 特点:
- 支持定时执行任务或周期性任务
- 使用 DelayQueue 作为任务队列
🔧 示例:
ScheduledExecutorService executor = Executors.newScheduledThreadPool(4);
executor.scheduleAtFixedRate(task, 1, 3, TimeUnit.SECONDS);
public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
return new DelegatedScheduledExecutorService
(new ScheduledThreadPoolExecutor(1));
}
🚀 适用场景:
- 定时任务调度,如:心跳检测、定时清理、周期性同步任务等
✅ 5. newWorkStealingPool (Java 8+)—— 工作窃取线程池
📌 特点:
- 使用 ForkJoinPool 实现
- 每个线程有自己的任务队列,空闲线程会从其他队列窃取任务
- 自动利用多核 CPU 的优势
🔧 示例:
ExecutorService executor = Executors.newWorkStealingPool();
public static ExecutorService newWorkStealingPool(int parallelism) {
return new ForkJoinPool
(parallelism,
ForkJoinPool.defaultForkJoinWorkerThreadFactory,
null, true);
}
🚀 适用场景:
- 多核 CPU、高并发、高吞吐、异步任务分解
- 任务粒度不均匀、性能敏感系统
⚠️ 注意事项
Java 官方不推荐直接使用 Executors 创建线程池,因为:
- 默认使用的队列有可能是无界的,存在 OOM 风险
- 无法直接设置核心参数如最大线程数、拒绝策略等
✅ 建议使用方式:
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(capacity),
new ThreadPoolExecutor.AbortPolicy()
);
线程池使用场景
CountDownLatch
CountDownLatch
是 Java 并发包 java.util.concurrent
中的一种常用同步工具类,用于控制一个或多个线程等待其他线程完成某些操作
CountDownLatch
是一个倒计时的锁,它允许一个或多个线程在等待其他线程完成任务后,再继续执行
方法 | 说明 |
---|---|
await() | 当前线程阻塞,直到计数器为 0 才继续执行 |
countDown() | 计数器减 1,表示一个任务已完成 |
await(long timeout, TimeUnit) | 带超时等待,防止永久阻塞 |
import java.util.concurrent.CountDownLatch;
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
int workerCount = 3;
CountDownLatch latch = new CountDownLatch(workerCount);
for (int i = 1; i <= workerCount; i++) {
final int workerId = i;
new Thread(() -> {
try {
System.out.println("子线程 " + workerId + " 开始工作...");
Thread.sleep((long)(Math.random() * 2000)); // 模拟工作时间
System.out.println("子线程 " + workerId + " 完成工作!");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
latch.countDown(); // 完成后减一
}
}).start();
}
System.out.println("主线程等待子线程完成...");
latch.await(); // 等待所有子线程完成
System.out.println("所有子线程已完成,主线程继续执行!");
}
}
场景 | 描述 |
---|---|
主线程等待多个子任务完成 | 如:多个模块加载完成后统一初始化界面 |
多线程并行执行任务 | 多个线程并行处理数据,主线程汇总 |
模拟并发压力测试 | 控制多个线程同时开始请求,测试并发能力 |
服务启动协调 | 启动多个服务组件,全部启动完后再提供服务 |
⚠️ 注意事项
CountDownLatch 一次性使用,计数器归零后不可重置
如果需要重复使用,建议使用 CyclicBarrier 或 Semaphore
多线程中使用时注意线程安全和异常处理(确保 countDown() 能被执行)

场景一(ES 数据批量导入)

场景二(数据汇总)
在电商网站中,用户下单后,需查询数据。数据涵盖三部分:订单信息、所购商品、物流信息。这三类信息分别在各自的微服务中实现。那么,我们应如何完成这一业务呢?

在实际开发过程中,难免需要调用多个接口汇总数据。若所有接口(或部分接口)间无依赖关系,即可利用线程池与Future机制来提升性能
场景三(异步调用)
保存搜索历史记录不能影响用户搜索

用@Async
注解
如何控制某个方法允许并发访问线程的数量
在多线程编程中,Semaphore
(信号量)工具类提供了对并发访问的控制
- 创建
Semaphore
对象时,可以为其指定一个容量 - 使用
acquire()
方法请求一个信号量,此时信号量的个数减1 - 通过调用
release()
方法释放一个信号量,此时信号量的个数加1
Semaphore semaphore = new Semaphore(int permits);
permits:允许同时访问的线程数量
默认是非公平模式(线程获取许可顺序不保证),可通过第二个参数设为 true 创建公平信号量
import java.util.concurrent.Semaphore;
public class Service {
// 只允许 3 个线程同时访问该方法
private final Semaphore semaphore = new Semaphore(3);
public void access() {
try {
// 尝试获取许可
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + " 获取许可,开始执行任务");
Thread.sleep(1000); // 模拟执行任务
System.out.println(Thread.currentThread().getName() + " 任务执行完毕");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放许可
semaphore.release();
}
}
}
public class Test {
public static void main(String[] args) {
Service service = new Service();
for (int i = 1; i <= 10; i++) {
new Thread(service::access, "线程-" + i).start();
}
}
}
拓展用法
用法 | 说明 |
---|---|
semaphore.tryAcquire() | 尝试立即获取许可,获取不到则返回false |
semaphore.acquire(timeout) | 限时等待获取许可 |
new Semaphore(permits, true) | 公平模式,按线程请求顺序发放许可 |
应用场景
接口限流(限制同时访问人数)
限制数据库连接、IO资源使用
控制线程池或任务队列的并发度
限制对某类共享资源的访问数量(如线程安全的缓存、文件等)
ThreadLocal
ThreadLocal
是一种在多线程环境中解决线程安全问题的高级操作类。它为每个线程分配一个独立的线程副本,从而有效解决了变量并发访问冲突的问题ThreadLocal
同时实现了线程内的资源共享
案例:在使用 JDBC 操作数据库时,会将每个线程的 Connection
放入各自的 ThreadLocal
中,确保每个线程都在各自的 Connection
上进行数据库操作,避免出现 A 线程关闭了 B 线程的连接的情况
基本使用
-
set(value)
设置值 -
get()
获取值 -
remove()
清除值
示例代码
static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
new Thread(() -> {
String name = Thread.currentThread().getName();
threadLocal.set("itcast");
print(name);
System.out.println(name + "-after remove : " + threadLocal.get());
}, "t1").start();
new Thread(() -> {
String name = Thread.currentThread().getName();
threadLocal.set("itheima");
print(name);
System.out.println(name + "-after remove : " + threadLocal.get());
}, "t2").start();
}
static void print(String str) {
// 打印当前线程中本地内存中本地变量的值
System.out.println(str + " : " + threadLocal.get());
// 清除本地内存中的本地变量
threadLocal.remove();
}
原理
ThreadLocalz本质来说就是一个线程内部存储类,从而让多个线程只操作自己内部的值,从而实现线程数据隔离

set 方法

get 方法

内存泄漏问题
Java 对象中的四种引用类型:强引用、软引用、弱引用、虚引用
强引用:
- 最为普通的引用方式,表示一个对象处于有用且必须的状态,如果一个对象具有强引用,则GC并不会回收它。即便堆中内存不足了,宁可出现OOM,也不会对其进行回收
-
User user = new User();
软引用:
- 表示一个对象处于可能有用但非必须的状态。在GC线程扫描内存区域时,一旦发现弱引用,就会回收到弱引用相关联的对象。对于弱引用的回收,无关内存区域是否足够,一旦发现则会被回收
User user = new User(); WeakReference weakReference = new WeakReference<>(user);
每个线程维护一个ThreadLocalMap
,在该ThreadLocalMap
中,Entry
对象继承自WeakReference
。其中,键为使用弱引用的ThreadLocal
实例,值为线程变量的副本

防止内存泄漏:务必 remove
ThreadLocal 补充
- 什么是 ThreadLocal? 用来解决什么问题的?
- 说说你对 ThreadLocal 的理解
- ThreadLocal 是如何实现线程隔离的?
- 为什么 ThreadLocal 会造成内存泄露? 如何解决
- 还有哪些使用 ThreadLocal 的应用场景?
ThreadLocal 简介
线程安全(是指广义上的共享资源访问安全性,因为线程隔离是通过副本保证本线程访问资源安全性,它不保证线程之间还存在共享关系的狭义上的安全性)的解决思路:
- 互斥同步: synchronized 和 ReentrantLock
- 非阻塞同步: CAS, AtomicXXXX
- 无同步方案: 栈封闭,本地存储(Thread Local),可重入代码
本地存储(Thread Local),官网的解释是这样的:
该类提供线程局部(thread-local)变量。此类变量与其常规对应物不同,因为每个访问该变量(通过其
get
或set
方法)的线程都拥有自己的独立初始化的变量副本。ThreadLocal
实例通常作为类中的私有静态字段,用于将与特定线程关联的状态(例如,用户ID或事务ID)相关联
总结而言:ThreadLocal 是一个将在多线程中为每一个线程创建单独的变量副本的类; 当使用 ThreadLocal 来维护变量时, ThreadLocal 会为每个线程创建单独的变量副本, 避免因多线程操作共享变量而导致的数据不一致的情况
ThreadLocal 理解
提到 ThreadLocal 被提到应用最多的是 session 管理和数据库链接管理,这里以数据访问为例:
- 如下数据库管理类在单线程使用是没有任何问题的
class ConnectionManager {
private static Connection connect = null;
public static Connection openConnection() {
if (connect == null) {
connect = DriverManager.getConnection();
}
return connect;
}
public static void closeConnection() {
if (connect != null)
connect.close();
}
}
很显然,在多线程中使用会存在线程安全问题:第一,这里面的 2 个方法都没有进行同步,很可能在 openConnection 方法中会多次创建 connect;第二,由于 connect 是共享变量,那么必然在调用 connect 的地方需要使用到同步来保障线程安全,因为很可能一个线程在使用 connect 进行数据库操作,而另外一个线程调用 closeConnection 关闭链接
- 为了解决上述线程安全的问题,第一考虑:互斥同步
你可能会说,将这段代码的两个方法进行同步处理,并且在调用 connect 的地方需要进行同步处理,比如用 Synchronized 或者 ReentrantLock 互斥锁
- 这里再抛出一个问题:这地方到底需不需要将 connect 变量进行共享?
事实上,是不需要的。假如每个线程中都有一个 connect 变量,各个线程之间对 connect 变量的访问实际上是没有依赖关系的,即一个线程不需要关心其他线程是否对这个 connect 进行了修改的。即改后的代码可以这样:
class ConnectionManager {
private Connection connect = null;
public Connection openConnection() {
if (connect == null) {
connect = DriverManager.getConnection();
}
return connect;
}
public void closeConnection() {
if (connect != null)
connect.close();
}
}
class Dao {
public void insert() {
ConnectionManager connectionManager = new ConnectionManager();
Connection connection = connectionManager.openConnection();
// 使用connection进行操作
connectionManager.closeConnection();
}
}
这样处理确实也没有任何问题,由于每次都是在方法内部创建的连接,那么线程之间自然不存在线程安全问题。但是这样会有一个致命的影响:导致服务器压力非常大,并且严重影响程序执行性能。由于在方法中需要频繁地开启和关闭数据库连接,这样不仅严重影响程序执行效率,还可能导致服务器压力巨大
- 这时候 ThreadLocal 登场了
那么这种情况下使用 ThreadLocal 是再适合不过的了,因为 ThreadLocal 在每个线程中对该变量会创建一个副本,即每个线程内部都会有一个该变量,且在线程内部任何地方都可以使用,线程之间互不影响,这样一来就不存在线程安全问题,也不会严重影响程序执行性能。下面就是网上出现最多的例子:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class ConnectionManager {
private static final ThreadLocal<Connection> dbConnectionLocal = new ThreadLocal<Connection>() {
@Override
protected Connection initialValue() {
try {
return DriverManager.getConnection("", "", "");
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
};
public Connection getConnection() {
return dbConnectionLocal.get();
}
}
- 再注意下 ThreadLocal 的修饰符
ThreaLocal 的 JDK 文档中说明:ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread。如果希望通过某个类将状态(例如用户 ID、事务 ID)与线程关联起来,那么通常在这个类中定义 private static 类型的 ThreadLocal 实例
但是要注意,虽然 ThreadLocal 能够解决上面说的问题,但是由于在每个线程中都创建了副本,所以要考虑它对资源的消耗,比如内存的占用会比不使用 ThreadLocal 要大
实现
早期设计
JDK8 及之后

ThreadLocal 原理
如何实现线程隔离
主要是用到了 Thread 对象中的一个 ThreadLocalMap 类型的变量 threadLocals, 负责存储当前线程的关于 Connection 的对象, dbConnectionLocal(以上述例子中为例) 这个变量为 Key, 以新建的 Connection 对象为 Value; 这样的话, 线程第一次读取的时候如果不存在就会调用 ThreadLocal 的 initialValue 方法创建一个 Connection 对象并且返回
具体关于为线程分配变量副本的代码如下:
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap threadLocals = getMap(t);
if (threadLocals != null) {
ThreadLocalMap.Entry e = threadLocals.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
- 首先获取当前线程对象 t, 然后从线程 t 中获取到 ThreadLocalMap 的成员属性 threadLocals
- 如果当前线程的 threadLocals 已经初始化(即不为 null) 并且存在以当前 ThreadLocal 对象为 Key 的值, 则直接返回当前线程要获取的对象(本例中为 Connection)
- 如果当前线程的 threadLocals 已经初始化(即不为 null)但是不存在以当前 ThreadLocal 对象为 Key 的的对象, 那么重新创建一个 Connection 对象, 并且添加到当前线程的 threadLocals Map 中, 并返回
- 如果当前线程的 threadLocals 属性还没有被初始化, 则重新创建一个 ThreadLocalMap 对象, 并且创建一个 Connection 对象并添加到 ThreadLocalMap 对象中并返回
如果存在则直接返回很好理解, 那么对于如何初始化的代码又是怎样的呢?
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
- 首先调用上面写的重载过后的 initialValue 方法, 产生一个 Connection 对象
- 继续查看当前线程的 threadLocals 是不是空的, 如果 ThreadLocalMap 已被初始化, 那么直接将产生的对象添加到 ThreadLocalMap 中, 如果没有初始化, 则创建并添加对象到其中
同时, ThreadLocal 还提供了直接操作 Thread 对象中的 threadLocals 的方法
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
这样也可以不实现 initialValue, 将初始化工作放到 DBConnectionFactory 的 getConnection 方法中:
public Connection getConnection() {
Connection connection = dbConnectionLocal.get();
if (connection == null) {
try {
connection = DriverManager.getConnection("", "", "");
dbConnectionLocal.set(connection);
} catch (SQLException e) {
e.printStackTrace();
}
}
return connection;
}
为什么 ThreadLocal 能够实现变量的多线程隔离?其实就是用了 Map 的数据结构给当前线程缓存了, 要使用的时候就从本线程的 threadLocals 对象中获取就可以了, key 就是当前线程
在当前线程下获取当前线程里面的 Map 里面的对象并操作肯定没有线程并发问题了, 当然能做到变量的线程间隔离了
ThreadLocalMap 是个什么对象, 为什么要用这个对象呢?
ThreadLocalMap 对象是什么
本质上来讲, 它就是一个 Map, 但是这个 ThreadLocalMap 与我们平时见到的 Map 有点不一样
- 它没有实现 Map 接口
- 它没有 public 的方法, 最多有一个 default 的构造方法, 因为这个 ThreadLocalMap 的方法仅仅在 ThreadLocal 类中调用, 属于静态内部类
- ThreadLocalMap 的 Entry 实现继承了 WeakReference<ThreadLocal<?>>
- 该方法仅仅用了一个 Entry 数组来存储 Key, Value; Entry 并不是链表形式, 而是每个 bucket 里面仅仅放一个 Entry
要了解 ThreadLocalMap 的实现, 先从入口开始, 就是往该 Map 中添加一个值:
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
先进行简单的分析, 对该代码表层意思进行解读:
- 看下当前 threadLocal 的在数组中的索引位置 比如:
i = 2
, 看i = 2
位置上面的元素(Entry)的Key
是否等于 threadLocal 这个 Key, 如果等于就很好说了, 直接将该位置上面的 Entry 的 Value 替换成最新的就可以了 - 如果当前位置上面的 Entry 的 Key 为空, 说明 ThreadLocal 对象已经被回收了, 那么就调用 replaceStaleEntry
- 如果清理完无用条目(ThreadLocal 被回收的条目)、并且数组中的数据大小 > 阈值的时候对当前的 Table 进行重新哈希。所以该 HashMap 是处理冲突检测的机制是向后移位, 清除过期条目,最终找到合适的位置
了解完 Set 方法, 后面就是 Get 方法了:
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
先找到 ThreadLocal 的索引位置, 如果索引位置处的 entry 不为空并且键与 threadLocal 是同一个对象, 则直接返回;否则去后面的索引位置继续查找
ThreadLocal 造成内存泄露的问题
网上有这样一个例子:
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadLocalDemo {
static class LocalVariable {
private Long[] a = new Long[1024 * 1024];
}
// (1)
final static ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES,
new LinkedBlockingQueue<>());
// (2)
final static ThreadLocal<LocalVariable> localVariable = new ThreadLocal<LocalVariable>();
public static void main(String[] args) throws InterruptedException {
// (3)
Thread.sleep(5000 * 4);
for (int i = 0; i < 50; ++i) {
poolExecutor.execute(new Runnable() {
public void run() {
// (4)
localVariable.set(new LocalVariable());
// (5)
System.out.println("use local varaible" + localVariable.get());
localVariable.remove();
}
});
}
// (6)
System.out.println("pool execute over");
}
}
如果用线程池来操作 ThreadLocal 对象确实会造成内存泄露, 因为对于线程池里面不会销毁的线程, 里面总会存在着 <ThreadLocal, LocalVariable>
的强引用, 因为 final static 修饰的 ThreadLocal 并不会释放, 而 ThreadLocalMap 对于 Key 虽然是弱引用, 但是强引用不会释放, 弱引用当然也会一直有值, 同时创建的 LocalVariable 对象也不会释放, 就造成了内存泄露; 如果 LocalVariable 对象不是一个大对象的话, 其实泄露的并不严重, 泄露的内存 = 核心线程数 * LocalVariable
对象的大小
所以, 为了避免出现内存泄露的情况, ThreadLocal 提供了一个清除线程中对象的方法, 即 remove, 其实内部实现就是调用 ThreadLocalMap 的 remove 方法:
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
找到 Key 对应的 Entry, 并且清除 Entry 的 Key(ThreadLocal)置空, 随后清除过期的 Entry 即可避免内存泄露
再看 ThreadLocal 应用场景
除了上述的数据库管理类的例子,再看看其它一些应用:
每个线程维护了一个“序列号”
再回想上文说的,如果希望通过某个类将状态(例如用户 ID、事务 ID)与线程关联起来,那么通常在这个类中定义 private static 类型的 ThreadLocal 实例
每个线程维护了一个“序列号”
public class SerialNum {
// The next serial number to be assigned
private static int nextSerialNum = 0;
private static ThreadLocal serialNum = new ThreadLocal() {
protected synchronized Object initialValue() {
return new Integer(nextSerialNum++);
}
};
public static int get() {
return ((Integer) (serialNum.get())).intValue();
}
}
Session 的管理
经典的另外一个例子:
private static final ThreadLocal threadSession = new ThreadLocal();
public static Session getSession() throws InfrastructureException {
Session s = (Session) threadSession.get();
try {
if (s == null) {
s = getSessionFactory().openSession();
threadSession.set(s);
}
} catch (HibernateException ex) {
throw new InfrastructureException(ex);
}
return s;
}
在线程内部创建 ThreadLocal
还有一种用法是在线程类内部创建 ThreadLocal,基本步骤如下:
- 在多线程的类(如 ThreadDemo 类)中,创建一个 ThreadLocal 对象 threadXxx,用来保存线程间需要隔离处理的对象 xxx
- 在 ThreadDemo 类中,创建一个获取要隔离访问的数据的方法 getXxx(),在方法中判断,若 ThreadLocal 对象为 null 时候,应该 new()一个隔离访问类型的对象,并强制转换为要应用的类型
- 在 ThreadDemo 类的 run()方法中,通过调用 getXxx()方法获取要操作的数据,这样可以保证每个线程对应一个数据对象,在任何时刻都操作的是这个对象
public class ThreadLocalTest implements Runnable{
ThreadLocal<Student> StudentThreadLocal = new ThreadLocal<Student>();
@Override
public void run() {
String currentThreadName = Thread.currentThread().getName();
System.out.println(currentThreadName + " is running...");
Random random = new Random();
int age = random.nextInt(100);
System.out.println(currentThreadName + " is set age: " + age);
Student Student = getStudentt(); //通过这个方法,为每个线程都独立的new一个Studentt对象,每个线程的的Studentt对象都可以设置不同的值
Student.setAge(age);
System.out.println(currentThreadName + " is first get age: " + Student.getAge());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println( currentThreadName + " is second get age: " + Student.getAge());
}
private Student getStudentt() {
Student Student = StudentThreadLocal.get();
if (null == Student) {
Student = new Student();
StudentThreadLocal.set(Student);
}
return Student;
}
public static void main(String[] args) {
ThreadLocalTest t = new ThreadLocalTest();
Thread t1 = new Thread(t,"Thread A");
Thread t2 = new Thread(t,"Thread B");
t1.start();
t2.start();
}
}
class Student{
int age;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
java 开发手册中推荐的 ThreadLocal
看看阿里巴巴 java 开发手册中推荐的 ThreadLocal 的用法:
import java.text.DateFormat;
import java.text.SimpleDateFormat;
public class DateUtils {
public static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>(){
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};
}
然后再要用到 DateFormat 对象的地方,这样调用:
DateUtils.df.get().format(new Date());
ThreadLocalMap


源码
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
// 类型是 ThreadLocal泛型的弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
/**
* The initial capacity -- MUST be a power of two.
*/
private static final int INITIAL_CAPACITY = 16;
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;
/**
* The number of entries in the table.
*/
private int size = 0;
/**
* The next size value at which to resize.
*/
private int threshold; // Default to 0
/**
* Set the resize threshold to maintain at worst a 2/3 load factor.
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
/**
* Increment i modulo len.
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
/**
* Decrement i modulo len.
*/
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
/**
* Construct a new map initially containing (firstKey, firstValue).
* ThreadLocalMaps are constructed lazily, so we only create
* one when we have at least one entry to put in it.
*/
// 构造函数
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY]; // Entry数组
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); // 获取下标
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
/**
* Construct a new map including all Inheritable ThreadLocals
* from given parent map. Called only by createInheritedMap.
*
* @param parentMap the map associated with parent thread.
*/
private ThreadLocalMap(ThreadLocalMap parentMap) {
// 1. 获取父线程的 table(即父线程局部变量的哈希表)
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
// 2. 设置新 map 的阈值,初始化新 map
setThreshold(len);
table = new Entry[len];
// 3. 遍历父线程的 table,复制有效条目到新 map
for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) {
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get(); // 获取父线程的 ThreadLocal 键
if (key != null) {
// 4. 获取子线程继承的值
Object value = key.childValue(e.value);
// 5. 插入新条目到当前线程的 table
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);
// 处理哈希冲突,使用开放地址法
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++; // 增加新条目
}
}
}
}
/**
* Get the entry associated with key. This method
* itself handles only the fast path: a direct hit of existing
* key. It otherwise relays to getEntryAfterMiss. This is
* designed to maximize performance for direct hits, in part
* by making this method readily inlinable.
*
* @param key the thread local object
* @return the entry associated with key, or null if no such
*/
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
/**
* Version of getEntry method for use when key is not found in
* its direct hash slot.
*
* @param key the thread local object
* @param i the table index for key's hash code
* @param e the entry at table[i]
* @return the entry associated with key, or null if no such
*/
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
/**
* Set the value associated with key.
*
* @param key the thread local object
* @param value the value to be set
*/
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
/**
* Remove the entry for key.
*/
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
/**
* Replace a stale entry encountered during a set operation
* with an entry for the specified key. The value passed in
* the value parameter is stored in the entry, whether or not
* an entry already exists for the specified key.
*
* As a side effect, this method expunges all stale entries in the
* "run" containing the stale entry. (A run is a sequence of entries
* between two null slots.)
*
* @param key the key
* @param value the value to be associated with key
* @param staleSlot index of the first stale entry encountered while
* searching for key.
*/
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
// 1. 扫描并找出运行区间内的所有过时条目
// Back up to check for prior stale entry in current run.
// We clean out whole runs at a time to avoid continual
// incremental rehashing due to garbage collector freeing
// up refs in bunches (i.e., whenever the collector runs).
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
// 2. 查找目标键,进行替换操作
// Find either the key or trailing null slot of run, whichever
// occurs first
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// If we find key, then we need to swap it
// with the stale entry to maintain hash table order.
// The newly stale slot, or any other stale slot
// encountered above it, can then be sent to expungeStaleEntry
// to remove or rehash all of the other entries in run.
if (k == key) {
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// 如果存在过时条目,则开始清理
// Start expunge at preceding stale entry if it exists
if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// If we didn't find stale entry on backward scan, the
// first stale entry seen while scanning for key is the
// first still present in the run.
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// 3. 如果没有找到目标键,将新条目插入到过时位置
// If key not found, put new entry in stale slot
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// 4. 清理任何其他过时条目
// If there are any other stale entries in run, expunge them
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
/**
* Expunge a stale entry by rehashing any possibly colliding entries
* lying between staleSlot and the next null slot. This also expunges
* any other stale entries encountered before the trailing null. See
* Knuth, Section 6.4
*
* @param staleSlot index of slot known to have null key
* @return the index of the next null slot after staleSlot
* (all between staleSlot and this slot will have been checked
* for expunging).
*/
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
/**
* Heuristically scan some cells looking for stale entries.
* This is invoked when either a new element is added, or
* another stale one has been expunged. It performs a
* logarithmic number of scans, as a balance between no
* scanning (fast but retains garbage) and a number of scans
* proportional to number of elements, that would find all
* garbage but would cause some insertions to take O(n) time.
*
* @param i a position known NOT to hold a stale entry. The
* scan starts at the element after i.
*
* @param n scan control: {@code log2(n)} cells are scanned,
* unless a stale entry is found, in which case
* {@code log2(table.length)-1} additional cells are scanned.
* When called from insertions, this parameter is the number
* of elements, but when from replaceStaleEntry, it is the
* table length. (Note: all this could be changed to be either
* more or less aggressive by weighting n instead of just
* using straight log n. But this version is simple, fast, and
* seems to work well.)
*
* @return true if any stale entries have been removed.
*/
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
i = nextIndex(i, len);
Entry e = tab[i];
if (e != null && e.get() == null) {
n = len;
removed = true;
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}
/**
* Re-pack and/or re-size the table. First scan the entire
* table removing stale entries. If this doesn't sufficiently
* shrink the size of the table, double the table size.
*/
private void rehash() {
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
if (size >= threshold - threshold / 4)
resize();
}
/**
* Double the capacity of the table.
*/
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null; // Help the GC
} else {
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
setThreshold(newLen);
size = count;
table = newTab;
}
/**
* Expunge all stale entries in the table.
*/
private void expungeStaleEntries() {
Entry[] tab = table;
int len = tab.length;
for (int j = 0; j < len; j++) {
Entry e = tab[j];
if (e != null && e.get() == null)
expungeStaleEntry(j);
}
}
}
内存泄露问题
无论采用强引用还是弱引用,均存在内存泄露的风险。为避免内存泄露,可采取以下两种方式:
- 使用完
ThreadLocal
后,调用其remove
方法删除对应的Entry
- 使用完
ThreadLocal
,当前线程随之运行结束
相较第一种方式,第二种方式控制难度更大,尤其是在使用线程池时,线程结束并不会销毁
也就是说,只要记得在使用完ThreadLocal
后及时调用remove
方法,无论key
是强引用还是弱引用,都不会存在问题
那么,为何key
要使用弱引用呢?
实际上,在ThreadLocalMap
中的set/getEntry
方法中,会对key
为null
(即ThreadLocal
为null
)进行判断。如果为null
,则会对value
置为null
这就意味着,在使用完ThreadLocal
且CurrentThread
依然运行的前提下,即便忘记调用remove
方法,弱引用相较于强引用提供额外一层保障:弱引用的ThreadLocal
会被回收,对应的value
在下一次ThreadLocalMap
调用set/get/remove
中的任一方法时会被清除,从而避免内存泄露