本篇主要是列举一些线程与进程理论的基础知识。
1. 并发 vs 并行
- 并发 (Concurrency) :在同一处理单元(如单核 CPU)上,通过时间片轮转等方式“看似同时”执行多个任务,强调对时间的共享。
- 并行 (Parallelism) :在不同处理单元(如多核 CPU 或多台服务器)上真正同时执行多个任务,强调空间上的同时性。
2. Java 进程与线程模型
- 进程:操作系统分配资源(内存、文件句柄等)的基本单位。Java 程序启动时即是一个进程。
- 线程:进程内部执行任务的最小单位,一个 Java 进程可以包含多个线程;主线程之外,JVM 还会启动若干后台线程,如垃圾回收线程、Finalizer 线程等。
示例:不到 9 行代码即可列出 JVM 中全部线程:
public class OnlyMain {
public static void main(String[] args) {
ThreadMXBean tmxb = ManagementFactory.getThreadMXBean();
ThreadInfo[] infos = tmxb.dumpAllThreads(false, false);
for (ThreadInfo ti : infos) {
System.out.println("[" + ti.getThreadId() + "] " + ti.getThreadName());
}
}
}

finalize()方法由 Finalizer 线程触发。如主线程已退出而 JVM 早于 Finalizer 线程退出,则可能永远不执行。
3. 启动线程的两种方式
-
继承
Thread类:class MyThread extends Thread { public void run() { /* 任务逻辑 */ } } new MyThread().start(); -
实现
Runnable接口:class MyTask implements Runnable { public void run() { /* 任务逻辑 */ } } new Thread(new MyTask()).start();
4. 线程中断与停止
1. stop() 方法的风险
- 已废弃:
Thread.stop()会强制终止线程,不会执行正常的清理逻辑(如 finally 块、释放锁、关闭资源等),容易导致数据不一致、死锁等不可预测后果。 - 仅限特殊场景:只有在你完全确认强制终止不会带来资源泄漏或状态破坏时,才可使用——且最好封装在高度受控的环境中。
2. interrupt() 的设计理念
-
协作式中断:调用
thread.interrupt(),向目标线程发出“请中断我”的请求,由线程自己决定何时、如何响应。 -
中断状态标志:
- 调用
interrupt()会将目标线程的中断标志(interrupt flag)置为true。 - 当线程处于
sleep()、wait()、join()、BlockingQueue.take()等阻塞状态时,抛出InterruptedException,并且会清除中断标志(置为false)。 - 如果想保持标志为
true,可在捕获InterruptedException后再次调用interrupt()。
- 调用
3. 判断中断状态:isInterrupted() vs. interrupted()
| 方法 | 作用 | 清除标志? |
|---|---|---|
boolean isInterrupted() | 检查当前线程或指定线程的中断状态,不修改标志 | 否 |
static boolean interrupted() | 检查当前线程的中断状态,并清除该标志(置为 false) | 是 |
-
使用建议:
- 在循环中定期调用
isInterrupted(),优雅地退出。 - 若需探测并立即清除中断状态,可用
interrupted()。
- 在循环中定期调用
4. 为什么设计为协作式?
- 资源回收:线程中途被唤醒、重新获得执行权时,如果仍保持中断标志,可能在未释放资源(如锁、IO 流)时被直接终止,造成浪费或死锁;让线程自己捕获
InterruptedException并清理后再退出,更安全。

5. 与 Future 的结合
- 使用
FutureTask、ExecutorService提交可取消任务时,future.cancel(true)会给底层线程发送中断。任务内部若响应中断,就能提前返回;否则会等正常结束。
因此:
-
不推荐使用
stop()、suspend()、resume()等强制性停止方法,因可能导致资源释放异常。 -
推荐使用
interrupt()方法,采用协作式中断:线程自行检查中断标志并优雅终止。isInterrupted():检查并保留中断状态;interrupted():检查并清除中断状态。
while(!isInterrupted()) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName()
+" in InterruptedException interrupt flag is "
+isInterrupted());
// 在这里做资源释放之后再改标识位。
interrupt();
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ " I am extends Thread.");
}
5. Runnable 中断示例
private static class UseRunnable implements Runnable{
@Override
public void run() {
// 中断一个线程。
while(!Thread.currentThread().isInterrupted()) {
System.out.println(Thread.currentThread().getName()
+ " I am implements Runnable.");
}
System.out.println(Thread.currentThread().getName()
+" interrupt flag is "+Thread.currentThread().isInterrupted());
}
}
6. 启动线程:start() vs. run()
-
只能调用一次:对同一个
Thread对象,连续两次调用start()会抛出IllegalThreadStateException,因为线程只能被启动一次。 -
start()- 在 JVM 中为线程分配资源,创建新线程并执行
run()方法。 - 启动后,JVM 调度该线程与其他线程并发运行。
- 在 JVM 中为线程分配资源,创建新线程并执行
-
run()- 只是普通方法调用,会在当前(调用者)线程中同步执行,不会创建新线程。
- 可重复调用,无异常,但失去多线程效果。
public class MyThread extends Thread {
private Runnable target;
public MyThread(Runnable target) { this.target = target; }
@Override
public void run() {
if (target != null) {
// 直接调用 target.run(),是在当前线程中执行
target.run();
}
}
}
7. 线程的六种状态
- NEW:对象创建后,尚未调用
start() - RUNNABLE:可运行状态,JVM 线程调度器可选择执行
- BLOCKED:等待获取对象监视器(
synchronized锁) - WAITING:调用
Object.wait()、Thread.join()(无超时)或LockSupport.park()后进入 - TIMED_WAITING:调用带超时的
wait(timeout)、sleep(timeout)、join(timeout)等后进入 - TERMINATED:
run()方法执行完毕或因异常退出
8. join() 方法
-
作用:让调用线程等待被
join()的线程执行完毕后再继续。 -
调用关系如同“栈” :
// A 线程: tB.start(); tB.join(); // A 等待 B 结束 // 如果 B 内部又调用了 tC.join(),则 B 也要等 C 结束 -
典型用法:同步多个线程完成其任务,再统一进行后续处理。
9. 线程优先级
- Java 中线程优先级范围:
MIN_PRIORITY (1)~MAX_PRIORITY (10),默认NORM_PRIORITY (5)。 - 实际效果有限:操作系统层面的线程调度策略各异,优先级只是一个建议,对执行顺序影响不大,不能用作精确控制。
10. 用户线程 vs. 守护线程
-
默认:
main线程 & 通过new Thread()启动的线程,均为 用户线程(非守护)。- JVM 进程在所有用户线程结束前一直存活。
-
守护线程:
- 可通过
thread.setDaemon(true)在start()之前设置。 - JVM 仅在无任何用户线程存活时,才会退出,此时所有守护线程会被强制终止。
- 注意:守护线程的
finally、关闭资源等钩子不一定执行(取决于 JVM 何时、是否给它分配 CPU 时间片)。
- 可通过
Thread daemon = new Thread(() -> {
try {
while (!Thread.currentThread().isInterrupted()) {
// 守护线程工作
}
} finally {
// 不可靠:可能打印不到
System.out.println("守护线程 finally");
}
});
daemon.setDaemon(true);
daemon.start();
11. 内置锁:synchronized
1. 锁的类型
-
对象锁(实例锁)
synchronized(this) { … } -
类锁(静态锁)
synchronized(MyClass.class) { … } // 或在 static 方法上加 synchronized public static synchronized void foo() { … }
2. 实现原理
-
JVM 在字节码中插入两条指令:
monitorenter(加锁)monitorexit(解锁)
-
如果在方法上使用
synchronized,Class 文件会带有ACC_SYNCHRONIZED标志,JVM 运行时同样会补入上述两条指令。
3. 对象头与 Monitor
-
对象头(Mark Word) 存储锁状态:
- 无锁、偏向锁、轻量级锁、重量级锁
- 持有线程 ID、重入计数、指向栈上锁记录等信息
-
Monitor 对象:每个可锁对象在 JVM 内部对应一个 Monitor,用于维护锁的竞争与线程挂起/唤醒。
4. 锁的升级路径
- 偏向锁:单线程无竞争时最快,记录线程 ID 重入不走系统调用
- 轻量级锁:出现短时竞争后,使用用户态 CAS 操作在栈帧中记录锁记录
- 重量级锁:严重竞争时膨胀到操作系统互斥量,才真正挂起线程(上下文切换成本高)
5. 性能考量
-
上下文切换:挂起 + 恢复大约 3–5 ms
-
优化建议:
- 临界区短、竞争少:偏向锁或轻量级锁即可
- 高并发竞争:可考虑更细粒度的锁或非阻塞算法
12. 轻量级可见性:volatile
1. 语义保障
- 可见性:写入
volatile变量后,立即刷新到主内存;其他线程读时能看到最新值。 - 内存屏障:禁止读/写
volatile前后的指令重排序。
2. 局限性
- 不保证原子性:
volatile++仍为 “读-改-写” 三步,不可替代锁。 - 只能做状态标志:适合“一写多读”、双重检查锁定(DCL)中的第一层检查。
3. 典型应用场景
- 启动/停止旗标
- 单例中的双重检查
- 轻量级状态同步,无需复杂互斥
三、选型 & 对比
| 特性 | synchronized | volatile |
|---|---|---|
| 可见性 | 有(自带) | 有 |
| 原子性 | 保证整个同步块内操作的原子性 | 不保证,仅针对单次读/写 |
| 重排序 | 禁止重排同步块前后指令 | 禁止重排读/写前后指令 |
| 性能开销 | 存在锁竞争成本,低竞争下可优化为偏向/轻量级 | 几乎无锁开销 |
| 适合场景 | 复合操作、临界区保护 | 简单标志、状态通知、一写多读 |
13. ThreadLocal:线程局部变量
13.1 概念
- 每个线程都维护一份独立的变量副本,互不干扰,避免了显式同步。
- 典型用法:为不同线程存储各自的上下文或中间结果,无需加锁即可安全读写。
结构说明
-
每个线程(Thread 对象)内部有且仅有 ThreadLocalMap
- 这个 ThreadLocalMap 用来存放“所有属于该线程的 ThreadLocal 变量(作为 key)及其对应的副本值(作为 value)”。 因为一个线程可以用多个 ThreadLocal,所以 ThreadLocalMap 底层采用数组(Entry[])+哈希寻址来存储这些不同的 ThreadLocal → value 的键值对。
Thread └─ ThreadLocalMap ←—— 只有一个 ├─ Entry: [ThreadLocalA] → valueA ├─ Entry: [ThreadLocalB] → valueB └─ ...(可以有很多 Entry,但 Map 只有一个) ``` -
ThreadLocalMap 的 key/value
- key:是 ThreadLocal 对象本身(注意,是弱引用)
- value:是这个 ThreadLocal 变量在该线程中的副本值(强引用)
-
对应关系
-
一条链:
线程对象 Thread └─ ThreadLocalMap (作为 Thread 的成员变量) ├─ Entry (key=ThreadLocal 弱引用, value=你的副本对象) ├─ Entry (key=ThreadLocal2, value=副本2) └─ ...
-
-
多线程时
- 每个线程有自己的 ThreadLocalMap
- 不同线程的 ThreadLocalMap 里,都可能有同一个 ThreadLocal 变量作为 key,但 value 各自独立
一个 ThreadLocal 变量在不同线程中“看起来像是同一个对象”,但每个线程访问时,底层其实是到自己私有的 ThreadLocalMap 里用 ThreadLocal 作为 key 找 value。这样各线程互不干扰,线程安全。
为什么不是“一个 ThreadLocal 放所有副本”?
- 每个 ThreadLocal 负责管理一个变量的线程隔离副本
- ThreadLocalMap 的 key 是 ThreadLocal,value 是副本值
- 你有几个不同类型/用途的变量需要线程隔离,就该有几个 ThreadLocal
因此,得到结论:
-
一个线程隔离变量 → 一个 ThreadLocal 实例
-
同一个 ThreadLocal 可以被多个线程用,但每个线程有自己独立的数据副本
-
ThreadLocalMap 自动管理所有 ThreadLocal 和它们的副本
举例说明,如果你有 3 个需要线程隔离的变量,比如 userId、sessionId、formatter,那么每个都要用一个独立的 ThreadLocal:
ThreadLocal<String> userIdLocal = new ThreadLocal<>();
ThreadLocal<String> sessionIdLocal = new ThreadLocal<>();
ThreadLocal<SimpleDateFormat> fmtLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
每个线程通过 userIdLocal.get()、sessionIdLocal.get()、fmtLocal.get() 拿到的,都是属于自己的副本,互相之间没有任何影响。这正是 ThreadLocal 设计的初衷:
- 线程 A 操作自己的 userIdLocal、sessionIdLocal、fmtLocal,得到的只是属于线程 A 的值
- 线程 B 操作同样的三个 ThreadLocal,也只能访问自己的副本,和线程 A 的副本完全隔离
总结一下这个模式的核心点:
- 你有多少个需要线程隔离的变量,就声明多少个 ThreadLocal 实例
- 每个线程内部通过 ThreadLocal 实例的
get()或set()访问的,都是自己的副本 - 线程之间不会串数据、不会互相影响,也不需要同步
13.2 实现机制
-
Thread 对象 内部持有一个
ThreadLocalMap threadLocals。 -
ThreadLocalMap
// ThreadLocalMap 的内部结构 static class ThreadLocalMap { /** 存储条目:键是弱引用的 ThreadLocal 对象 */ static class Entry extends WeakReference<ThreadLocal<?>> { Object value; // 对应线程副本的值 Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } /** 用于存放 Entry 的数组,初始长度为 16 */ private Entry[] table; // …… } -
工作流程
- 第一次
get()时,会调用initialValue()并将结果封装为Entry放入table。 - 后续
get/set均在该 map 中查找同一个ThreadLocal键来操作对应的value。
- 第一次
13.3 为什么用数组?
- Open-addressing 哈希:避免链表结构的额外对象开销,快速定位和冲突解决。
- 动态扩容:当负载因子过高时扩大数组长度,保持查找效率。
- 多 ThreadLocal 支持:每个线程可创建任意多个 ThreadLocal,统一放在一张表里管理。
13.4 内存泄漏风险
- 弱引用键:
Entry.key(ThreadLocal 对象)为弱引用,ThreadLocal 对象被 GC 后,key变为null,但value不会自动回收,成为“僵尸条目”。 - GC Root:线程(Thread 对象)仍持有对
ThreadLocalMap的强引用,僵尸Entry的value只能等到线程结束才清理。 - 结果:长期运行的线程中,反复创建并丢弃 ThreadLocal 会导致
ThreadLocalMap内存不断被“泄漏”,直到线程销毁。
13.5 键为何用弱引用?
- 避免强引用阻止 ThreadLocal 对象回收:若用强引用,ThreadLocal 一旦不再使用,map 中的键永远无法被回收,更早出现泄漏。
- 弱引用机制:只要用户不再持有 ThreadLocal 变量,GC 即可清除对应的键,触发
expungeStaleEntry()清理僵尸条目。
13.6 使用建议
- 显式清理:用完后调用
threadLocal.remove(),及时移除对应Entry,避免僵尸残留。 - 慎用匿名/临时 ThreadLocal:推荐把 ThreadLocal 作为类成员,并在不再需要时 remove。
- 避免在线程池中遗漏清理:线程复用时,一定要在任务结束前清除,否则下一任务会看到前任务残留值。
13.7 常见误用示例
// 错误示例:直接用 static 共享对象,完全没用 ThreadLocal
public static Number number = new Number(0);
- 后果:所有线程都操作同一个对象,毫无隔离性,反而比共享变量更难管控。
正确做法:
private static final ThreadLocal<Number> threadNumber =
ThreadLocal.withInitial(() -> 0);
// 每个线程调用时都独享一份副本
Number n = threadNumber.get();
13.8 TheadLocal的内存泄漏
ThreadLocal 的内存泄漏是指:你 set 进去的 value(比如各种对象),由于没有及时 remove,导致即使 ThreadLocal 被 GC 回收,value 依然被线程持有,无法被 GC,造成内存一直被占用。
典型引用链(GC Root)如下:
GC Root (如 main thread, 线程池线程)
│
↓
Thread (线程对象)
│
↓
ThreadLocalMap (Thread 的成员)
│
↓
Entry (数组中的某一项)
│
├─ key: WeakReference<ThreadLocal> → null (已经被GC)
└─ value: Object (你的副本对象) —— 强引用(value 是被当前线程(Thread 对象)强引用着)!!!
只要 Thread 没退出,ThreadLocalMap 还在,Entry 的 value 还在,被强引用,无法回收。用完 ThreadLocal 变量后记得 remove(),让 Entry.value 可以被回收!
总结:ThreadLocal 通过“每线程一份表 + 弱引用键”实现隔离,但也带来内存泄漏风险;务必配合
remove()使用,并在设计阶段规划好生命周期管理。
14. 低级线程协作机制
1. wait / notify / notifyAll
-
必须在
synchronized(obj){…}内调用,否则运行时抛IllegalMonitorStateException。 -
标准范式
// 等待线程 synchronized(lock) { while (!condition) { lock.wait(); } // 处理逻辑 } // 通知线程 synchronized(lock) { // 修改 condition lock.notify(); // 随机唤醒一个等待线程 // 或 lock.notifyAll(); // 唤醒所有等待线程 } -
锁的行为
wait():释放当前对象锁,进入等待队列;notify()/notifyAll():仅把线程从等待队列移到“可运行”,不立即释放锁;直到退出同步块后才释放。
2. yield() / sleep() / wait() 对锁的影响
| 方法 | 释放锁? | 作用说明 |
|---|---|---|
Thread.yield() | 否 | 暂停当前线程,让调度器选择其他线程运行;不保证一定让出。 |
Thread.sleep(ms) | 否 | 挂起当前线程指定时间;不释放任何锁。 |
obj.wait() | 是 | 释放 obj 锁并等待;被唤醒后需重新竞争锁。 |
3. Fork/Join
- 核心思想:将大任务递归拆分为多个子任务(fork),并在完成后汇总结果(join)。
- API:
ForkJoinPool+RecursiveTask<V>/RecursiveAction - 适用场景:可分解且合并成本低的大规模并行计算,如数组归并排序、矩阵乘法等。
4. CountDownLatch
-
原理:维护一个计数器,初始化时指定;每次调用
countDown()减一;当计数器归零时,所有await()的线程被唤醒。 -
特点
- 计数器与线程数无须一一对应,可大于或小于线程数。
- 一次性:计数器归零后无法重置(需重建实例)。
-
典型用法:主线程等待若干“准备”或“完成”事件,再统一执行。
5. CyclicBarrier
-
原理:一组线程互相等待,直到到达屏障点才一起继续。
-
特点
- 构造时指定参与线程数,线程必须一一对应。
- 可携带
barrierAction,在所有线程到齐时执行一次汇总/合并操作。 - 可重用:通过
reset()或再次调用await()实现循环屏障。

cyclicBarrier.await()可以被反复调用。也就是可以让几个线程反复的在屏障处汇总等。
countdownLatch只能降低,变成0之后,就不能再触发了。
Countdownlatch的协调执行需要外边的线程来执行,而CyclicBarrier是工作线程本身相互协调的。, 这也导致了 Countdownlatch 的计数器与线程并不需要由很强烈的数量绑定。而CyclicBarrier 线程数密切相关的。Countdownlatch在其他线程跑起来之后,没有提供api来对结果进行汇总。而CyclicBarrier提供了这个能力。就是利用barrierAction.
总结:
- CountDownLatch适用于一个或多个线程等待其他线程完成某些操作的场景,使用计数器来进行协调。一旦计数器归零,等待的线程就可以继续执行。
- CyclicBarrier适用于多个线程之间相互等待,直到所有线程都准备就绪才一起继续执行的场景,使用屏障点来进行协调。所有线程必须互相等待,共同超过这个屏障点。
通过这种方式,CountDownLatch是外部控制线程协调执行(一组线程等待另一组线程),而CyclicBarrier是由参与的工作线程本身相互协调以同步地前进。
6. Semaphore
-
原理:维护一组“许可证”(permits),
acquire()请求一个许可证,release()归还。 -
特点
- 可控制并发访问量,例如数据库连接池。
- 注意:直接调用
release()可人为“造”许可证,破坏初始限制。
new Semaphore(10);
如果不调用aquire,直接调用release的话,初始化设置的permits就没有太大的意义了,因为Semephore允许我们凭空造出许可证往里边放。

7. Exchanger
- 原理:两线程在同步点相遇时交换数据:
exchange(V my)。 - 特点:仅支持两方,互相等待对方到达并交换;可用于双向缓冲区切换场景。
8. Runnable / Callable / FutureTask / Future
-
Runnable:无返回值,只能
new Thread(r).start()。 -
Callable<V :带返回值,可抛检查型异常;需通过 FutureTask<V 或
ExecutorService提交。 -
FutureTask<V :
- 实现了 RunnableFuture<V(既是 `Runnable```,又是 Future<V)。
- 可直接作为任务交给线程,也可用
future.get()获取结果或cancel(true)中断任务。
-
Future<V :接口,表示异步计算结果,可用来取消任务、查询是否完成、获取结果。
// 示例:用 FutureTask 包装 Callable,并获取/取消任务
UseCallable task = new UseCallable();
FutureTask<Integer> future = new FutureTask<>(task);
new Thread(future).start();
Thread.sleep(1000);
if (new Random().nextBoolean()) {
System.out.println("结果 = " + future.get());
} else {
future.cancel(true);
System.out.println("已取消");
}

FutrueTask实现了RunnableRuture,FutureTask即可以作为Runable交个线程执行,也可以获取执行的结果。Future是做什么用的?对任务可以获取结果,并且可以取消任务的接口。
示例代码:
/**
* 实现Callable接口,允许有返回值
*/
private static class UseCallable implements Callable<Integer> {
private int sum;
@Override
public Integer call() throws Exception {
System.out.println("Callable子线程开始计算!");
Thread.sleep(2000);
for (int i = 0; i < 5000; i++) {
sum = sum + i;
}
System.out.println("Callable子线程计算结束!结果为: " + sum);
return sum;
}
}
public static void main(String[] args) throws InterruptedException, ExecutionException {
UseCallable useCallable = new UseCallable();
// 用FutureTask包装Callable
FutureTask<Integer> futureTask = new FutureTask<>(useCallable);
// 交给Thread去运行
new Thread(futureTask).start();
Random r = new Random();
Thread.sleep(1000);
// 用随机的方式决定是获得结果还是终止任务
if(r.nextBoolean()) {
System.out.println("Get UseCallable result = "+futureTask.get());
}else {
System.out.println("中断计算。 ");
futureTask.cancel(true);
}
}
学后检测
选择题
1. 并发和并行的本质区别在于?
A. 并发侧重多个处理器同时执行, 并行侧重一个处理器切换任务
B. 并发是逻辑上的同时发生,并行是物理上的同时发生
C. 并发效率更高
D. 并行必须依赖多线程
答案:B
解析:并发强调“看起来同时”——实际上是同一处理器快速切换;并行是真正同时发生,依赖多处理器/核心。
2. 下列关于 Java 线程创建方式的描述,正确的是?
A. 只能通过 Thread 子类创建线程
B. 只能通过 Runnable 接口创建线程
C. 线程只能用 Thread/Runnable 两种抽象方式创建
D. Callable 是线程创建的第三种方式
答案:C
解析:Thread 代表线程本身,Runnable 代表任务本身,Callable 只是为任务返回值服务,不是线程抽象。
3. 调用 Thread.stop() 的主要安全风险是?
A. 线程会被无限挂起
B. 线程会导致 CPU 飙高
C. 线程资源无法安全释放,可能导致系统不一致
D. stop 方法并不会终止线程
答案:C
解析:stop 强制终止线程,不会释放资源和锁,极易引发死锁、数据不一致等隐患。
4. isInterrupted() 和 interrupted() 的区别是?
A. 两者功能完全相同
B. isInterrupted 查询并重置中断标志,interrupted 只查询
C. isInterrupted 只查询,interrupted 查询并重置中断标志
D. 两者都能清除中断标志
答案:C
解析:isInterrupted() 不会修改标志位,interrupted() 会查询并清除。
5. 下列关于 ThreadLocal 的说法,错误的是?
A. 每个线程持有自己独立的变量副本
B. ThreadLocalMap 的 key 是强引用
C. ThreadLocal 适合做线程隔离
D. ThreadLocal 需要注意及时 remove,避免内存泄漏
答案:B
解析:ThreadLocalMap 的 key 是弱引用(WeakReference)。
6. 在 synchronized 锁对象时,JVM 会使用哪个机制来实现锁定?
A. 偏向锁
B. Monitor 对象
C. ReentrantLock
D. 信号量
答案:B
解析:无论是对象锁还是类锁,底层都是依靠 Monitor 实现的。
判断题
7. Java 线程的 sleep() 方法会释放锁。( )
答案:错
解析:sleep 只是让线程暂停,不会释放已持有的锁。
8. 守护线程一定会执行 finally 代码块。( )
答案:错
解析:守护线程可能被强制结束,finally 不一定有机会执行。
9. 多个线程调用同一个 CountDownLatch 实例的 countDown() 方法,计数器归零后,所有 await() 的线程会被唤醒。( )
答案:对
解析:CountDownLatch 本质是“等计数归零,一起放行”。
10. CyclicBarrier 必须要有 barrierAction 才能用。( )
答案:错
解析:barrierAction 只是可选的同步后统一操作,核心作用是线程互相等待。
简答题
11. ThreadLocal 为什么容易造成内存泄漏?如何避免?
参考答案:
ThreadLocalMap 的 key 是弱引用,value 是强引用,如果 ThreadLocal 对象被回收,key 变为 null,但 value 仍被线程强引用,直到线程结束。只要线程不销毁(比如线程池复用),value 就泄漏了。
避免方式:每次用完 ThreadLocal 后,调用 remove() 主动清除。
12. 解释 synchronized 锁对象和锁方法的字节码层面差别。
参考答案:
锁对象时,编译后的字节码会出现 monitorenter/monitorexit 指令;锁方法时,字节码增加 ACC_SYNCHRONIZED 标志,但 monitorenter/monitorexit 由 JVM 运行期插入。
13. 简述 CountDownLatch 与 CyclicBarrier 的核心区别。
参考答案:
CountDownLatch 适合“一组线程等待另一组线程”的场景,计数器归零一次性放行,不能重复用;CyclicBarrier 适合“多线程彼此等待同步推进”,屏障点可重复使用,线程数和屏障点绑定。CyclicBarrier 支持 barrierAction 汇总操作,CountDownLatch 没有。
编程题
14. 写出一个用 Runnable 实现线程中断的安全样例(中断时安全释放资源)。
参考代码:
class SafeInterruptRunnable implements Runnable {
@Override
public void run() {
try {
while (!Thread.currentThread().isInterrupted()) {
// 模拟工作
Thread.sleep(100);
System.out.println("Running...");
}
} catch (InterruptedException e) {
// 响应中断
System.out.println("Interrupted! Cleaning up...");
} finally {
// 释放资源
System.out.println("Cleanup done.");
}
}
}
15. 假设有三个线程需要一起出发处理任务,必须等到都准备好后才能同步开始,请用 CyclicBarrier 写伪代码。
参考代码:
CyclicBarrier barrier = new CyclicBarrier(3);
for (int i = 0; i < 3; i++) {
new Thread(() -> {
// 线程准备工作
System.out.println(Thread.currentThread().getName() + " ready");
barrier.await();
// 同步出发
System.out.println(Thread.currentThread().getName() + " go!");
}).start();
}