跳到内容

元数据卡

  • 前置知识:线程创建(ch14-concurrency-intro)、方法与栈(第5章)
  • 预计时间:20 分钟
  • 完成标志:能用 synchronized 保护共享数据,理解竞态条件,知道 volatile 的作用,能识别死锁

老陈在工坊里教你打铁。你和老陈各拿一把锤子,同时去敲同一块铁——锤子撞在一起,铁块变形,两个人都没敲好。这就是竞态条件

上一页学会了创建线程。但你也看到了那个计数器问题:两个线程各加 10000 次,结果不是 20000。丢的数去哪了?

竞态条件:为什么 count++ 不安全

count++ 在 CPU 里不是一步——它拆成三步:读 → 改 → 写

线程A: 读到 100
线程B: 读到 100           ← 两个线程读到同一个值!
线程A: +1 → 写回 101
线程B: +1 → 写回 101       ← 加了两次,只增加 1

多个线程在"读-改-写"三步之间插队,数据就不一致了。

java
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 就是那个"我先来"——同一时刻只有一个线程能进入被保护的代码块。

java
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

两种写法——锁的范围越小越好:

java
// 写法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 编译器可能把变量值缓存到寄存器,一个线程看不到另一个线程的修改:

java
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 解决:

java
private static volatile boolean running = true;

注意volatile 只保证可见性,不保证原子性count++ 的问题仍需 synchronized

差异窗口:Python 有 GIL,纯 Python 的 count += 1 是安全的。C++ 用 std::atomicstd::mutex

死锁:锁的循环等待

两个线程各持一把锁等另一把:

java
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(...) 像每次打铁都临时招学徒——线程池一次性招好工人,来了任务就干。

看线程池详解 →

Built with VitePress | Software Systems Atlas