第 38 章:多线程基础
学习目标
- 理解进程、线程、并发、并行
- 掌握线程创建的四种方式
- 理解线程安全问题的根源
- 掌握 synchronized 与 volatile
一、基本概念
| 概念 | 说明 |
|---|---|
| 进程 | 一个运行中的程序,独立内存空间 |
| 线程 | 进程内的执行单元,共享进程内存 |
| 并发 | 多个任务交替执行(单核也能并发) |
| 并行 | 多个任务同时执行(必须多核) |
共享堆内存 = 线程安全问题的根源。多个线程改同一个对象,就可能出错。
二、创建线程的四种方式
java
// ① 继承 Thread(不推荐:Java 单继承,占用了继承名额)
class MyThread extends Thread {
@Override
public void run() { System.out.println("running"); }
}
new MyThread().start();
// ② 实现 Runnable(推荐:解耦任务和线程)
new Thread(() -> System.out.println("running")).start();
// ③ 实现 Callable(有返回值 + 能抛异常)
FutureTask<String> task = new FutureTask<>(() -> "结果");
new Thread(task).start();
String result = task.get(); // 阻塞等待结果
// ④ 线程池(生产环境唯一正确姿势,见第 39 章)
ExecutorService pool = Executors.newFixedThreadPool(4);
Future<String> f = pool.submit(() -> "结果");| 方式 | 返回值 | 异常 | 推荐度 |
|---|---|---|---|
Thread | ❌ | 只能内部处理 | ⭐ |
Runnable | ❌ | 只能内部处理 | ⭐⭐⭐ |
Callable | ✅ | 可抛出,get() 时接到 | ⭐⭐⭐⭐ |
| 线程池 | ✅ | 同上 | ⭐⭐⭐⭐⭐ |
⚠️ 必须调
start()不能调run()。run()只是普通方法调用,还在当前线程执行,压根没开新线程。
三、线程生命周期
| 状态 | 说明 |
|---|---|
NEW | 已创建未启动 |
RUNNABLE | 可运行(含就绪和运行中) |
BLOCKED | 等待 synchronized 锁 |
WAITING | 无限等待,需别人唤醒 |
TIMED_WAITING | 限时等待 |
TERMINATED | 已结束 |
四、常用方法
java
Thread.sleep(1000); // ① 睡眠,不释放锁
thread.join(); // ② 等待该线程结束
thread.interrupt(); // ③ 设置中断标记(不是强制停止!)
Thread.currentThread().getName();
thread.setDaemon(true); // ④ 守护线程,主线程结束它就结束(必须在 start 前设置)sleep vs wait
| 维度 | Thread.sleep() | Object.wait() |
|---|---|---|
| 所属 | Thread 静态方法 | Object 实例方法 |
| 释放锁 | ❌ 不释放 | ✅ 释放 |
| 唤醒 | 时间到自动醒 | 需 notify() / notifyAll() |
| 使用位置 | 任意 | 必须在 synchronized 块内 |
如何优雅停止线程?
java
// ❌ stop() 已废弃:强制杀死会导致资源不释放、数据不一致
// ✅ 用中断标记
public class Worker implements Runnable {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) { // ① 检查中断标记
try {
doWork();
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // ② 恢复中断标记!
break; // ③ 退出循环
}
}
cleanup(); // ④ 优雅清理
}
}② 为什么要恢复中断标记?
sleep()抛出InterruptedException时会清除中断标记。不恢复的话,外层的isInterrupted()会返回 false,循环继续跑,中断请求就失效了。
五、线程安全问题的根源
java
public class Counter {
private int count = 0;
public void increment() {
count++; // ❌ 看起来一行,实际是三步:读 → 加 1 → 写回
}
}
// 1000 个线程各加 1000 次,结果远小于 100 万三大特性
| 特性 | 含义 | 破坏原因 |
|---|---|---|
| 原子性 | 操作不可分割 | count++ 是三步 |
| 可见性 | 一个线程改了,别的线程立刻看到 | CPU 缓存未同步到主内存 |
| 有序性 | 代码按写的顺序执行 | 编译器/CPU 指令重排 |
六、synchronized
java
// ① 同步方法:锁的是 this
public synchronized void increment() { count++; }
// ② 同步静态方法:锁的是 Counter.class
public static synchronized void staticMethod() { }
// ③ 同步代码块:锁范围更小,性能更好(推荐)
public void increment() {
// 非临界区代码...
synchronized (this) { // 只锁必要的部分
count++;
}
}
// ④ 自定义锁对象(最清晰)
private final Object lock = new Object();
public void increment() {
synchronized (lock) { count++; }
}synchronized 保证三大特性全部满足。
锁升级(JDK 6+ 优化)
所以「
synchronized很慢」是老黄历了。无竞争场景下开销极小。
坑:锁错对象
java
// ❌ 锁 Integer:自动装箱后每次都是不同对象,锁不住
private Integer lock = 0;
synchronized (lock) { lock++; } // ❌ lock++ 后换了个对象
// ❌ 锁 String 字面量:常量池共享,可能和别的代码抢同一把锁
synchronized ("lock") { }
// ✅ 用 final Object
private final Object lock = new Object();七、volatile
java
public class Flag {
private volatile boolean running = true; // ① 保证可见性
public void stop() { running = false; }
public void run() {
while (running) { } // ② 没有 volatile 时,可能永远读到缓存的 true
}
}| 保证 | volatile | synchronized |
|---|---|---|
| 可见性 | ✅ | ✅ |
| 有序性 | ✅(禁止重排) | ✅ |
| 原子性 | ❌ | ✅ |
java
// ❌ volatile 不保证原子性
private volatile int count = 0;
count++; // 依然是三步,依然线程不安全
// ✅ 用 AtomicInteger(CAS 无锁)
private final AtomicInteger count = new AtomicInteger();
count.incrementAndGet();volatile 适用场景:一写多读的状态标记位。涉及「读-改-写」的一律用锁或原子类。
经典应用:双重检查锁单例
java
public class Singleton {
private static volatile Singleton instance; // ① volatile 不能省!
private Singleton() { }
public static Singleton getInstance() {
if (instance == null) { // ② 第一次检查:避免每次都加锁
synchronized (Singleton.class) {
if (instance == null) { // ③ 第二次检查:防止重复创建
instance = new Singleton(); // ④ 这行不是原子操作!
}
}
}
return instance;
}
}④ 为什么必须加 volatile?
new Singleton()实际分三步:(1) 分配内存 (2) 初始化对象 (3) 引用指向内存。 JVM 可能重排为 (1)(3)(2)。此时另一个线程在 ② 处看到instance != null就直接返回了一个还没初始化完成的对象,使用时报错。volatile禁止这种重排。
八、本章小结
| 要点 | 关键 |
|---|---|
| 创建线程 | 生产环境只用线程池 |
start() vs run() | 只有 start 才开新线程 |
| 停止线程 | 用中断标记,别用 stop |
| 三大特性 | 原子性 / 可见性 / 有序性 |
synchronized | 三个都保证,有锁升级优化 |
volatile | 只保证可见性 + 有序性 |
| 双检锁 | volatile 不能省 |
动手练习
练习 1:基础题
写一个多线程计数器,10 个线程各加 10000 次。分别用「不加锁」「synchronized」「AtomicInteger」三种方式,对比结果和耗时。
练习 2:进阶题
用 wait / notify 实现生产者-消费者模型:容量为 10 的缓冲区,3 个生产者、2 个消费者。
下一章:第 39 章:线程池 →