Java并发编程面试题,73道Java多线程八股文(3.5万字145张手绘图),面渣逆袭必看👍
前言
3.5 万字 145 张手绘图,详解 73 道 Java 多线程面试高频题(让天下没有难背的八股),面渣背会这些并发编程八股文,这次吊打面试官,我觉得稳了(手动 dog)。
第一版作者是二哥编程星球的嘉宾三分恶,第二版由二哥结合球友们的面经+技术派+PmHub+mydb 的项目进行全新升级。更适合拿来背诵突击面试+底层原理理解。
亮白版本更适合拿出来打印,这也是很多学生党喜欢的方式,打印出来背诵的效率会更高。
2025 年 01 月 22 日开始着手第二版更新。
- 对于高频题,会标注在《Java 面试指南(付费)》中出现的位置,哪家公司,原题是什么,并且会加🌟,目录一目了然;如果你想节省时间的话,可以优先背诵这些题目,尽快做到知彼知己,百战不殆。
- 区分八股精华回答版本和原理底层解释,让大家知其然知其所以然,同时又能做到面试时的高效回答。
- 结合项目(技术派、pmhub)来组织语言,让面试官最大程度感受到你的诚意,而不是机械化的背诵。
- 修复第一版中出现的问题,包括球友们的私信反馈,网站留言区的评论,以及 GitHub 仓库中的 issue,让这份面试指南更加完善。
- 增加二哥编程星球的球友们拿到的一些 offer,对面渣逆袭的感谢,以及对简历修改的一些认可,以此来激励大家,给大家更多信心。
- 优化排版,增加手绘图,重新组织答案,使其更加口语化,从而更贴近面试官的预期。
由于 PDF 没办法自我更新,所以需要最新版的小伙伴,可以微信搜【沉默王二】,或者扫描/长按识别下面的二维码,关注二哥的公众号,回复【222】即可拉取最新版本。
当然了,请允许我的一点点私心,那就是星球的 PDF 版本会比公众号早一个月时间,毕竟星球用户都付费过了,我有必要让他们先享受到一点点福利。相信大家也都能理解,毕竟在线版是免费的,CDN、服务器、域名、OSS 等等都是需要成本的。
更别说我付出的时间和精力了,大家觉得有帮助还请给个口碑,让你身边的同事、同学都能受益到。
我把二哥的 Java 进阶之路、JVM 进阶之路、并发编程进阶之路,以及所有面渣逆袭的版本都放进来了,涵盖 Java基础、Java集合、Java并发、JVM、Spring、MyBatis、计算机网络、操作系统、MySQL、Redis、RocketMQ、分布式、微服务、设计模式、Linux 等 16 个大的主题,共有 40 多万字,2000+张手绘图,可以说是诚意满满。
展示一下暗黑版本的 PDF 吧,排版清晰,字体优雅,更加适合夜服,晚上看会更舒服一点。
基础
1.并行跟并发有什么区别?
- 并行是多核 CPU 上的多任务处理,多个任务在同一时间真正地同时执行。
- 并发是单核 CPU 上的多任务处理,多个任务在同一时间段内交替执行,通过时间片轮转实现交替执行,用于解决 IO 密集型任务的瓶颈。
举个例子,就好像我们去食堂打饭,并行就是每个人对应一个阿姨,同时打饭;而并发就是一个阿姨,轮流给每个人打饭,假如有个人磨磨唧唧,阿姨就会吆喝下一个人,这样就能提高食堂的打饭效率。
你是如何理解线程安全的?
推荐阅读:多线程带来了哪些问题?
如果一段代码块或者一个方法被多个线程同时执行,还能够正确地处理共享数据,那么这段代码块或者这个方法就是线程安全的。
可以从三个要素来确保线程安全:
①、原子性:一个操作要么完全执行,要么完全不执行,不会出现中间状态。
可以通过同步关键字 synchronized 或原子操作,如 AtomicInteger 来保证原子性。
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // 原子操作
②、可见性:当一个线程修改了共享变量,其他线程能够立即看到变化。
可以通过 volatile 关键字来保证可见性。
private volatile String itwanger = "沉默王二";
③、有序性:要确保线程不会因为死锁、饥饿、活锁等问题导致无法继续执行。
- Java 面试指南(付费)收录的华为 OD 面经同学 1 一面面试原题:对于多线程编程的了解?
- Java 面试指南(付费)收录的快手面经同学 1 部门主站技术部面试原题:你对线程安全的理解是什么?
memo:2025 年 1 月 22 日修改至此。
2.🌟说说进程和线程的区别?
推荐阅读:进程与线程的区别是什么?
进程说简单点就是我们在电脑上启动的一个个应用。它是操作系统分配资源的最小单位。
线程是进程中的独立执行单元。多个线程可以共享同一个进程的资源,如内存;每个线程都有自己独立的栈和寄存器。
如何理解协程?
协程被视为比线程更轻量级的并发单元,可以在单线程中实现并发执行,由我们开发者显式调度。
协程是在用户态进行调度的,避免了线程切换时的内核态开销。
Java 自身是不支持携程的,我们可以使用 Quasar、Kotlin 等框架来实现协程。
fun main() = runBlocking {
launch {
delay(1000L)
println("World!")
}
println("Hello,")
}
线程间是如何进行通信的?
原则上可以通过消息传递和共享内存两种方法来实现。Java 采用的是共享内存的并发模型。
这个模型被称为 Java 内存模型,简写为 JMM,它决定了一个线程对共享变量的写入,何时对另外一个线程可见。当然了,本地内存是 JMM 的一个抽象概念,并不真实存在。
用一句话来概括就是:共享变量存储在主内存中,每个线程的私有本地内存,存储的是这个共享变量的副本。
线程 A 与线程 B 之间如要通信,需要要经历 2 个步骤:
- 线程 A 把本地内存 A 中的共享变量副本刷新到主内存中。
- 线程 B 到主内存中读取线程 A 刷新过的共享变量,再同步到自己的共享变量副本中。
- Java 面试指南(付费)收录的字节跳动商业化一面的原题:进程和线程区别,线程共享内存和进程共享内存的区别
- Java 面试指南(付费)收录的小米春招同学 K 一面面试原题:协程和线程和进程的区别
- Java 面试指南(付费)收录的字节跳动面经同学 1 Java 后端技术一面面试原题:线程和进程有什么区别?
- Java 面试指南(付费)收录的华为 OD 面经同学 1 一面面试原题:对于多线程编程的了解?
- Java 面试指南(付费)收录的美团面经同学 2 Java 后端技术一面面试原题:进程和线程的区别?
- Java 面试指南(付费)收录的华为面经同学 9 Java 通用软件开发一面面试原题:进程和线程的区别
- Java 面试指南(付费)收录的 小公司面经合集好未来测开面经同学 3 测开一面面试原题:进程和线程的区别
- Java 面试指南(付费)收录的招商银行面经同学 6 招银网络科技面试原题:进程和线程的区别?
- Java 面试指南(付费)收录的用友面试原题:线程和进程的区别
- Java 面试指南(付费)收录的vivo 面经同学 10 技术一面面试原题:线程的概念,线程有哪些状态
- Java 面试指南(付费)收录的海康威视同学 4面试原题:对协程的了解,为什么协程比线程还有更低的资源消耗
memo:2025 年 8 月 17 日修改至此。今天在帮球友修改简历的时候,收到他的反馈说:上次也麻烦我帮他改了改简历,也顺利找到了实习,秋招逼近,希望我能再帮他看看实习经历。感谢球友的每一次口碑。
3.🌟说说线程有几种创建方式?
有三种,分别是继承 Thread 类、实现 Runnable 接口、实现 Callable 接口。
第一种需要重写父类 Thread 的 run() 方法,并且调用 start() 方法启动线程。
class ThreadTask extends Thread {
public void run() {
System.out.println("看完二哥的 Java 进阶之路,上岸了!");
}
public static void main(String[] args) {
ThreadTask task = new ThreadTask();
task.start();
}
}
这种方法的缺点是,如果 ThreadTask 已经继承了另外一个类,就不能再继承 Thread 类了,因为 Java 不支持多重继承。
第二种需要重写 Runnable 接口的 run() 方法,并将实现类的对象作为参数传递给 Thread 对象的构造方法,最后调用 start() 方法启动线程。
class RunnableTask implements Runnable {
public void run() {
System.out.println("看完二哥的 Java 进阶之路,上岸了!");
}
public static void main(String[] args) {
RunnableTask task = new RunnableTask();
Thread thread = new Thread(task);
thread.start();
}
}
这种方法的优点是可以避免 Java 的单继承限制,并且更符合面向对象的编程思想,因为 Runnable 接口将任务代码和线程控制的代码解耦了。
第三种需要重写 Callable 接口的 call() 方法,然后创建 FutureTask 对象,参数为 Callable 实现类的对象;紧接着创建 Thread 对象,参数为 FutureTask 对象,最后调用 start() 方法启动线程。
class CallableTask implements Callable {
public String call() {
return "看完二哥的 Java 进阶之路,上岸了!";
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
CallableTask task = new CallableTask();
FutureTask futureTask = new FutureTask<>(task);
Thread thread = new Thread(futureTask);
thread.start();
System.out.println(futureTask.get());
}
}
这种方法的优点是可以获取线程的执行结果。
一个 8G 内存的系统最多能创建多少个线程?
推荐阅读:深入理解 JVM 的运行时数据区
理论上大约 8000 个。
创建线程的时候,至少需要分配一个虚拟机栈,在 64 位操作系统中,默认大小为 1M,因此一个线程大约需要 1M 的内存。
但 JVM、操作系统本身的运行就要占一定的内存空间,所以实际上可以创建的线程数远比 8000 少。
详细解释一下。
可以通过 java -XX:+PrintFlagsFinal -version | grep ThreadStackSize 命令查看 JVM 栈的默认大小。
其中 ThreadStackSize 的单位是 KB,也就是说默认的 JVM 栈大小是 1024 KB,也就是 1M。
启动一个 Java 程序,你能说说里面有哪些线程吗?
首先是 main 线程,这是程序执行的入口。
然后是垃圾回收线程,它是一个后台线程,负责回收不再使用的对象。
还有编译器线程,比如 JIT,负责把一部分热点代码编译后放到 codeCache 中。
可以通过下面的代码进行检测:
class ThreadLister {
public static void main(String[] args) {
// 获取所有线程的堆栈跟踪
Map threads = Thread.getAllStackTraces();
for (Thread thread : threads.keySet()) {
System.out.println("Thread: " + thread.getName() + " (ID=" + thread.getId() + ")");
}
}
}
结果如下所示:
Thread: Monitor Ctrl-Break (ID=5)
Thread: Reference Handler (ID=2)
Thread: main (ID=1)
Thread: Signal Dispatcher (ID=4)
Thread: Finalizer (ID=3)
简单解释下:
Thread: main (ID=1)- 主线程,Java 程序启动时由 JVM 创建。Thread: Reference Handler (ID=2)- 这个线程是用来处理引用对象的,如软引用、弱引用和虚引用。负责清理被 JVM 回收的对象。Thread: Finalizer (ID=3)- 终结器线程,负责调用对象的 finalize 方法。对象在垃圾回收器标记为可回收之前,由该线程执行其 finalize 方法,用于执行特定的资源释放操作。Thread: Signal Dispatcher (ID=4)- 信号调度线程,处理来自操作系统的信号,将它们转发给 JVM 进行进一步处理,例如响应中断、停止等信号。Thread: Monitor Ctrl-Break (ID=5)- 监视器线程,通常由一些特定的 IDE 创建,用于在开发过程中监控和管理程序执行或者处理中断。
你平时有用过多线程吗?你在代码中是哪些场景用呢?
用得比较多,批量数据处理、异步任务处理、定时任务调度都需要用到多线程。
比如说在技术派的首页内容加载中,就用到了多线程来并行加载不同的模块,提高页面的响应速度。
- Java 面试指南(付费)收录的字节跳动面经同学 1 Java 后端技术一面面试原题:有多少种实现线程的方法?
- Java 面试指南(付费)收录的农业银行同学 1 面试原题:实现线程的方式和区别
- Java 面试指南(付费)收录的农业银行面经同学 3 Java 后端面试原题:说说线程的创建方法
- Java 面试指南(付费)收录的小公司面经合集同学 1 Java 后端面试原题:线程创建的方式?Runable 和 Callable 有什么区别?
- Java 面试指南(付费)收录的阿里面经同学 5 阿里妈妈 Java 后端技术一面面试原题:一个 8G 内存的系统最多能创建多少线程?(奇怪的问题,答了一些 pcb、页表、虚拟机栈什么的)启动一个 Java 程序,你能说说里面有哪些线程吗?
- Java 面试指南(付费)收录的招商银行面经同学 6 招银网络科技面试原题:如何创建线程?
- Java 面试指南(付费)收录的百度面经同学 1 文心一言 25 实习 Java 后端面试原题:java 如何创建线程?每次都要创建新线程来实现异步操作,很繁琐,有了解线程池吗?
- Java 面试指南(付费)收录的美团面经同学 4 一面面试原题:平时怎么使用多线程
memo:2025 年 9 月 26 日修改至此。今天有球友在星球里报喜说拿到了字节的意向,感谢二哥的面渣逆袭。
4.🌟调用 start 方法时会执行 run 方法,那怎么不直接调用 run方法?
调用 start() 会创建一个新的线程,并异步执行 run() 方法中的代码。
直接调用 run() 方法只是一个普通的同步方法调用,所有代码都在当前线程中执行,不会创建新线程。没有新的线程创建,也就达不到多线程并发的目的。
通过敲代码体验一下。
class MyThread extends Thread {
public void run() {
System.out.println(Thread.currentThread().getName());
}
public static void main(String[] args) {
MyThread t1 = new MyThread();
t1.start(); // 正确的方式,创建一个新线程,并在新线程中执行 run()
t1.run(); // 仅在主线程中执行 run(),没有创建新线程
}
}
来看输出结果:
main
Thread-0
也就是说,调用 start() 方法会通知 JVM,去调用底层的线程调度机制来启动新线程。
调用 start() 后,线程进入就绪状态,等待操作系统调度;一旦调度执行,线程会执行其 run() 方法中的代码。
- Java 面试指南(付费)收录的小公司面经合集同学 1 Java 后端面试原题:启动一个线程是 run()还是 start()?
- Java 面试指南(付费)收录的百度面经同学 1 文心一言 25 实习 Java 后端面试原题:java 如何启动多线程,有哪些方式?
- 二哥编程星球球友枕云眠美团 AI 面试原题:java 线程操作中的 start 和 run 方法区别是什么
memo:2025 年 1 月 26 日修改至此。
5.线程有哪些常用的调度方法?
比如说 start 方法用于启动线程并让操作系统调度执行;sleep 方法用于让当前线程休眠一段时间;wait 方法会让当前线程等待,notify 会唤醒一个等待的线程。
说说wait方法和notify方法?
当线程 A 调用共享对象的 wait() 方法时,线程 A 会被阻塞挂起,直到:
- 线程 B 调用了共享对象的
notify()方法或者notifyAll()方法; - 其他线程调用线程 A 的
interrupt()方法,导致线程 A 抛出 InterruptedException 异常。
线程 A 调用共享对象的 wait(timeout)方法后,没有在指定的 timeout 时间内被其它线程唤醒,那么这个方法会因为超时而返回。
当线程 A 调用共享对象的 notify() 方法后,会唤醒一个在这个共享对象上调用 wait 系列方法被挂起的线程。
共享对象上可能会有多个线程在等待,具体唤醒哪个线程是随机的。
如果调用的是 notifyAll 方法,会唤醒所有在这个共享变量上调用 wait 系列方法而被挂起的线程。
说说 sleep 方法?
当线程 A 调用了 Thread 的 sleep 方法后,线程 A 会暂时让出指定时间的执行权。
指定的睡眠时间到了后该方法会正常返回,接着参与 CPU 调度,获取到 CPU 资源后可以继续执行。
说说yield方法?
yield() 方法的目的是让当前线程让出 CPU 使用权,回到就绪状态。但是线程调度器可能会忽略。
说说interrupt方法?
推荐阅读:interrupt 方法
interrupt() 方法用于通知线程停止,但不会直接终止线程,需要线程自行处理中断标志。
常与 isInterrupted() 或 Thread.interrupted() 配合使用。
Thread thread = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("Running");
}
System.out.println("Interrupted");
});
thread.start();
thread.interrupt(); // 中断线程
说说 stop 方法?
stop 方法用来强制停止线程,目前已经处于废弃状态,因为 stop 方法可能会在不一致的状态下释放锁,破坏对象的一致性。
- Java 面试指南(付费)收录的帆软同学 3 Java 后端一面的原题:怎么停止一个线程,interrupt 和 stop 区别
memo:2025 年 1 月 27 日修改至此。
6.🌟线程有几种状态?
6 种。
new 代表线程被创建但未启动;runnable 代表线程处于就绪或正在运行状态,由操作系统调度;blocked 代表线程被阻塞,等待获取锁;waiting 代表线程等待其他线程的通知或中断;timed_waiting 代表线程会等待一段时间,超时后自动恢复;terminated 代表线程执行完毕,生命周期结束。
也就是说,线程的生命周期可以分为五个主要阶段:新建、就绪、运行、阻塞和终止。线程在运行过程中会根据状态的变化在这些阶段之间切换。
class ThreadStateExample {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
try {
Thread.sleep(2000); // TIMED_WAITING
synchronized (ThreadStateExample.class) {
ThreadStateExample.class.wait(); // WAITING
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
System.out.println("State after creation: " + thread.getState()); // NEW
thread.start();
System.out.println("State after start: " + thread.getState()); // RUNNABLE
Thread.sleep(500);
System.out.println("State while sleeping: " + thread.getState()); // TIMED_WAITING
synchronized (ThreadStateExample.class) {
ThreadStateExample.class.notify(); // 唤醒线程
}
thread.join();
System.out.println("State after termination: " + thread.getState()); // TERMINATED
}
}
用一个表格来做个总结:
| 状态 | 说明 |
|---|---|
| NEW | 当线程被创建后,如通过new Thread(),它处于新建状态。此时,线程已经被分配了必要的资源,但还没有开始执行。 |
| RUNNABLE | 当调用线程的start()方法后,线程进入可运行状态。在这个状态下,线程可能正在运行也可能正在等待获取 CPU 时间片,具体取决于线程调度器的调度策略。 |
| BLOCKED | 线程在试图获取一个锁以进入同步块/方法时,如果锁被其他线程持有,线程将进入阻塞状态,直到它获取到锁。 |
| WAITING | 线程进入等待状态是因为调用了如下方法之一:Object.wait()或LockSupport.park()。在等待状态下,线程需要其他线程显式地唤醒,否则不会自动执行。 |
| TIME_WAITING | 当线程调用带有超时参数的方法时,如Thread.sleep(long millis)、Object.wait(long timeout) 或LockSupport.parkNanos(),它将进入超时等待状态。线程在指定的等待时间过后会自动返回可运行状态。 |
| TERMINATED | 当线程的run()方法执行完毕后,或者因为一个未捕获的异常终止了执行,线程进入终止状态。一旦线程终止,它的生命周期结束,不能再被重新启动。 |
如何强制终止线程?
第一步,调用线程的 interrupt() 方法,请求终止线程。
第二步,在线程的 run() 方法中检查中断状态,如果线程被中断,就退出线程。
class MyTask implements Runnable {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
try {
System.out.println("Running...");
Thread.sleep(1000); // 模拟工作
} catch (InterruptedException e) {
// 捕获中断异常后,重置中断状态
Thread.currentThread().interrupt();
System.out.println("Thread interrupted, exiting...");
break;
}
}
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new MyTask());
thread.start();
Thread.sleep(3000); // 主线程等待3秒
thread.interrupt(); // 请求终止线程
}
}
中断结果:
- Java 面试指南(付费)收录的招商银行面经同学 6 招银网络科技面试原题:线程的生命周期和状态?
- Java 面试指南(付费)收录的快手同学 2 一面面试原题:线程有哪些状态?
- Java 面试指南(付费)收录的 OPPO 面经同学 1 面试原题:Java里线程的生命周期
- Java 面试指南(付费)收录的同学 D 小米一面原题:线程的生命周期
7.什么是线程上下文切换?
线程上下文切换是指 CPU 从一个线程切换到另一个线程执行时的过程。
在线程切换的过程中,CPU 需要保存当前线程的执行状态,并加载下一个线程的上下文。
之所以要这样,是因为 CPU 在同一时刻只能执行一个线程,为了实现多线程并发执行,需要不断地在多个线程之间切换。
为了让用户感觉多个线程是在同时执行的, CPU 资源的分配采用了时间片轮转的方式,线程在时间片内占用 CPU 执行任务。当线程使用完时间片后,就会让出 CPU 让其他线程占用。
线程可以被多核调度吗?
多核处理器提供了并行执行多个线程的能力。每个核心可以独立执行一个或多个线程,操作系统的任务调度器会根据策略和算法,如优先级调度、轮转调度等,决定哪个线程何时在哪个核心上运行。
- Java 面试指南(付费)收录的字节跳动同学 7 Java 后端实习一面的原题:线程可以被多核调度吗?
- Java 面试指南(付费)收录的携程面经同学 1 Java 后端技术一面面试原题:线程上下文切换(我答的内核态和用户态切换时机,和切换需要加载哪些内容)
8.守护线程了解吗?
了解,守护线程是一种特殊的线程,它的作用是为其他线程提供服务。
Java 中的线程分为两类,一种是守护线程,另外一种是用户线程。
JVM 启动时会调用 main 方法,main 方法所在的线程就是一个用户线程。在 JVM 内部,同时还启动了很多守护线程,比如垃圾回收线程。
守护线程和用户线程有什么区别呢?
区别之一是当最后一个非守护线程束时, JVM 会正常退出,不管当前是否存在守护线程,也就是说守护线程是否结束并不影响 JVM 退出。
换而言之,只要有一个用户线程还没结束,正常情况下 JVM 就不会退出。
9.线程间有哪些通信方式?
线程之间传递信息的方式有多种,比如说使用 volatile 和 synchronized 关键字共享对象、使用 wait() 和 notify() 方法实现生产者-消费者模式、使用 Exchanger 进行数据交换、使用 Condition 实现线程间的协调等。
简单说说 volatile 和 synchronized 的使用方式?
多个线程可以通过 volatile 和 synchronized 关键字访问和修改同一个对象,从而实现信息的传递。
关键字 volatile 可以用来修饰成员变量,告知程序任何对该变量的访问均需要从共享内存中获取,并同步刷新回共享内存,保证所有线程对变量访问的可见性。
关键字 synchronized 可以修饰方法,或者同步代码块,确保多个线程在同一个时刻只有一个线程在执行方法或代码块。
class SharedObject {
private String message;
private boolean hasMessage = false;
public synchronized void writeMessage(String message) {
while (hasMessage) {
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
this.message = message;
hasMessage = true;
notifyAll();
}
public synchronized String readMessage() {
while (!hasMessage) {
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
hasMessage = false;
notifyAll();
return message;
}
}
public class Main {
public static void main(String[] args) {
SharedObject sharedObject = new SharedObject();
Thread writer = new Thread(() -> {
sharedObject.writeMessage("Hello from Writer!");
});
Thread reader = new Thread(() -> {
String message = sharedObject.readMessage();
System.out.println("Reader received: " + message);
});
writer.start();
reader.start();
}
}
wait() 和 notify() 方法的使用方式了解吗?
一个线程调用共享对象的 wait() 方法时,它会进入该对象的等待池,释放已经持有的锁,进入等待状态。
一个线程调用 notify() 方法时,它会唤醒在该对象等待池中等待的一个线程,使其进入锁池,等待获取锁。
class MessageBox {
private String message;
private boolean empty = true;
public synchronized void produce(String message) {
while (!empty) {
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
empty = false;
this.message = message;
notifyAll();
}
public synchronized String consume() {
while (empty) {
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
empty = true;
notifyAll();
return message;
}
}
public class Main {
public static void main(String[] args) {
MessageBox box = new MessageBox();
Thread producer = new Thread(() -> {
box.produce("Message from producer");
});
Thread consumer = new Thread(() -> {
String message = box.consume();
System.out.println("Consumer received: " + message);
});
producer.start();
consumer.start();
}
}
Condition 也提供了类似的方法,await() 负责阻塞、signal() 和 signalAll() 负责通知。
通常与锁 ReentrantLock 一起使用,为线程提供了一种等待某个条件成真的机制,并允许其他线程在该条件变化时通知等待线程。
Exchanger 的使用方式了解吗?
Exchanger 是一个同步点,可以在两个线程之间交换数据。一个线程调用 exchange() 方法,将数据传递给另一个线程,同时接收另一个线程的数据。
class Main {
public static void main(String[] args) {
Exchanger exchanger = new Exchanger<>();
Thread thread1 = new Thread(() -> {
try {
String message = "Message from thread1";
String response = exchanger.exchange(message);
System.out.println("Thread1 received: " + response);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
Thread thread2 = new Thread(() -> {
try {
String message = "Message from thread2";
String response = exchanger.exchange(message);
System.out.println("Thread2 received: " + response);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
thread1.start();
thread2.start();
}
}
CompletableFuture 的使用方式了解吗?
CompletableFuture 是 Java 8 引入的一个类,支持异步编程,允许线程在完成计算后将结果传递给其他线程。
class Main {
public static void main(String[] args) {
CompletableFuture future = CompletableFuture.supplyAsync(() -> {
// 模拟长时间计算
return "Message from CompletableFuture";
});
future.thenAccept(message -> {
System.out.println("Received: " + message);
});
}
}
- Java 面试指南(付费)收录的华为 OD 的面试中出现过该原题。
- Java 面试指南(付费)收录的阿里面经同学 1 闲鱼后端一面的原题:线程之间传递信息?
- Java 面试指南(付费)收录的理想汽车面经同学 2 一面面试原题:线程内有哪些通信方式?线程之间有哪些通信方式?
memo:2025 年 1 月 28 日修改至此。
10.🌟请说说 sleep 和 wait 的区别?(补充)
2024 年 03 月 21 日增补
sleep 会让当前线程休眠,不需要获取对象锁,属于 Thread 类的方法;wait 会让获得对象锁的线程等待,要提前获得对象锁,属于 Object 类的方法。
详细解释下。
①、所属类不同
sleep()方法专属于Thread类。wait()方法专属于Object类。
②、锁行为不同
如果一个线程在持有某个对象锁时调用了 sleep 方法,它在睡眠期间仍然会持有这个锁。
class SleepDoesNotReleaseLock {
private static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread sleepingThread = new Thread(() -> {
synchronized (lock) {
System.out.println("Thread 1 会继续持有锁,并且进入睡眠状态");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 1 醒来了,并且释放了锁");
}
});
Thread waitingThread = new Thread(() -> {
synchronized (lock) {
System.out.println("Thread 2 进入同步代码块");
}
});
sleepingThread.start();
Thread.sleep(1000);
waitingThread.start();
}
}
输出结果:
Thread 1 会继续持有锁,并且进入睡眠状态
Thread 1 醒来了,并且释放了锁
Thread 2 进入同步代码块
从输出中我们可以看到,waitingThread 必须等待 sleepingThread 完成睡眠后才能进入同步代码块。
而当线程执行 wait 方法时,它会释放持有的对象锁,因此其他线程也有机会获取该对象的锁。
class WaitReleasesLock {
private static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread waitingThread = new Thread(() -> {
synchronized (lock) {
try {
System.out.println("Thread 1 持有锁,准备等待 5 秒");
lock.wait(5000);
System.out.println("Thread 1 醒来了,并且退出同步代码块");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread notifyingThread = new Thread(() -> {
synchronized (lock) {
System.out.println("Thread 2 尝试唤醒等待中的线程");
lock.notify();
System.out.println("Thread 2 执行完了 notify");
}
});
waitingThread.start();
Thread.sleep(1000);
notifyingThread.start();
}
}
输出结果:
Thread 1 持有锁,准备等待 5 秒
Thread 2 尝试唤醒等待中的线程
Thread 2 执行完了 notify
Thread 1 醒来了,并且退出同步代码块
这表明 waitingThread 在调用 wait 后确实释放了锁。
③、使用条件不同
sleep()方法可以在任何地方被调用。wait()方法必须在同步代码块或同步方法中被调用,这是因为调用wait()方法的前提是当前线程必须持有对象的锁。否则会抛出IllegalMonitorStateException异常。
④、唤醒方式不同
- 调用 sleep 方法后,线程会进入 TIMED_WAITING 状态,即在指定的时间内暂停执行。当指定的时间结束后,线程会自动恢复到 RUNNABLE 状态,等待 CPU 调度再次执行。
- 调用 wait 方法后,线程会进入 WAITING 状态,直到有其他线程在同一对象上调用 notify 或 notifyAll 方法,线程才会从 WAITING 状态转变为 RUNNABLE 状态,准备再次获得 CPU 的执行权。
我们来通过代码再感受一下 sleep() 和 wait() 在用法上的区别,先看 sleep() 的用法:
class SleepExample {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println("线程准备休眠 2 秒");
try {
Thread.sleep(2000); // 线程将睡眠2秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程醒来了");
});
thread.start();
}
}
再来看 wait() 的用法:
class WaitExample {
public static void main(String[] args) {
final Object lock = new Object();
Thread thread = new Thread(() -> {
synchronized (lock) {
try {
System.out.println("线程准备等待 2 秒");
lock.wait(2000); // 线程会等待2秒,或者直到其他线程调用 lock.notify()/notifyAll()
System.out.println("线程结束等待");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
}
}
- Java 面试指南(付费)收录的腾讯 Java 后端实习一面原题:说说 sleep 和 wait 的区别
- 二哥编程星球球友枕云眠美团 AI 面试原题:解释一下 java 线程中 sleep 和 wait 方法的主要区别?使用时会对线程状态有什么影响
- Java 面试指南(付费)收录的快手同学 2 一面面试原题:调用wait()方法时是哪个状态,sleep和wait区别?
- Java 面试指南(付费)收录的同学 D 小米一面原题:sleep和wait的区别
memo:2025 年 1 月 29 日修改至此。
11.🌟怎么保证线程安全?(补充)
2024 年 05 月 01 日增补
线程安全是指在并发环境下,多个线程访问共享资源时,程序能够正确地执行,而不会出现数据不一致的问题。
为了保证线程安全,可以使用 synchronized 关键字对方法加锁,对代码块加锁。线程在执行同步方法、同步代码块时,会获取类锁或者对象锁,其他线程就会阻塞并等待锁。
如果需要更细粒度的锁,可以使用 ReentrantLock 并发重入锁等。
如果需要保证变量的内存可见性,可以使用 volatile 关键字。
对于简单的原子变量操作,还可以使用 Atomic 原子类。
对于线程独立的数据,可以使用 ThreadLocal 来为每个线程提供专属的变量副本。
对于需要并发容器的地方,可以使用 ConcurrentHashMap、CopyOnWriteArrayList 等。
有个int的变量为0,十个线程轮流对其进行++操作(循环10000次),结果大于10 万还是小于等于10万,为什么?
在这个场景中,最终的结果会小于 100000,原因是多线程环境下,++ 操作并不是一个原子操作,而是分为读取、加 1、写回三个步骤。
- 读取变量的值。
- 将读取到的值加 1。
- 将结果写回变量。
这样的话,就会有多个线程读取到相同的值,然后对这个值进行加 1 操作,最终导致结果小于 100000。
详细解释下。
多个线程在并发执行 ++ 操作时,可能出现以下竞态条件:
- 线程 1 读取变量值为 0。
- 线程 2 也读取变量值为 0。
- 线程 1 进行加法运算并将结果 1 写回变量。
- 线程 2 进行加法运算并将结果 1 写回变量,覆盖了线程 1 的结果。
可以通过 synchronized 关键字为 ++ 操作加锁。
class Main {
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
for (int i = 0; i < 10000; i++) {
synchronized (Main.class) {
count++;
}
}
};
List threads = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(task);
threads.add(thread);
thread.start();
}
for (Thread thread : threads) {
thread.join();
}
System.out.println("Final count: " + count);
}
}
或者使用 AtomicInteger 的 incrementAndGet() 方法来替代 ++ 操作,保证变量的原子性。
class Main {
private static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
for (int i = 0; i < 10000; i++) {
count.incrementAndGet();
}
};
List threads = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(task);
threads.add(thread);
thread.start();
}
for (Thread thread : threads) {
thread.join();
}
System.out.println("Final count: " + count.get());
}
}
场景:有一个 key 对应的 value 是一个json 结构,json 当中有好几个子任务,这些子任务如果对 key 进行修改的话,会不会存在线程安全的问题?
会。
在单节点环境中,可以使用 synchronized 关键字或 ReentrantLock 来保证对 key 的修改操作是原子的。
class KeyManager {
private final ReentrantLock lock = new ReentrantLock();
private String key = "{\"tasks\": [\"task1\", \"task2\"]}";
public String readKey() {
lock.lock();
try {
return key;
} finally {
lock.unlock();
}
}
public void updateKey(String newKey) {
lock.lock();
try {
this.key = newKey;
} finally {
lock.unlock();
}
}
}
在多节点环境中,可以使用分布式锁 Redisson 来保证对 key 的修改操作是原子的。
class DistributedKeyManager {
private final RedissonClient redisson;
public DistributedKeyManager() {
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
this.redisson = Redisson.create(config);
}
public void updateKey(String key, String newValue) {
RLock lock = redisson.getLock(key);
lock.lock();
try {
// 模拟读取和更新操作
String currentValue = readFromDatabase(key); // 假设读取 JSON 数据
String updatedValue = modifyJson(currentValue, newValue); // 修改 JSON
writeToDatabase(key, updatedValue); // 写回数据库
} finally {
lock.unlock();
}
}
private String readFromDatabase(String key) {
// 模拟从数据库读取
return "{\"tasks\": [\"task1\", \"task2\"]}";
}
private String modifyJson(String json, String newValue) {
// 使用 JSON 库解析并修改
return json.replace("task1", newValue);
}
private void writeToDatabase(String key, String value) {
// 模拟写回数据库
}
}
说一个线程安全的使用场景?
单例模式。在多线程环境下,如果多个线程同时尝试创建实例,单例类必须确保只创建一个实例,并提供一个全局访问点。
饿汉式是一种比较直接的实现方式,它通过在类加载时就立即初始化单例对象来保证线程安全。
class Singleton {
private static final Singleton instance = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
}
懒汉式单例则在第一次使用时初始化单例对象,这种方式需要使用双重检查锁定来确保线程安全,volatile 关键字用来保证可见性,syncronized 关键字用来保证同步。
class LazySingleton {
private static volatile LazySingleton instance;
private LazySingleton() {}
public static LazySingleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (LazySingleton.class) {
if (instance == null) { // 第二次检查
instance = new LazySingleton();
}
}
}
return instance;
}
}
能说一下 Hashtable 的底层数据结构吗?
与 HashMap 类似,Hashtable 的底层数据结构也是一个数组加上链表的方式,然后通过 synchronized 加锁来保证线程安全。
- Java 面试指南(付费)收录的 360 面经同学 3 Java 后端技术一面面试原题:线程安全,说一个使用场景 -讲了下单例模式的双重检查锁定,懒汉式和饿汉式
- Java 面试指南(付费)收录的快手同学 2 一面面试原题:有个int的变量为0,十个线程轮流对其进行++操作(循环10000次),结果是大于小于还是等于10万,为什么?
- Java 面试指南(付费)收录的TP联洲同学 5 Java 后端一面的原题:怎么保证多线程安全,Hashtable数据结构 底层
- Java 面试指南(付费)收录的百度同学 4 面试原题:线程安全和线程不安全是什么意思?
- Java 面试指南(付费)收录的京东面经同学 1 Java 技术一面面试原题:除了 ThreadLocal,还有什么解决线程安全问题的方法
memo:2025 年 1 月 30 日修改至此。
ThreadLocal
推荐阅读:ThreadLocal 全面解析
12.🌟ThreadLocal 是什么?
ThreadLocal 是一种用于实现线程局部变量的工具类。它允许每个线程都拥有自己的独立副本,从而实现线程隔离。
使用 ThreadLocal 通常分为四步:
①、创建 ThreadLocal
//创建一个ThreadLocal变量
public static ThreadLocal localVariable = new ThreadLocal<>();
②、设置 ThreadLocal 的值
//设置ThreadLocal变量的值
localVariable.set("沉默王二是沙雕");
③、获取 ThreadLocal 的值
//获取ThreadLocal变量的值
String value = localVariable.get();
④、删除 ThreadLocal 的值
//删除ThreadLocal变量的值
localVariable.remove();
在 Web 应用中,可以使用 ThreadLocal 存储用户会话信息,这样每个线程在处理用户请求时都能方便地访问当前用户的会话信息。
在数据库操作中,可以使用 ThreadLocal 存储数据库连接对象,每个线程有自己独立的数据库连接,从而避免了多线程竞争同一数据库连接的问题。
public class DsContextHolder {
private static final ThreadLocal CONTEXT_HOLDER = new InheritableThreadLocal<>();
public static void reset() {
DsNode ds = CONTEXT_HOLDER.get();
if (ds == null) {
return;
}
if (ds.pre != null) {
CONTEXT_HOLDER.set(ds.pre);
} else {
CONTEXT_HOLDER.remove(); // 清除ThreadLocal中的数据
}
}
}
在格式化操作中,例如日期格式化,可以使用 ThreadLocal 存储 SimpleDateFormat 实例,避免多线程共享同一实例导致的线程安全问题。
ThreadLocal 有哪些优点?
每个线程访问的变量副本都是独立的,避免了共享变量引起的线程安全问题。由于 ThreadLocal 实现了变量的线程独占,使得变量不需要同步处理,因此能够避免资源竞争。
ThreadLocal 可用于跨方法、跨类时传递上下文数据,不需要在方法间传递参数。
- Java 面试指南(付费)收录的滴滴同学 2 技术二面的原题:ThreadLocal 有哪些问题,为什么使用线程池会存在复用问题
- Java 面试指南(付费)收录的支付宝面经同学 2 春招技术一面面试原题:讲讲 ThreadLocal?ThreadLocal 被谁引用?
- Java 面试指南(付费)收录的字节跳动面经同学 1 Java 后端技术一面面试原题:ThreadLocal 是什么?ThreadLocal 的实现原理?
- Java 面试指南(付费)收录的快手面经同学 1 部门主站技术部面试原题:请说一下 ThreadLocal 的作用和使用场景?
- Java 面试指南(付费)收录的携程面经同学 1 Java 后端技术一面面试原题:ThreadLocal,(作用,演进,软指针,删除过程)
- Java 面试指南(付费)收录的美团同学 9 一面面试原题:threadlocal的优点?
memo:2025 年 8 月 23 日修改至此,今天有球友在 VIP 群里发贴说拿到了美团背景的转正 offer,真的要恭喜啊,8 月下旬就能转正,太舒服了。
13.你在工作中用到过 ThreadLocal 吗?
有用到过,用来存储用户信息。
技术派实战项目是典型的 MVC 架构,登录后的用户每次访问接口,都会在请求头中携带一个 token,在控制层可以根据这个 token,解析出用户的基本信息。
假如在服务层和持久层也要用到用户信息,就可以在控制层拦截请求把用户信息存入 ThreadLocal。
这样我们在任何一个地方,都可以取出 ThreadLocal 中存的用户信息。
很多其它场景的 cookie、session 等等数据隔离都可以通过 ThreadLocal 去实现。
14.🌟ThreadLocal 怎么实现的呢?
当我们创建一个 ThreadLocal 对象并调用 set 方法时,其实是在当前线程中初始化了一个 ThreadLocalMap。
ThreadLocalMap 是 ThreadLocal 的一个静态内部类,它内部维护了一个 Entry 数组,key 是 ThreadLocal 对象,value 是线程的局部变量,这样就相当于为每个线程维护了一个变量副本。
Entry 继承了 WeakReference,它限定了 key 是一个弱引用,弱引用的好处是当内存不足时,JVM 会回收 ThreadLocal 对象,并且将其对应的 Entry.value 设置为 null,这样可以在很大程度上避免内存泄漏。
static class Entry extends WeakReference> {
/** The value associated with this ThreadLocal. */
Object value;
//节点类
Entry(ThreadLocal> k, Object v) {
//key赋值
super(k);
//value赋值
value = v;
}
}
简版回答:
ThreadLocal 的实现原理是,每个线程维护一个 Map,key 为 ThreadLocal 对象,value 为想要实现线程隔离的对象。
- 1、通过 ThreadLocal 的 set 方法将对象存入 Map 中。
- 2、通过 ThreadLocal 的 get 方法从 Map 中取出对象。
- 3、Map 的大小由 ThreadLocal 对象的多少决定。
memo:2025 年 9 月 18 日修改至此,今天有 27 届的球友拿到了小红书的日常实习 offer,特意来报喜,还称赞了二哥的面渣逆袭和派聪明项目,祝贺!
什么是弱引用,什么是强引用?
我先说一下强引用,比如 User user = new User("沉默王二") 中,user 就是一个强引用,new User("沉默王二") 就是强引用对象。
当 user 被置为 null 时(user = null),new User("沉默王二") 对象就会被垃圾回收;否则即便是内存空间不足,JVM 也不会回收 new User("沉默王二") 这个强引用对象,宁愿抛出 OutOfMemoryError。
弱引用,比如说在使用 ThreadLocal 中,Entry 的 key 就是一个弱引用对象。
ThreadLocal userThreadLocal = new ThreadLocal<>();
userThreadLocal.set(new User("沉默王二"));
userThreadLocal 是一个强引用,new ThreadLocal<>() 是一个强引用对象;
new User("沉默王二") 是一个强引用对象。
调用 set 方法后,会将 key = new ThreadLocal<>() 放入 ThreadLocalMap 中,此时的 key 是一个弱引用对象。当 JVM 进行垃圾回收时,如果发现了弱引用对象,就会将其回收。
其关系链就是:
- ThreadLocal 强引用 -> ThreadLocal 对象。
- Thread 强引用 -> ThreadLocalMap。
ThreadLocalMap[i]强引用了 -> Entry。- Entry.key 弱引用 -> ThreadLocal 对象。
- Entry.value 强引用 -> 线程的局部变量对象。
- Java 面试指南(付费)收录的支付宝面经同学 2 春招技术一面面试原题:讲讲 ThreadLocal?ThreadLocal 被谁引用?
- Java 面试指南(付费)收录的字节跳动面经同学 1 Java 后端技术一面面试原题:ThreadLocal 是什么?ThreadLocal 的实现原理?
- Java 面试指南(付费)收录的京东面经同学 1 Java 技术一面面试原题:ThreadLocal 原理,解决什么问题
- Java 面试指南(付费)收录的携程面经同学 1 Java 后端技术一面面试原题:ThreadLocal,(作用,演进,软指针,删除过程)
- Java 面试指南(付费)收录的虾皮面经同学 13 一面面试原题:threadlocal 原理 怎么避免垃圾回收?
memo:2025 年 8 月 23 日修改至此。今天又有球友在 VIP 群里报喜说拿到了美团的转正 offer,并且直言:暑期真的可以冲团子,转正率高达百分百。
15.🌟ThreadLocal 内存泄露是怎么回事?
ThreadLocalMap 的 Key 是 弱引用,但 Value 是强引用。
如果一个线程一直在运行,并且 value 一直指向某个强引用对象,那么这个对象就不会被回收,从而导致内存泄漏。
那怎么解决内存泄漏问题呢?
很简单,使用完 ThreadLocal 后,及时调用 remove() 方法释放内存空间。
try {
threadLocal.set(value);
// 执行业务操作
} finally {
threadLocal.remove(); // 确保能够执行清理
}
remove() 会调用 ThreadLocalMap 的 remove 方法遍历哈希表,找到 key 等于当前 ThreadLocal 的 Entry,找到后会调用 Entry 的 clear 方法,将 Entry 的 value 设置为 null。
private void remove(ThreadLocal> key) {
Entry[] tab = table;
int len = tab.length;
// 计算 key 的 hash 值
int i = key.threadLocalHashCode & (len-1);
// 遍历数组,找到 key 为 null 的 Entry
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
// 将该 Entry 的 key 置为 null(即 Entry 失效)
e.clear();
// 清理过期的 entry
expungeStaleEntry(i);
return;
}
}
}
public void clear() {
this.referent = null;
}
然后执行 expungeStaleEntry() 方法,清除 key 为 null 的 Entry。
你每次操作都会remove吗?
我不是每次操作都 remove,主要是根据使用场景来决定的。在一些短生命周期的场景中,比如处理单个 HTTP 请求的上下文信息,我通常会在请求结束时统一 remove。
public class ReqInfoContext {
private static TransmittableThreadLocal contexts = new TransmittableThreadLocal<>();
public static void addReqInfo(ReqInfo reqInfo) {
contexts.set(reqInfo);
}
public static void clear() {
contexts.remove(); // 清除ThreadLocal中的数据
}
public static ReqInfo getReqInfo() {
return contexts.get();
}
}
但在一些需要跨多个方法调用保持状态的场景中,就不会每次都 remove。
我的使用原则是:
- 在方法级别使用时,try-finally 保证 remove
- 在请求级别使用时,通过拦截器或 Filter 统一清理
- 如果存储的对象比较大,使用完立即 remove
- 定期检查 ThreadLocal 的使用情况,避免遗漏
那为什么 key 要设计成弱引用?
弱引用的好处是,当内存不足的时候,JVM 能够及时回收掉弱引用的对象。
比如说:
WeakReference key = new WeakReference(new ThreadLocal());
key 是弱引用,new WeakReference(new ThreadLocal()) 是弱引用对象,当 JVM 进行垃圾回收时,只要发现了弱引用对象,就会将其回收。
一旦 key 被回收,ThreadLocalMap 在进行 set、get 的时候就会对 key 为 null 的 Entry 进行清理。
总结一下,在 ThreadLocal 被垃圾收集后,下一次访问 ThreadLocalMap 时,Java 会自动清理那些键为 null 的 entry,这个过程会在执行 get()、set()、remove()时触发。
你了解哪些 ThreadLocal 的改进方案?
在 JDK 20 Early-Access Build 28 版本中,出现了 ThreadLocal 的改进方案,即 ScopedValue。
还有 Netty 中的 FastThreadLocal,它是 Netty 对 ThreadLocal 的优化,内部维护了一个索引常量 index,每次创建 FastThreadLocal 中都会自动+1,用来取代 hash 冲突带来的损耗,用空间换时间。
private final int index;
public FastThreadLocal() {
index = InternalThreadLocalMap.nextVariableIndex();
}
public static int nextVariableIndex() {
int index = nextIndex.getAndIncrement();
if (index < 0) {
nextIndex.decrementAndGet();
}
return index;
}
以及阿里的 TransmittableThreadLocal,不仅实现了子线程可以继承父线程 ThreadLocal 的功能,并且还可以跨线程池传递值。
TransmittableThreadLocal context = new TransmittableThreadLocal<>();
// 在父线程中设置
context.set("value-set-in-parent");
// 在子线程中可以读取,值是"value-set-in-parent"
String value = context.get();
- Java 面试指南(付费)收录的滴滴同学 2 技术二面的原题:ThreadLocal 有哪些问题,为什么使用线程池会存在复用问题
- Java 面试指南(付费)收录的腾讯面经同学 22 暑期实习一面面试原题:ThreadLocal 什么情况下会内存泄漏
- Java 面试指南(付费)收录的字节跳动面经同学 1 Java 后端技术一面面试原题:使用 ThreadLocal 有什么问题吗?如何解决?
- Java 面试指南(付费)收录的快手面经同学 1 部门主站技术部面试原题:ThreadLocal 有什么缺陷?你了解哪些 ThreadLocal 的改进方案?
- Java 面试指南(付费)收录的携程面经同学 1 Java 后端技术一面面试原题:ThreadLocal,(作用,演进,软指针,删除过程)
- Java 面试指南(付费)收录的美团同学 9 一面面试原题:threadlocal他会出现什么问题?出现内存泄漏怎么解决?
memo:2025 年 08 月 23 日修改至此。今天在帮球友修改简历的时候,收到这样一个反馈:1 月份加入星球,3 月份拿到一个小日常,后面又拿到了蚂蚁的暑期,周三答辩,转正没什么问题。真的非常感谢球友们的正反馈,这是我一路坚持下去的最强动力。
16.ThreadLocalMap 的源码看过吗?
有研究过。
ThreadLocalMap 虽然被叫做 Map,但它并没有实现 Map 接口,是一个简单的线性探测哈希表。
static class ThreadLocalMap {
static class Entry extends WeakReference> {
Object value;
Entry(ThreadLocal> k, Object v) {
super(k); // 这里的 Key 是 WeakReference
value = v;
}
}
private Entry[] table; // 存储 ThreadLocal 变量的数组
private int size; // 当前 Entry 数量
private int threshold; // 触发扩容的阈值
}
底层的数据结构也是数组,数组中的每个元素是一个 Entry 对象,Entry 对象继承了 WeakReference,key 是 ThreadLocal 对象,value 是线程的局部变量。
当调用 ThreadLocal.set(value) 时,会将 value 存入 ThreadLocalMap。
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}
set() 方法是 ThreadLocalMap 的核心方法,通过 key 的哈希码与数组长度取模,计算出 key 在数组中的位置,这一点和 HashMap 的实现类似。
private void set(ThreadLocal> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len - 1); // 计算索引
for (Entry e = tab[i]; e != null; e = tab[nextIndex(i, len)]) {
ThreadLocal> k = e.get();
if (k == key) { // 如果 key 已存在,更新 value
e.value = value;
return;
}
if (k == null) { // Key 为 null,清理无效 Entry
replaceStaleEntry(key, value, i);
return;
}
...
热门评论
11 条评论
回复