元数据卡
- 前置知识:线程创建(ch14-concurrency-intro)、方法与栈(第5章)
- 预计时间:20 分钟
- 完成标志:能用 synchronized 保护共享数据,理解竞态条件,知道 volatile 的作用,能识别死锁
老陈在工坊里教你打铁。你和老陈各拿一把锤子,同时去敲同一块铁——锤子撞在一起,铁块变形,两个人都没敲好。这就是竞态条件。
上一页学会了创建线程。但你也看到了那个计数器问题:两个线程各加 10000 次,结果不是 20000。丢的数去哪了?
竞态条件:为什么 count++ 不安全
count++ 在 CPU 里不是一步——它拆成三步:读 → 改 → 写。
线程A: 读到 100
线程B: 读到 100 ← 两个线程读到同一个值!
线程A: +1 → 写回 101
线程B: +1 → 写回 101 ← 加了两次,只增加 1多个线程在"读-改-写"三步之间插队,数据就不一致了。
public class RaceConditionDemo {
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) count++;
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) count++;
});
t1.start(); t2.start(); t1.join(); t2.join();
System.out.println("最终结果: " + count);
}
}语言: Java 8+ 运行: javac RaceConditionDemo.java && java RaceConditionDemo,多跑几次 预期输出: 每次不同,如 19973
synchronized:排队进铁匠铺
老陈说:"下次你说'我先来',我等你敲完——这不就解决了?"
synchronized 就是那个"我先来"——同一时刻只有一个线程能进入被保护的代码块。
public class SafeCounter {
private int count = 0;
public synchronized void increment() { count++; }
public synchronized int getCount() { return count; }
public static void main(String[] args) throws InterruptedException {
SafeCounter counter = new SafeCounter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) counter.increment();
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) counter.increment();
});
t1.start(); t2.start(); t1.join(); t2.join();
System.out.println("结果: " + counter.getCount());
}
}语言: Java 8+ 运行: 编译运行,多跑几次 预期输出: 每次都是 结果: 20000
两种写法——锁的范围越小越好:
// 写法1: 锁整个方法
public synchronized void increment() { count++; }
// 写法2: 锁代码块——只保护关键几行
public void incrementWithTiming() {
long start = System.nanoTime(); // 不需要锁
synchronized (this) { // 只锁这一小段
count++;
}
long elapsed = System.nanoTime() - start;
}锁的不是代码,是对象。每个 Java 对象有一个内置锁(monitor)。两个线程如果用不同的锁对象——不互斥。
volatile:你改了,我看不到
另一类问题是可见性。JIT 编译器可能把变量值缓存到寄存器,一个线程看不到另一个线程的修改:
public class VisibilityProblem {
private static boolean running = true;
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
while (running) { /* 可能永远看不到 running 变了 */ }
System.out.println("工作线程退出");
});
worker.start();
Thread.sleep(1000);
running = false;
System.out.println("主线程设置 running=false");
}
}语言: Java 8+ 运行: 编译运行——程序可能不退出 预期输出: 打印"主线程设置 running=false"后卡住
加 volatile 解决:
private static volatile boolean running = true;注意:volatile 只保证可见性,不保证原子性。count++ 的问题仍需 synchronized。
差异窗口:Python 有 GIL,纯 Python 的 count += 1 是安全的。C++ 用 std::atomic 或 std::mutex。
死锁:锁的循环等待
两个线程各持一把锁等另一把:
public class DeadlockDemo {
private static final Object lockA = new Object();
private static final Object lockB = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (lockA) {
sleep(100);
synchronized (lockB) { }
}
});
Thread t2 = new Thread(() -> {
synchronized (lockB) {
sleep(100);
synchronized (lockA) { }
}
});
t1.start(); t2.start();
}
static void sleep(long ms) {
try { Thread.sleep(ms); } catch (InterruptedException e) { }
}
}语言: Java 8+ 运行: 编译运行——卡住 解法: 所有线程按相同顺序获取锁(先 lockA 再 lockB)。
常见陷阱
Thread.sleep()不释放锁。- getter 也要加 synchronized——否则读到旧值。
volatile不能替代synchronized。
旅人笔记
竞态条件 = 多线程"读-改-写"插队。synchronized 锁住对象,保证临界区串行。volatile 保证可见性,不保证原子性。死锁 = 锁顺序不一致。80% 的并发问题靠 synchronized 就够了。
→ 下一步:线程池
每次 new Thread(...) 像每次打铁都临时招学徒——线程池一次性招好工人,来了任务就干。