线程与进程理论知识基础

473 阅读20分钟

本篇主要是列举一些线程与进程理论的基础知识。

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());
        }
    }
}

image.png

finalize() 方法由 Finalizer 线程触发。如主线程已退出而 JVM 早于 Finalizer 线程退出,则可能永远不执行。

3. 启动线程的两种方式

  1. 继承 Thread 类:

    class MyThread extends Thread {
        public void run() { /* 任务逻辑 */ }
    }
    new MyThread().start();
    
  2. 实现 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 并清理后再退出,更安全。

image.png

5. 与 Future 的结合

  • 使用 FutureTaskExecutorService 提交可取消任务时,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 调度该线程与其他线程并发运行。
  • 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. 线程的六种状态

  1. NEW:对象创建后,尚未调用 start()
  2. RUNNABLE:可运行状态,JVM 线程调度器可选择执行
  3. BLOCKED:等待获取对象监视器(synchronized 锁)
  4. WAITING:调用 Object.wait()Thread.join()(无超时)或 LockSupport.park() 后进入
  5. TIMED_WAITING:调用带超时的 wait(timeout)sleep(timeout)join(timeout) 等后进入
  6. TERMINATEDrun() 方法执行完毕或因异常退出

线程六种状态示意图


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. 锁的升级路径

  1. 偏向锁:单线程无竞争时最快,记录线程 ID 重入不走系统调用
  2. 轻量级锁:出现短时竞争后,使用用户态 CAS 操作在栈帧中记录锁记录
  3. 重量级锁:严重竞争时膨胀到操作系统互斥量,才真正挂起线程(上下文切换成本高)

5. 性能考量

  • 上下文切换:挂起 + 恢复大约 3–5 ms

  • 优化建议

    • 临界区短、竞争少:偏向锁或轻量级锁即可
    • 高并发竞争:可考虑更细粒度的锁或非阻塞算法

12. 轻量级可见性:volatile

1. 语义保障

  • 可见性:写入 volatile 变量后,立即刷新到主内存;其他线程读时能看到最新值。
  • 内存屏障:禁止读/写 volatile 前后的指令重排序。

2. 局限性

  • 不保证原子性volatile++ 仍为 “读-改-写” 三步,不可替代锁。
  • 只能做状态标志:适合“一写多读”、双重检查锁定(DCL)中的第一层检查。

3. 典型应用场景

  • 启动/停止旗标
  • 单例中的双重检查
  • 轻量级状态同步,无需复杂互斥

三、选型 & 对比

特性synchronizedvolatile
可见性有(自带)
原子性保证整个同步块内操作的原子性不保证,仅针对单次读/写
重排序禁止重排同步块前后指令禁止重排读/写前后指令
性能开销存在锁竞争成本,低竞争下可优化为偏向/轻量级几乎无锁开销
适合场景复合操作、临界区保护简单标志、状态通知、一写多读

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;
        // ……
    }
    
  • 工作流程

    1. 第一次 get() 时,会调用 initialValue() 并将结果封装为 Entry 放入 table
    2. 后续 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 的强引用,僵尸 Entryvalue 只能等到线程结束才清理。
  • 结果:长期运行的线程中,反复创建并丢弃 ThreadLocal 会导致 ThreadLocalMap 内存不断被“泄漏”,直到线程销毁。

13.5 键为何用弱引用?

  • 避免强引用阻止 ThreadLocal 对象回收:若用强引用,ThreadLocal 一旦不再使用,map 中的键永远无法被回收,更早出现泄漏。
  • 弱引用机制:只要用户不再持有 ThreadLocal 变量,GC 即可清除对应的键,触发 expungeStaleEntry() 清理僵尸条目。

13.6 使用建议

  1. 显式清理:用完后调用 threadLocal.remove(),及时移除对应 Entry,避免僵尸残留。
  2. 慎用匿名/临时 ThreadLocal:推荐把 ThreadLocal 作为类成员,并在不再需要时 remove。
  3. 避免在线程池中遗漏清理:线程复用时,一定要在任务结束前清除,否则下一任务会看到前任务残留值。

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)。
  • APIForkJoinPool + RecursiveTask<V> / RecursiveAction
  • 适用场景:可分解且合并成本低的大规模并行计算,如数组归并排序、矩阵乘法等。

4. CountDownLatch

  • 原理:维护一个计数器,初始化时指定;每次调用 countDown() 减一;当计数器归零时,所有 await() 的线程被唤醒。

  • 特点

    • 计数器与线程数无须一一对应,可大于或小于线程数。
    • 一次性:计数器归零后无法重置(需重建实例)。
  • 典型用法:主线程等待若干“准备”或“完成”事件,再统一执行。


5. CyclicBarrier

  • 原理:一组线程互相等待,直到到达屏障点才一起继续。

  • 特点

    • 构造时指定参与线程数,线程必须一一对应。
    • 可携带 barrierAction,在所有线程到齐时执行一次汇总/合并操作。
    • 可重用:通过 reset() 或再次调用 await() 实现循环屏障。

image.png

cyclicBarrier.await()可以被反复调用。也就是可以让几个线程反复的在屏障处汇总等。

image.png 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允许我们凭空造出许可证往里边放。

image.png

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("已取消");
}

image.png

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);
    }
}

测试源码:github.com/xingchaozha…

学后检测

选择题

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();
}