04 多线程基础

写过 C 的 pthreads,你对 pthread_create、互斥锁、条件变量已有肌肉记忆。好消息:这些概念在 Java 中全部存在且更易用;坏消息:Java 内存模型(JMM)引入了 C 程序员不常深究的新问题——可见性与有序性。本章从进程线程概念讲到 synchronized/volatile 两大基石,再到死锁排查,为下一章的并发包打底。

前置知识:流程控制集合框架深入(并发容器的铺垫)。


一、进程与线程

1.1 概念速通

概念定义类比
进程资源分配的最小单位,独立地址空间一个正在运行的程序
线程CPU 调度的最小单位,共享所属进程的资源程序内部的多条执行流

C 背景对照:

维度C (pthreads)Java
创建pthread_create(&tid, attr, fn, arg)new Thread(...).start()
等待结束pthread_join(tid)thread.join()
互斥锁pthread_mutex_t + lock/unlocksynchronized 关键字或 ReentrantLock
条件变量pthread_cond_wait/signalObject.wait()/notify()
分离属性PTHREAD_CREATE_DETACHED默认非守护线程,setDaemon(true) 对应分离

本质上 JVM 的每个 Java 线程就是一个内核级线程(1:1 模型),Java 只是把它包装成了对象。这个包装在 JDK 21 被彻底革新——虚拟线程让”一个任务一个线程”变得可行(见 Java 新特性)。

为什么 Java 的线程模型是 1:1?

JVM 选择 1:1 模型(一个 Java 线程对应一个内核线程)而非 N:M 模型(多个用户线程映射到少量内核线程),是因为 1:1 模型最简单、最可靠:内核调度器直接管理所有线程,不存在用户态调度器的复杂性;一个线程阻塞(如 I/O)不影响其他线程;多核 CPU 的并行度天然最大化。代价是线程创建/切换成本较高(需要陷入内核态),但在 JDK 21 虚拟线程出现之前,这个代价被认为是值得的。


二、创建线程的三种方式

2.1 完整示例

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
 
public class ThreadCreation {
    public static void main(String[] args) throws Exception {
 
        // 方式一:继承 Thread,重写 run
        class ByExtend extends Thread {
            @Override
            public void run() {
                System.out.println("方式一:" + Thread.currentThread().getName());
            }
        }
        new ByExtend().start();
 
        // 方式二:实现 Runnable(推荐——任务与执行器解耦,还能交给线程池)
        Runnable task = () -> System.out.println(
            "方式二:" + Thread.currentThread().getName());
        new Thread(task, "worker-1").start();
 
        // 方式三:Callable + FutureTask —— 有返回值、能抛异常
        Callable<Integer> calc = () -> {
            Thread.sleep(100);              // 模拟耗时计算
            return 6 * 7;
        };
        FutureTask<Integer> future = new FutureTask<>(calc);
        new Thread(future, "worker-2").start();
        Integer result = future.get();      // 阻塞直到结果就绪
        System.out.println("方式三计算结果:" + result);
 
        // 主线程等待所有子线程结束再退出
        Thread.sleep(200);
    }
}

三种方式对比:

维度继承 Thread实现 RunnableCallable+FutureTask
返回值
异常只能内部消化只能内部消化可抛出并由 get() 重放
耦合度任务绑定线程任务独立任务独立
推荐度不推荐(占用继承位)常规推荐需要结果时使用

关键细节:调用 start() 才会开新线程;直接调 run() 只是普通方法调用。线程只能启动一次,重复 start 抛 IllegalThreadStateException


三、线程生命周期:六种状态

stateDiagram-v2
    [*] --> NEW: new Thread()
    NEW --> RUNNABLE: start()
    RUNNABLE --> BLOCKED: 等待 synchronized 锁
    BLOCKED --> RUNNABLE: 获得锁
    RUNNABLE --> WAITING: wait() / join() / LockSupport.park()
    WAITING --> RUNNABLE: notify() / join 的线程结束 / unpark
    RUNNABLE --> TIMED_WAITING: sleep(ms) / wait(ms) / join(ms)
    TIMED_WAITING --> RUNNABLE: 超时或被唤醒
    RUNNABLE --> TERMINATED: run() 执行完毕
    TERMINATED --> [*]
状态进入方式与 pthreads 对照
NEW已创建未 start未 create
RUNNABLE就绪或正在运行(Java 合并了两态)running/runnable
BLOCKED抢 synchronized 锁失败等 mutex
WAITINGwait()/join()/park(),无限期等唤醒cond_wait
TIMED_WAITING带 timeout 的等待版本timedwait
TERMINATEDrun 结束terminated

注意区分:BLOCKED 只针对 synchronized;用 JUC 锁(ReentrantLock)等待时处于 WAITING。

3.1 状态转换详解

NEW(新建):
  Thread t = new Thread(...)  →  状态为 NEW
  此时线程只是一个 Java 对象,操作系统还未创建真正的线程
 
RUNNABLE(就绪/运行):
  t.start()  →  状态变为 RUNNABLE
  Java 的 RUNNABLE 合并了操作系统的 ready 和 running 两种状态
  具体是否在 CPU 上执行,取决于操作系统调度器
 
BLOCKED(阻塞):
  进入:synchronized 代码块/方法时,锁被其他线程持有
  退出:获得锁
  注意:只有 synchronized 才会进入此状态
 
WAITING(等待):
  进入:Object.wait()(无超时)、Thread.join()(无超时)、LockSupport.park()
  退出:被 notify()/unpark() 唤醒
  线程在这里"无限期"等待,直到被唤醒
 
TIMED_WAITING(超时等待):
  进入:Thread.sleep(ms)、Object.wait(ms)、Thread.join(ms)
  退出:超时时间到 或 被唤醒
  与 WAITING 的区别是"有超时兜底"
 
TERMINATED(终止):
  run() 方法执行完毕 或 抛出未捕获异常
  线程不可重新 start()

为什么 Java 要区分 BLOCKED 和 WAITING?

这两个状态的区别在于”你在等什么”。BLOCKED 是在等锁——其他线程持有一把 synchronized 锁,你只能等它释放。WAITING 是在等条件——你主动调用 wait() 让出锁并等待通知,或者调用 Thread.sleep() 让出 CPU。jstack 看到线程处于 BLOCKED 意味着”有锁竞争”,处于 WAITING 意味着”在等某件事发生”。排查问题时这个区分非常关键。


四、synchronized:三种用法与 monitor 原理

4.1 三种用法

public class SyncUsages {
    private int count = 0;
    private static int staticCount = 0;
    private final Object lock = new Object();   // 专用锁对象
 
    // 用法一:实例方法 —— 锁的是 this
    public synchronized void incr() {
        count++;
    }
 
    // 用法二:静态方法 —— 锁的是 Class 对象(全类唯一)
    public static synchronized void staticIncr() {
        staticCount++;
    }
 
    // 用法三:同步代码块 —— 锁任意对象,粒度最细
    public void batch(int times) {
        for (int i = 0; i < times; i++) {
            synchronized (lock) {       // 只锁临界区,循环体其余部分并行友好
                count++;
            }
            doOtherWork();
        }
    }
 
    private void doOtherWork() { /* ... */ }
}
用法锁对象保护范围
实例方法this实例级别的状态
静态方法XxxClass.class类级别(静态)状态,全 JVM 唯一
代码块括号里指定的对象自定义粒度,最灵活

4.2 monitor 原理

每个 Java 对象都关联一把内置锁(monitor)。编译后同步块的头尾是字节码指令 monitorenter/monitorexit;同步方法则是方法标志位 ACC_SYNCHRONIZED。底层依赖对象头 Mark Word 中的锁状态,JDK 6 后有锁升级路径:

flowchart LR
    U["无锁"] --> B["偏向锁<br/>单线程反复进入<br/>仅记录线程 ID"]
    B --> L["轻量级锁<br/>交替竞争,CAS 自旋"]
    L --> H["重量级锁<br/>激烈竞争,阻塞挂起"]

锁只能升级不能降级(整体而言)。这就是”JDK 8 后 synchronized 性能不差”的原因——多数场景停留在偏向/轻量级阶段。(偏向锁在 JDK 15 后已默认废弃,因为维护成本高于收益,面试可作加分点提一句。)

4.3 锁升级过程详解

无锁状态:
  Mark Word 存放: hashCode + 分代年龄 + 锁标志(01)
  任何对象创建后默认无锁
 
偏向锁(JDK 8 默认开启,JDK 15 默认关闭):
  触发: 第一个线程进入 synchronized 块
  Mark Word 变为: 线程ID + 分代年龄 + 锁标志(01)
  同一线程再次进入时,只需比对线程ID(无CAS、无系统调用)
  适用场景: 只有一个线程反复进入的同步块
  弊端: 撤销偏向锁需要 STW,多线程交替进入时反而增加开销
 
轻量级锁:
  触发: 偏向锁被第二个线程竞争时撤销,升级为轻量级锁
  Mark Word 变为: 指向栈帧锁记录的指针 + 锁标志(00)
  竞争线程 CAS 自旋尝试获取锁(不阻塞,不陷入内核)
  适用场景: 线程交替进入同步块(无实际竞争)
  弊端: 长时间自旋浪费 CPU
 
重量级锁:
  触发: 自旋超过阈值(默认自适应自旋)或竞争激烈
  Mark Word 变为: 指向 monitor 对象的指针 + 锁标志(10)
  未获得锁的线程被操作系统挂起(上下文切换,代价高)
  适用场景: 激烈竞争
  优势: 不浪费 CPU 自旋,等待线程被真正挂起

4.4 经典示例:卖票问题

public class TicketSeller {
    private int tickets = 100;
 
    public synchronized boolean sell() {
        if (tickets <= 0) {
            return false;                 // 卖完了
        }
        tickets--;
        return true;
    }
 
    public static void main(String[] args) throws InterruptedException {
        TicketSeller seller = new TicketSeller();
 
        Runnable job = () -> {
            while (true) {
                if (!seller.sell()) break;
            }
        };
 
        // 三个窗口并发卖同一批票
        Thread w1 = new Thread(job, "窗口1");
        Thread w2 = new Thread(job, "窗口2");
        Thread w3 = new Thread(job, "窗口3");
        w1.start(); w2.start(); w3.start();
        w1.join(); w2.join(); w3.join();
 
        System.out.println("剩余票数(必须为 0):" + seller.tickets);
    }
}

sell() 的 synchronized 去掉跑一次,你会看到剩余票数为负——这就是竞态条件的现场演示。根源在于 tickets-- 是三步操作(读-减-写),线程可能在任意两步之间被打断。


五、synchronized vs ReentrantLock

5.1 对比总表

维度synchronizedReentrantLock
获取锁方式自动获取手动 lock()
释放锁方式自动释放手动 unlock()(必须在 finally 中)
可中断不可中断lockInterruptibly() 可中断
超时获取不支持tryLock(timeout) 支持
公平锁非公平可选公平/非公平
条件变量只有一个等待队列(wait/notify)可创建多个 Condition
锁升级支持偏向→轻量→重量无锁升级(直接 CAS + 阻塞)
性能JDK 6+ 已优化,常规场景持平高竞争场景略优

5.2 ReentrantLock 完整示例

import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.Condition;
import java.util.LinkedList;
import java.util.Queue;
 
public class ReentrantLockDemo {
 
    private final ReentrantLock lock = new ReentrantLock(true);  // true = 公平锁
    private final Condition notFull = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();
    private final Queue<Integer> buffer = new LinkedList<>();
    private final int capacity = 5;
 
    public void produce(int item) throws InterruptedException {
        lock.lock();
        try {
            while (buffer.size() == capacity) {
                notFull.await();    // 缓冲区满,等待消费者唤醒
            }
            buffer.offer(item);
            System.out.println("生产 " + item + ",库存 " + buffer.size());
            notEmpty.signal();      // 唤醒消费者
        } finally {
            lock.unlock();         // 必须在 finally 中释放!
        }
    }
 
    public int consume() throws InterruptedException {
        lock.lock();
        try {
            while (buffer.isEmpty()) {
                notEmpty.await();   // 缓冲区空,等待生产者唤醒
            }
            int item = buffer.poll();
            System.out.println("消费 " + item + ",库存 " + buffer.size());
            notFull.signal();       // 唤醒生产者
            return item;
        } finally {
            lock.unlock();
        }
    }
 
    public static void main(String[] args) {
        ReentrantLockDemo demo = new ReentrantLockDemo();
 
        new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                try { demo.produce(i); Thread.sleep(50); }
                catch (InterruptedException e) { return; }
            }
        }, "producer").start();
 
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try { demo.consume(); Thread.sleep(120); }
                catch (InterruptedException e) { return; }
            }
        }, "consumer").start();
    }
}

5.3 什么时候选 ReentrantLock?

选择 synchronized:
  ✓ 简单的同步需求(保护一块临界区)
  ✓ 不需要中断/超时/公平性
  ✓ 代码块较短,不会长时间持有锁
  ✓ 熟悉 JVM 优化(偏向锁、轻量级锁)
 
选择 ReentrantLock:
  ✓ 需要 tryLock() 超时获取(避免死锁)
  ✓ 需要 lockInterruptibly() 响应中断
  ✓ 需要公平锁(按等待顺序获取)
  ✓ 需要多个 Condition 精确唤醒
  ✓ 需要获取锁的状态信息(isLocked/getHoldCount)

为什么 Java 要提供两种锁?

synchronized 是 JVM 内置的关键字,使用简单但功能有限。ReentrantLock 是 java.util.concurrent 包中的类,功能更丰富但使用更复杂(必须手动 unlock)。这不是设计失误,而是渐进式暴露复杂度的策略:大多数场景用 synchronized 就够了,需要高级功能时再用 ReentrantLock。两者底层都基于 AQS(AbstractQueuedSynchronizer),性能差异在 JDK 6+ 后已经很小。


六、volatile:可见性与禁止重排

6.1 三大问题

并发 bug 的三大根源,全部源于 CPU 缓存与编译器优化的”好心”:

问题含义示例
原子性操作不可分割,可能被中断穿插count++ 非原子
可见性一个线程的修改,另一个线程看不到写线程改 flag,读线程死循环
有序性指令重排导致逻辑顺序被打乱双重检查锁的单例拿到半初始化对象

6.2 volatile 解决其中两个

public class VolatileDemo {
    // 没有 volatile 时,读线程可能永远读到缓存里的 false,死循环
    private static volatile boolean running = true;
 
    // DCL 单例中 volatile 的第二个用途:防止指令重排
    private static volatile VolatileDemo instance;
 
    public static void main(String[] args) throws InterruptedException {
        Thread reader = new Thread(() -> {
            while (running) {
                // 忙等
            }
            System.out.println("reader 观察到停止信号");
        });
        reader.start();
 
        Thread.sleep(100);
        running = false;    // volatile 写:立即刷回主内存并使其他核心缓存失效
        reader.join();
        System.out.println("main 结束");
    }
}

volatile 的两层保证:

  1. 可见性:写 volatile 变量后立刻刷主内存,读它强制从主内存加载
  2. 禁止重排:通过内存屏障保证 happens-before 语义——volatile 写之前的所有普通写,对 volatile 读之后的代码可见

但它不保证原子性volatile int count; count++ 依然丢更新。要原子性请用 AtomicInteger 或锁。

6.3 volatile 的底层原理:内存屏障

内存屏障 (Memory Barrier):
┌──────────────────────────────────────────────┐
│  CPU 为了性能,会重排指令、缓存数据             │
│  内存屏障强制"禁止重排"和"刷新缓存"             │
│                                               │
│  volatile 写操作后插入 StoreStore + StoreLoad  │
│  volatile 读操作前插入 LoadLoad + LoadStore    │
│                                               │
│  效果:                                         │
│  写 volatile 前的普通写 → 对其他线程立即可见      │
│  读 volatile 后的普通读 → 能看到之前的写          │
│                                               │
│  CPU 级别实现:                                  │
│  x86: lock 前缀指令(兼做原子性保证)            │
│  ARM: dmb 指令                                 │
└──────────────────────────────────────────────┘

6.4 volatile 的典型使用场景

// 场景一:状态标志
private volatile boolean shutdownRequested = false;
 
public void shutdown() {
    shutdownRequested = true;
}
 
public void run() {
    while (!shutdownRequested) {
        // 工作循环
    }
}
 
// 场景二:双重检查锁定 (DCL) 单例
public class Singleton {
    private static volatile Singleton instance;
 
    public static Singleton getInstance() {
        if (instance == null) {              // 第一次检查(无锁)
            synchronized (Singleton.class) {
                if (instance == null) {      // 第二次检查(有锁)
                    instance = new Singleton(); // volatile 防止重排序
                }
            }
        }
        return instance;
    }
}
 
// 场景三:一次性安全发布
private volatile Config config;
 
public Config getConfig() {
    return config;  // 其他线程看到非 null 的 config 时,其内部字段一定已初始化完成
}

DCL 中 volatile 为什么必要?

instance = new Singleton() 不是原子操作,实际分三步:①分配内存 ②初始化对象 ③将引用指向内存。JVM 可能重排为 ①→③→②。如果没有 volatile,另一个线程可能在 ③ 执行后、② 执行前看到非 null 的 instance,拿到一个半初始化的对象。volatile 的禁止重排保证了 ①②③ 的执行顺序。

happen-before 入门版:若 A 操作 happens-before B 操作,则 A 的结果对 B 可见。规则包括:程序顺序规则、monitor 锁的解锁先于加锁、volatile 写先于读、线程 start/join 等。这是理解 JMM 的钥匙,本章建立直觉即可。


七、ThreadLocal:用法与陷阱

7.1 基本用法

public class ThreadLocalDemo {
    // 每个线程独立的 SimpleDateFormat 实例
    // SimpleDateFormat 非线程安全,不能共享
    private static final ThreadLocal<SimpleDateFormat> DATE_FORMAT =
        ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
 
    public static String formatDate(Date date) {
        return DATE_FORMAT.get().format(date);
    }
 
    public static void main(String[] args) {
        // 多个线程并发格式化,互不干扰
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                String result = formatDate(new Date());
                System.out.println(Thread.currentThread().getName() + ": " + result);
            }, "t" + i).start();
        }
    }
}

7.2 ThreadLocal 的存储原理

每个线程内部维护一个 ThreadLocalMap:
┌─────────────────────────────────┐
│  Thread 对象                     │
│  ├── threadLocals: ThreadLocalMap │
│  │   ├── Entry[0]:               │
│  │   │   key: ThreadLocal 对象(弱引用) │
│  │   │   value: 实际存储的值       │
│  │   ├── Entry[1]:               │
│  │   │   key: 另一个 ThreadLocal   │
│  │   │   value: 另一个值          │
│  │   └── ...                     │
│  └── ...                         │
└─────────────────────────────────┘
 
key 是 ThreadLocal 对象的弱引用(WeakReference)
value 是你实际 set 的对象(强引用)

7.3 ThreadLocal 的内存泄漏问题

public class ThreadLocalLeakDemo {
    // 线程池中使用 ThreadLocal 是高危操作
    private static final ExecutorService pool = Executors.newFixedThreadPool(10);
    private static final ThreadLocal<byte[]> BUFFER = new ThreadLocal<>();
 
    public static void process() {
        BUFFER.set(new byte[1024 * 1024]);  // 1MB
        try {
            // 业务处理...
        } finally {
            BUFFER.remove();  // 必须清理!
        }
    }
 
    public static void main(String[] args) {
        // 错误示例:忘记 remove
        pool.submit(() -> {
            BUFFER.set(new byte[1024 * 1024]);
            // 处理完毕...
            // 没有 remove()!
            // 线程被回收到池中,ThreadLocal 被 GC 回收
            // 但 Entry 的 value 仍被 Thread 强引用,无法回收
            // 1MB 泄漏,线程复用 N 次就泄漏 N MB
        });
    }
}

泄漏根因分析:

正常流程:
  set() → ThreadLocalMap.put() → Entry(key=弱引用, value=强引用)
  remove() → Entry 被删除 → value 可被 GC
 
泄漏流程:
  set() → Entry 存入 ThreadLocalMap
  线程池回收线程 → Thread 对象存活
  ThreadLocal 对象被 GC → key 变为 null
  但 Entry 的 value 仍被 Thread.threadLocals 强引用
  → value 无法被 GC → 内存泄漏
 
为什么 key 用弱引用?
  如果 key 是强引用,ThreadLocal 对象永远不会被 GC
  → ThreadLocalMap 永远增长 → 更严重的泄漏
  弱引用让 ThreadLocal 对象可以被 GC
  → key 变为 null → 后续 get/set 时可以清理这些"垃圾 Entry"

7.4 ThreadLocal 最佳实践

// 1. 用完必须 remove()(在 finally 中)
try {
    threadLocal.set(value);
    // 业务逻辑...
} finally {
    threadLocal.remove();
}
 
// 2. 线程池场景必须 remove(线程复用 = ThreadLocal 复用)
// 3. 尽量用 ThreadLocal.withInitial() 而非 init-value,懒加载
// 4. 考虑用 InheritableThreadLocal 在父子线程间传递值
// 5. 考虑用 ScopedValue(JDK 21+)替代 ThreadLocal

为什么 ThreadLocal 用弱引用做 key?

如果 ThreadLocalMap 的 Entry 用强引用指向 key(ThreadLocal 对象),那么只要 Entry 存在,ThreadLocal 对象就永远无法被 GC。当 ThreadLocal 不再被外部引用时,它应该被回收,但强引用阻止了这一点。弱引用让 ThreadLocal 对象在没有外部引用时可以被 GC,Entry 的 key 变为 null。后续的 get()/set() 操作会清理这些 key 为 null 的 Entry,防止无限增长。这是弱引用在实际框架中的典型应用。


八、CompletableFuture 异步编程

8.1 基本用法

import java.util.concurrent.CompletableFuture;
 
public class CompletableFutureDemo {
 
    public static void main(String[] args) {
        // 基本异步执行
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            // 异步执行耗时操作
            try { Thread.sleep(1000); } catch (InterruptedException e) {}
            return "Hello";
        });
 
        // 链式调用
        CompletableFuture<String> result = CompletableFuture
            .supplyAsync(() -> "Hello")
            .thenApply(s -> s + " World")           // 同步转换
            .thenApplyAsync(s -> s + "!")            // 异步转换
            .thenAccept(s -> System.out.println(s)); // 消费结果
 
        // 组合多个异步任务
        CompletableFuture<String> userFuture = CompletableFuture
            .supplyAsync(() -> fetchUser(1));
        CompletableFuture<Double> priceFuture = CompletableFuture
            .supplyAsync(() -> fetchPrice(1));
 
        // 两个任务都完成后组合结果
        CompletableFuture<String> combined = userFuture
            .thenCombine(priceFuture,
                (user, price) -> user + " - " + price);
 
        System.out.println(combined.join());
    }
 
    static String fetchUser(int id) {
        try { Thread.sleep(500); } catch (InterruptedException e) {}
        return "User-" + id;
    }
 
    static double fetchPrice(int id) {
        try { Thread.sleep(800); } catch (InterruptedException e) {}
        return 99.9;
    }
}

8.2 异常处理

public class CompletableFutureErrorHandling {
 
    public static void main(String[] args) {
        // 方式一: exceptionally(类似 catch)
        CompletableFuture<String> future = CompletableFuture
            .supplyAsync(() -> {
                if (true) throw new RuntimeException("出错了");
                return "OK";
            })
            .exceptionally(ex -> {
                System.out.println("捕获异常: " + ex.getMessage());
                return "默认值";
            });
 
        // 方式二:handle(类似 try-catch-finally)
        CompletableFuture<String> future2 = CompletableFuture
            .supplyAsync(() -> "data")
            .handle((result, ex) -> {
                if (ex != null) {
                    return "错误恢复: " + ex.getMessage();
                }
                return "成功: " + result;
            });
 
        // 方式三:超时控制(JDK 9+)
        CompletableFuture<String> future3 = CompletableFuture
            .supplyAsync(() -> {
                try { Thread.sleep(5000); } catch (InterruptedException e) {}
                return "慢结果";
            })
            .orTimeout(2, java.util.concurrent.TimeUnit.SECONDS)
            .exceptionally(ex -> "超时默认值");
    }
}

8.3 实战模式:并行查询后合并

public class ParallelQueryExample {
 
    record Product(String name, double price, String description) {}
 
    public static void main(String[] args) {
        long start = System.currentTimeMillis();
 
        // 三个数据源并行查询
        CompletableFuture<String> nameFuture = CompletableFuture
            .supplyAsync(() -> {
                sleep(300);
                return "iPhone 15";
            });
 
        CompletableFuture<Double> priceFuture = CompletableFuture
            .supplyAsync(() -> {
                sleep(500);
                return 7999.0;
            });
 
        CompletableFuture<String> descFuture = CompletableFuture
            .supplyAsync(() -> {
                sleep(400);
                return "A17 芯片,钛金属边框";
            });
 
        // allOf 等待全部完成
        CompletableFuture<Void> allDone = CompletableFuture.allOf(
            nameFuture, priceFuture, descFuture);
 
        // join 获取结果
        Product product = allDone.thenApply(v ->
            new Product(nameFuture.join(), priceFuture.join(), descFuture.join())
        ).join();
 
        long elapsed = System.currentTimeMillis() - start;
        System.out.println("产品: " + product);
        System.out.println("耗时: " + elapsed + "ms (串行需1200ms,并行只需500ms)");
    }
 
    static void sleep(long ms) {
        try { Thread.sleep(ms); } catch (InterruptedException e) {}
    }
}

为什么 CompletableFuture 比 Future 更好?

原始 Future 只有 get()(阻塞等待)和 isDone()(轮询检查),无法组合多个 Future、无法在完成时回调、无法处理异常。CompletableFuture 引入了函数式编程的链式风格:thenApply(转换结果)、thenCompose(扁平化嵌套)、thenCombine(组合结果)、exceptionally(处理异常)。这是从”拉模式”(调用方阻塞拉取结果)到”推模式”(结果就绪时主动推送)的转变,更符合异步编程的心智模型。


九、sleep / wait / notify

9.1 三者区别(高频面试题)

维度Thread.sleepObject.waitObject.notify
所属Thread 静态方法Object 实例方法Object 实例方法
释放锁不释放释放 monitor 锁唤醒等待者
使用前提任意处可调必须持有该对象锁(在 synchronized 内)同左
唤醒方式时间到自动恢复notify/notifyAll/中断——
所在类java.lang.Threadjava.lang.Objectjava.lang.Object

“wait 为什么定义在 Object 而不是 Thread?“——因为锁是对象级别的,等待的是”这个对象的监视器条件”,任何对象都可能成为锁。

9.2 标准的 wait/notify 范式

import java.util.LinkedList;
import java.util.Queue;
 
public class WaitNotifyQueue {
    private final Queue<Integer> buffer = new LinkedList<>();
    private final int capacity = 5;
 
    public synchronized void produce(int item) throws InterruptedException {
        // 关键一:用 while 而不是 if——防止虚假唤醒与被抢锁后条件已变
        while (buffer.size() == capacity) {
            System.out.println("缓冲区满,生产者等待");
            wait();                     // 释放 this 锁,进入等待队列
        }
        buffer.offer(item);
        System.out.println("生产 " + item + ",当前库存 " + buffer.size());
        notifyAll();                    // 关键二:唤醒消费者
    }
 
    public synchronized int consume() throws InterruptedException {
        while (buffer.isEmpty()) {
            System.out.println("缓冲区空,消费者等待");
            wait();
        }
        int item = buffer.poll();
        System.out.println("消费 " + item + ",当前库存 " + buffer.size());
        notifyAll();
        return item;
    }
 
    public static void main(String[] args) {
        WaitNotifyQueue q = new WaitNotifyQueue();
 
        new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                try { q.produce(i); Thread.sleep(50); }
                catch (InterruptedException e) { return; }
            }
        }, "producer").start();
 
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try { q.consume(); Thread.sleep(120); }
                catch (InterruptedException e) { return; }
            }
        }, "consumer").start();
    }
}

两条铁律:条件检查必须用 while;优先 notifyAll 而非 notify(notify 只唤醒一个,若唤醒的恰好是同类线程会全员卡死)。


十、join、yield 与守护线程

public class ThreadUtilsDemo {
    public static void main(String[] args) throws InterruptedException {
        // join:让主线程等子线程干完活再继续
        Thread worker = new Thread(() -> {
            try {
                Thread.sleep(300);
                System.out.println("worker 完成");
            } catch (InterruptedException e) { }
        });
        worker.start();
        System.out.println("main 先做自己的事");
        worker.join();          // 等价于循环 isAlive + wait,可带超时参数
        System.out.println("main 拿到结果继续");
 
        // yield:提示调度器"我可以让出 CPU",仅是建议,几乎不该用于业务逻辑
        Thread.yield();
 
        // daemon:守护线程随 JVM 退出而消亡,适合后台心跳/监控类任务
        Thread daemon = new Thread(() -> {
            while (true) {
                try { Thread.sleep(500); }
                catch (InterruptedException e) { return; }
                // 注意:JVM 退出时不通知守护线程,不要用它写关键数据!
            }
        });
        daemon.setDaemon(true);     // 必须在 start 之前设置
        daemon.start();
        Thread.sleep(100);
        System.out.println("main 结束,JVM 退出,daemon 直接消亡");
    }
}
API作用使用建议
t.join()当前线程等待 t 结束任务编排的基本手段
Thread.yield()让出 CPU 的提示测试代码偶用,业务禁用
setDaemon(true)设为守护线程后台辅助任务;不能持有关键资源

十一、死锁:四条件与排查

11.1 制造一个死锁

public class DeadlockDemo {
    static final Object FORK_A = new Object();
    static final Object FORK_B = new Object();
 
    public static void main(String[] args) throws InterruptedException {
        Thread p1 = new Thread(() -> {
            synchronized (FORK_A) {
                sleep(50);                       // 确保 p2 已拿到 B,制造交叉
                synchronized (FORK_B) {
                    System.out.println("p1 同时拿到两把叉");
                }
            }
        });
        Thread p2 = new Thread(() -> {
            synchronized (FORK_B) {              // 加锁顺序相反!
                synchronized (FORK_A) {
                    System.out.println("p2 同时拿到两把叉");
                }
            }
        });
        p1.start(); p2.start();
        p1.join(); p2.join();
        System.out.println("永远走不到这里");
    }
 
    static void sleep(long ms) {
        try { Thread.sleep(ms); } catch (InterruptedException e) { }
    }
}

11.2 死锁四条件(Coffman)

条件含义打破手段
互斥资源一次只能一人使用无锁结构/CAS
持有并等待抱着已有资源去要新的一次性申请所有资源
不可剥夺锁不能被强行抢走tryLock(timeout) 失败就放弃重来
循环等待形成环形依赖链全局固定加锁顺序(最常用)

实战中最有效的两条:统一加锁顺序、用 tryLock 设置超时。

11.3 jstack 排查实战

程序疑似卡死时:

# 1. 找到 Java 进程号
jps -l
 
# 2. 导出线程快照(或直接 jstack <pid>)
jstack <pid> > thread.dump
 
# 3. 在 dump 中搜索关键字,jstack 甚至直接给出结论:
# Found one Java-level deadlock:
# "Thread-0":
#   waiting to lock object 0x... (a Object) which is held by "Thread-1"

jstack 会自动检测死锁并在末尾输出 Found one Java-level deadlock 及互相持有的锁信息。图形化工具(IDEA / VisualVM / JConsole)也能一键检测。更多命令行工具见 JVM 调优

11.4 预防性设计清单

死锁预防清单:
1. 锁的获取顺序全项目统一(如按对象 id 排序后加锁)
2. 缩小临界区,锁内绝不调用未知的外部方法
3. 能用无锁并发容器就不自己加锁
4. 高风险路径加超时与重试(tryLock + timeout)
5. 避免嵌套锁(持有一把锁去请求另一把锁)
6. 用 openj9 的 -XX:+DeadlockDetection 检测死锁

十二、为什么 Java 的线程模型是这样设计的?

12.1 设计哲学对比

维度C (pthreads)Java (Thread)Java 21+ (Virtual Thread)
线程模型1:1(用户态=内核态)1:1N:M(虚拟线程:载体线程)
创建成本低(~8KB 栈)高(~1MB 栈)极低(~几KB)
上下文切换内核态切换内核态切换用户态切换
并发数数千数千(受限于内存)数百万
阻塞代价高(占内核线程)低(让出载体线程)

12.2 Java 线程模型的演进

JDK 1.0: 简单的 Thread 包装,无并发包

JDK 5.0: java.util.concurrent 包(线程池、锁、并发容器)
    ↓  解决了"手动管理线程"的复杂性
JDK 8.0: CompletableFuture、Lambda 简化异步编程
    ↓  解决了"回调地狱"问题
JDK 9.0: Flow API(响应式流)
    ↓  解决了"背压"问题
JDK 21.0: 虚拟线程(Project Loom)
    ↓  解决了"线程成本高"的根本问题

为什么 Java 21 引入虚拟线程?

传统 Java 线程(平台线程)映射到操作系统内核线程,每个线程需要 ~1MB 栈内存和内核态切换开销。当应用需要处理大量并发 I/O(如 Web 服务器)时,创建数十万个线程是不现实的——内存先爆了。虚拟线程的本质是”N:M 模型”:数百万个虚拟线程映射到少量载体线程(平台线程),虚拟线程在 I/O 阻塞时让出载体线程,载体线程去执行其他虚拟线程。这让”一个请求一个线程”的简单编程模型在百万级并发下成为可能。


十三、小结

知识点一句话
创建方式Runnable 为主,要返回值用 Callable+FutureTask
六状态NEW/RUNNABLE/BLOCKED/WAITING/TIMED_WAITING/TERMINATED
synchronized方法锁 this、静态锁 Class、块锁自定义对象;底层 monitor + 锁升级
vs ReentrantLocksynchronized 简单自动,ReentrantLock 支持中断/超时/公平/多 Condition
volatile保可见性与有序性,不保原子性
ThreadLocal线程隔离存储,用完必须 remove,线程池中是泄漏高发区
CompletableFuture异步编程的函数式组合,比 Future 更灵活
wait vs sleepwait 放锁且需持锁,sleep 不放锁随处可用
死锁四条件破其一即可,首选固定加锁顺序 + tryLock + jstack 排查
线程模型1:1 映射内核线程,JDK 21 虚拟线程改为 N:M 以支持高并发


练习

题号题目链接知识点
1114按序打印https://leetcode.cn/problems/print-in-order/线程同步、锁
1115交替打印FooBarhttps://leetcode.cn/problems/print-foobar-alternately/线程通信