Skip to content

元数据卡

  • 前置知识:第5章(方法与栈)、第6章(数组)
  • 预计时间:90 分钟
  • 核心难度:
  • 阅读模式:🚀 高压模式(建议清醒时读)
  • 完成标志:能理解多线程的基本概念并创建线程;能用 synchronized 保护共享数据;理解线程状态流转;了解线程池的基本用法

可选预览章:并发编程是中级到高级话题。 对于编程初学者建议仅扫描阅读——先建立"程序可以同时做多件事"+"共享数据很危险"的概念, 真正的线程同步、volatile、线程池等内容将在 Vol 3 中系统学习。

本章分层

  • 必读(仅预览):进程 vs 线程的基本概念、"程序可以同时做多件事"的直观印象、共享数据 + 并发修改 = 危险的警觉
  • 选读:线程创建(Thread / Runnable)、synchronized 的基本语义
  • 深水区:线程六状态流转、wait/notify 的基本模式

本章不会要求你掌握

  • volatile / AtomicInteger / 线程池 —— 留到 Vol 3
  • ReentrantLock / ReadWriteLock / ConcurrentHashMap —— 留到 Vol 3
  • 死锁的检测与避免策略(Vol 3 专题)

你在哪

变量村的图书馆系统上线了,阿花一个人用得好好的。可今天大促来了 100 个读者同时登录——系统慢得像蜗牛,有几笔借阅还莫名其妙重复了。

你写了一个图书管理系统。单人使用,完美。

然后你发现,你需要处理 100 个用户的并发访问——凌晨 0 点整,大促开始,100 个人同时抢同一本书的借阅权。你的系统运行在 8 核 CPU 上——可是你只用了 1 个线程先跑查询、再处理借阅、再返回结果——剩下 7 个核在一边凉快。

更糟的是,如果一个线程正在扣减库存,另一个线程同时读到旧库存,两个人都借到了同一本"最后一本书"——物理上只有一本,系统却借出了两本。

老陈师傅靠在椅背上,看着你抓狂的样子:"你学的东西都假设世界上只有一个人在运行程序。但真正的计算机——是同时干很多件事的。CPU 有多个核心,硬盘同时在读写,网络请求同时涌进来……"

"你不是一个人在战斗——你需要学会让代码在多个人手中不打架。"

你的任务

这章是你遇到的第一个"真正的难题"。并发编程的坑比你之前遇到的所有 Bug 加起来都多。但理解这些基础概念,会让你从"只会写单线程玩具"变成"能处理生产级负载的程序员"。

你将学会:

  1. 线程创建——Thread 类和 Runnable 接口,让你的程序"分身有术"
  2. 线程状态——从出生到死亡,一个线程经历的六个阶段
  3. synchronized——锁:让多个人排队操作共享资源
  4. volatile——轻量级同步:保证线程看到的是最新值
  5. wait/notify——线程间通信:"你干完了叫我"
  6. 线程池——不是每次创建新线程,而是用好"已有的劳工"
  7. 竞争条件与原子性——Bug 之王:为什么 count++ 在多线程里不是安全的

这不是一个"零基础"主题。如果你觉得跟不上了——后退一下,把前几章再看一遍。因为这些概念是串联的:线程是基础 → 锁是防护 → 线程池才是生产级方案。

遭遇战 → 获得技能

第一幕:分身有术——创建线程

你的 main 方法运行在"主线程"里。要从主线程分出一个新伙计,有两种做法。

方式1:继承 Thread 类

java
// 定义一个线程类——继承 Thread,重写 run() 方法
class DownloadThread extends Thread {
    private String url;
    
    public DownloadThread(String url) {
        this.url = url;
    }
    
    @Override
    public void run() {
        // run() 里的代码在新线程中执行
        for (int i = 0; i <= 100; i += 25) {
            System.out.println("下载 " + url + ": " + i + "%");
            try {
                Thread.sleep(500);  // 模拟网络延迟,暂停 500 毫秒
            } catch (InterruptedException e) {
                System.out.println("下载被中断");
            }
        }
        System.out.println(url + " 下载完成!");
    }
}

// 主程序——启动两个线程"同时"下载
public class ThreadDemo {
    public static void main(String[] args) {
        System.out.println("主线程开始");
        
        DownloadThread t1 = new DownloadThread("https://example.com/file1.zip");
        DownloadThread t2 = new DownloadThread("https://example.com/file2.zip");
        
        // 注意:调 start(),不是 run()!
        t1.start();
        t2.start();
        
        System.out.println("主线程继续——不等待下载完成");
    }
}

语言:Java 示例输出(每次运行可能不同):

主线程开始
主线程继续——不等待下载完成
下载 https://example.com/file1.zip: 0%
下载 https://example.com/file2.zip: 0%
下载 https://example.com/file2.zip: 25%
下载 https://example.com/file1.zip: 25%
...

关键区分

  • thread.start() —— 创建一个新线程,新线程执行 run() 方法
  • thread.run() —— 不创建新线程!直接在当前线程中执行 run() 方法——和普通方法调用一样

方式2:实现 Runnable 接口(推荐)

继承 Thread 的问题:Java 是单继承——如果一个类已经继承了其他类,就无法再继承 Thread。

java
// Runnable 接口:就是一个"能被线程执行的任务"
class DownloadTask implements Runnable {
    private String url;
    
    public DownloadTask(String url) {
        this.url = url;
    }
    
    @Override
    public void run() {
        System.out.println("开始下载: " + url + " 在线程 " + 
            Thread.currentThread().getName());
        // 模拟下载...
        try { Thread.sleep(1000); } catch (InterruptedException e) { }
        System.out.println(url + " 下载完成");
    }
}

public class RunnableDemo {
    public static void main(String[] args) {
        // Thread 接收一个 Runnable 对象
        Thread t1 = new Thread(new DownloadTask("file1.zip"), "下载线程-1");
        Thread t2 = new Thread(new DownloadTask("file2.zip"), "下载线程-2");
        
        t1.start();
        t2.start();
        
        System.out.println("活跃线程数: " + Thread.activeCount());
    }
}

语言:Java 示例输出

开始下载: file1.zip 在线程 下载线程-1
开始下载: file2.zip 在线程 下载线程-2
活跃线程数: 3    // 主线程 + 两个下载线程
file1.zip 下载完成
file2.zip 下载完成

Java 8+ 简洁写法(Lambda)

java
// Runnable 是函数式接口——可以用 Lambda
Thread t = new Thread(() -> {
    System.out.println("在线程中执行: " + Thread.currentThread().getName());
});
t.start();

语言:Java 8+

🪟 差异窗口

Python 创建线程:

python
import threading
import time

def download(url):
    print(f"开始下载: {url}")
    time.sleep(1)
    print(f"{url} 下载完成")

# 创建并启动线程
t1 = threading.Thread(target=download, args=("file1.zip",))
t2 = threading.Thread(target=download, args=("file2.zip",))
t1.start()
t2.start()

Python 的 threading.Thread(target=func) 相当于 Java 的 new Thread(runnable)。差异在于:

  • Python 的 GIL(全局解释器锁)——多个线程并不能真正并行执行 CPU 密集计算;它们轮流持有 GIL
  • Java 的线程是真正的 OS 线程——多核下可以真正并行运行

C++11 创建线程:

cpp
`#include <thread>`
`#include <iostream>`

void download(std::string url) {
    std::cout << "下载 " << url << std::endl;
}

int main() {
    std::thread t1(download, "file1.zip");
    std::thread t2(download, "file2.zip");
    
    t1.join();  // 等价于 Java 的 t1.join()
    t2.join();
    return 0;
}

C++ 用 std::thread 直接创建线程,join() 等待线程结束。C++ 没有 GC,资源管理更复杂——但线程是真正的 OS 线程。


第二幕:生老病死——线程的六个状态

一个线程从创建到终结,总要经历一些阶段。

java
public class ThreadStateDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            System.out.println("线程运行中...");
            try {
                Thread.sleep(2000);  // 进入 TIMED_WAITING 状态
            } catch (InterruptedException e) {
                //
            }
        });
        
        System.out.println("1. 刚创建: " + t.getState());      // NEW
        t.start();
        System.out.println("2. 启动后: " + t.getState());      // RUNNABLE(可能是)
        
        Thread.sleep(100);  // 让线程有时间进入 sleep
        System.out.println("3. sleep 中: " + t.getState());    // TIMED_WAITING
        
        t.join();  // 等待线程结束
        System.out.println("4. 结束后: " + t.getState());      // TERMINATED
    }
}

语言:Java 预期输出

1. 刚创建: NEW
2. 启动后: RUNNABLE
3. sleep 中: TIMED_WAITING
线程运行中...
4. 结束后: TERMINATED

线程六状态一览

状态什么时候进入
NEWnew Thread() 之后,start() 之前
RUNNABLEstart() 后——正在运行或等待 CPU 调度
BLOCKED等待进入 synchronized 同步块/方法(被锁挡在外面)
WAITING调用了 wait() / join()(无限期等别人唤醒)
TIMED_WAITINGsleep(ms) / wait(timeout) / join(ms)(带超时的等待)
TERMINATEDrun() 执行完毕,或者因未捕获异常终止
                    ┌───────────┐
                    │   NEW     │ 
                    └─────┬─────┘
                          │ start()

                    ┌───────────┐
              ┌────►│ RUNNABLE  │◄──── CPU 重新调度
              │     └──┬───┬───┘
              │        │   │
              │ 获取锁◄─┘   │ 调用 wait/join/sleep
              │  失败      │
              │    ┌───────┘
              │    ▼
    ┌───────────┐  ┌──────────────┐
    │  BLOCKED  │  │  WAITING 或  │
    └───────────┘  │ TIMED_WAITING│
                   └──────┬───────┘
              ▲           │ 被唤醒/超时
              │           ▼
              └──────────┐

                    ┌───────────────┐
                    │  TERMINATED   │
                    └───────────────┘

关键理解:RUNNABLE 不意味"正在执行"——它意味着"可以被 CPU 调度"。实际上 CPU 核心数有限(比如 4 核),但可能有 20 个线程在 RUNNABLE 状态。操作系统调度器决定哪个线程真正占用 CPU——这是线程切换。Java 开发者看不到"运行中 vs 等待调度"的区别,JVM 把它们归为 RUNNABLE。


第三幕:锁住——synchronized

问题来了。两个线程同时修改同一个变量,会发生什么?

java
public class CounterProblem {
    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: " + count);  // 期望 20000,但往往更少
    }
}

语言:Java 示例输出(不稳定):

最终 count: 19973

为什么不到 20000? count++ 实际上分解为三步:

  1. 从内存读取 count 的值
  2. CPU 中 +1
  3. 写回内存

如果两个线程同时执行第 1 步——都读到 100,然后都 +1 写回 101——明明加了两次,实际只加了 1。这叫竞争条件(race condition)。

解决方案:synchronized——给代码加锁

java
public class SafeCounter {
    private int count = 0;
    
    // synchronized 方法——同一时刻只有一个线程能进入
    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());  // 总是 20000
    }
}

语言:Java 预期输出

安全计数器结果: 20000

synchronized 的两种形式

java
// 形式1:同步方法——锁住整个方法
public synchronized void increment() {  // 等价于 synchronized(this) { ... }
    count++;
}

// 形式2:同步代码块——只锁住需要保护的几行代码
public void increment() {
    // 这里是"非同步区"——不需要保护
    long start = System.nanoTime();
    
    synchronized (this) {  // 用 this 作为"锁对象"
        count++;           // 这一小段才需要同步
    }
    
    // 其他非同步操作
    long elapsed = System.nanoTime() - start;
}

synchronized 的锁机制

每个 Java 对象都有一个内置锁(监视器——monitor)。synchronized 获取这个锁。

  • 线程 A 进入 synchronized 块 → 获取锁 → 执行代码
  • 线程 B 也想进入同样的 synchronized 块 → 发现锁被 A 持有 → 阻塞(BLOCKED 状态)→ 等待 A 释放
  • 线程 A 退出 synchronized 块 → 释放锁 → 线程 B 拿到锁 → 继续
java
public class LockDemo {
    private final Object lock = new Object();  // 专门的锁对象
    
    public void doSomething() {
        synchronized (lock) {  // 多线程同时进入这里,只有一个能成功
            // 临界区——每次只允许一个线程执行
            System.out.println(Thread.currentThread().getName() + " 进入");
            try { Thread.sleep(1000); } catch (InterruptedException e) {}
            System.out.println(Thread.currentThread().getName() + " 退出");
        }
    }
    
    public static void main(String[] args) {
        LockDemo demo = new LockDemo();
        
        new Thread(demo::doSomething, "线程A").start();
        new Thread(demo::doSomething, "线程B").start();
        new Thread(demo::doSomething, "线程C").start();
    }
}

语言:Java 预期输出

线程A 进入
线程A 退出
线程C 进入
线程C 退出
线程B 进入
线程B 退出

🪟 差异窗口

Python 的锁:

python
import threading

lock = threading.Lock()
count = 0

def increment():
    global count
    for _ in range(10000):
        with lock:    # 等价于 synchronized
            count += 1

Python 也有 GIL,所以 count += 1 在纯 Python 层面其实是受 GIL 保护的——但不代表不会丢更新。with lock 是显式锁。

C++ 的锁:

cpp
`#include <mutex>`
`#include <thread>`

std::mutex mtx;
int count = 0;

void increment() {
    for (int i = 0; i < 10000; i++) {
        std::lock_guard<std::mutex> lock(mtx);  // RAII 锁——离开作用域自动释放
        count++;
    }
}

std::lock_guard 是 C++ 中管理锁生命周期的惯用方式——构造时上锁,析构时释放。Java 的 synchronized 块结束后也自动释放。


第四幕:你看不到的变化——volatile

synchronized 是重型武器——它同时保证原子性(操作不被打断)和可见性(一个线程的修改立刻被其他线程看到)。

但有些场景你只需要"可见性":

java
// 问题代码:线程可能永远看不到 flag 的变化
class FlagExample {
    private static boolean running = true;
    
    public static void main(String[] args) throws InterruptedException {
        Thread worker = new Thread(() -> {
            while (running) {   // 这行可能被优化成:if (!running) 死循环
                // 做一些工作
            }
            System.out.println("工作线程退出");
        });
        worker.start();
        
        Thread.sleep(1000);
        running = false;        // 主线程改了——但 worker 可能看不到
        System.out.println("主线程设置 running=false");
    }
}

语言:Java 可能出现的问题:主线程改了 running = false,但工作线程的 while 循环永远停不下来。为什么?因为 JIT 编译器可能把 while (running) 优化成 if (!running) { 死循环 }——它认为 running 永远不会被其他线程改。

解决方案:volatile——轻量级的"共享通知"

java
class VolatileExample {
    // volatile 告诉 JVM:这个变量可能被多个线程修改,不要做任何"永远不变"的优化
    private static volatile 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;   // 这个修改立刻对 worker 可见
        System.out.println("主线程设置 running=false");
    }
}

语言:Java 预期输出

主线程设置 running=false
工作线程退出

volatile 能做什么、不能做什么

volatile 能保证volatile 不能保证
可见性——写 volatile 变量的值立刻对其他线程可见原子性——volatile count++ 仍然不是安全的
禁止指令重排序(保证读写的顺序不被编译器打乱)复合操作(check-then-act)仍需要 synchronized

使用场景:标志位(runningshutdown)、状态开关——"纯读"和"纯写"的简单场景。涉及"读-改-写"的复合操作,必须用synchronized。


第五幕:线程间通信——wait/notify

你已经可以用锁保护共享数据了。但如果线程之间需要"你干完了,换我来"呢?

经典场景:生产者-消费者问题。一个线程生产数据,另一个线程消费数据——当队列满了,生产者等待;当队列空了,消费者等待。

java
import java.util.LinkedList;
import java.util.Queue;

class SharedQueue {
    private Queue<String> queue = new LinkedList<>();
    private final int MAX_SIZE = 5;
    
    // 生产者调用——往队列放数据
    public synchronized void produce(String item) throws InterruptedException {
        // 关键:用 while 而不是 if 检查条件
        while (queue.size() == MAX_SIZE) {
            System.out.println("队列满了," + Thread.currentThread().getName() + " 等待...");
            wait();   // 释放锁,进入 WAITING 状态。等待被 notify 唤醒
        }
        
        queue.add(item);
        System.out.println("生产: " + item + " | 队列大小: " + queue.size());
        notifyAll();  // 通知等待的消费者:有数据了,来拿吧
    }
    
    // 消费者调用——从队列取数据
    public synchronized void consume() throws InterruptedException {
        while (queue.isEmpty()) {
            System.out.println("队列空了," + Thread.currentThread().getName() + " 等待...");
            wait();   // 等待生产者放数据
        }
        
        String item = queue.poll();
        System.out.println("消费: " + item + " | 队列大小: " + queue.size());
        notifyAll();  // 通知等待的生产者:有空间了,来放吧
    }
}

public class ProducerConsumerDemo {
    public static void main(String[] args) {
        SharedQueue sq = new SharedQueue();
        
        // 生产者线程
        Thread producer = new Thread(() -> {
            try {
                for (int i = 1; i <= 10; i++) {
                    sq.produce("订单-" + i);
                    Thread.sleep(200);  // 生产速度
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }, "生产者");
        
        // 消费者线程
        Thread consumer = new Thread(() -> {
            try {
                for (int i = 1; i <= 10; i++) {
                    sq.consume();
                    Thread.sleep(500);  // 消费速度慢于生产——队列会满
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }, "消费者");
        
        producer.start();
        consumer.start();
    }
}

语言:Java 示例输出

生产: 订单-1 | 队列大小: 1
生产: 订单-2 | 队列大小: 2
消费: 订单-1 | 队列大小: 1
生产: 订单-3 | 队列大小: 2
生产: 订单-4 | 队列大小: 3
生产: 订单-5 | 队列大小: 4
生产: 订单-6 | 队列大小: 5
队列满了,生产者 等待...
消费: 订单-2 | 队列大小: 4
生产: 订单-7 | 队列大小: 5
...

wait/notify 黄金三规则

  1. 必须在 synchronized 块/方法中调用 —— 因为 wait() 要求当前线程持有锁
  2. 调用 wait() 后,线程释放锁并进入 WAITING 状态 —— 注意:释放锁了
  3. notify() 唤醒后,线程重新竞争锁 —— 拿到锁后才能继续执行
方法作用
wait()释放锁,进入 WAITING,等别人 notify
wait(timeout)释放锁,最多等 timeout ms,超时自动醒
notify()随机唤醒一个 WAITING 线程
notifyAll()唤醒所有 WAITING 线程(推荐——避免死等)

为什么建议用 notifyAll() 而不是 notify()?如果队列里同时有多个生产者和多个消费者在等——notify() 可能唤醒了一个生产者,但实际需要唤醒的是消费者——线程白等。


🧭 本章只做概念引入:线程池的详细参数调优、拒绝策略、ThreadPoolExecutor 构造器在 Vol 3 中系统讲解。

第六幕:线程池——不要每次 new Thread()

每次需要并发就 new Thread(...)——有个问题:创建线程是有开销的(分配栈空间、注册 OS 线程)。大量短生命周期的线程甚至会让系统变慢。另外,如果同时创建了 1000 个线程——系统资源直接爆了。

线程池:提前创建好一组线程,把任务交给它们执行。线程重用,资源可控。

java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolDemo {
    public static void main(String[] args) {
        // 创建一个固定大小的线程池(4 个线程)
        ExecutorService pool = Executors.newFixedThreadPool(4);
        
        // 提交 10 个任务——池子里只有 4 个线程,任务排队执行
        for (int i = 1; i <= 10; i++) {
            int taskId = i;
            pool.submit(() -> {
                System.out.println("任务 " + taskId + " 由线程 "
                    + Thread.currentThread().getName() + " 执行");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
        }
        
        // 关闭线程池——不再接受新任务,已提交的任务继续执行
        pool.shutdown();
        // pool.shutdownNow();  // 尝试停止正在执行的任务——慎用
        
        System.out.println("主线程结束——但线程池里的任务还在跑");
    }
}

语言:Java 示例输出(线程名规律):

任务 1 由线程 pool-1-thread-1 执行
任务 2 由线程 pool-1-thread-2 执行
任务 3 由线程 pool-1-thread-3 执行
任务 4 由线程 pool-1-thread-4 执行
主线程结束——但线程池里的任务还在跑
任务 5 由线程 pool-1-thread-1 执行  // 线程1 完成了任务1,复用
任务 6 由线程 pool-1-thread-2 执行  // 线程2 复用
...

Executors 常用工厂方法

工厂方法线程数队列适用场景
newFixedThreadPool(n)固定 n 个无界队列任务量可控,知道最优线程数
newCachedThreadPool()动态增减同步队列(不排队)大量短任务,线程空闲 60s 被回收
newSingleThreadExecutor()1 个无界队列需要顺序执行任务
newScheduledThreadPool(n)固定 n 个定时任务、周期性任务

生产环境警告Executors 工厂方法创建的线程池在高压下可能有问题。例如 newFixedThreadPoolLinkedBlockingQueue(无界队列)——任务堆积太多会导致 OOM。更安全的做法是用 ThreadPoolExecutor 构造函数直接控制队列大小和拒绝策略:

java
new ThreadPoolExecutor(
    2,                          // 核心线程数
    10,                         // 最大线程数
    60, TimeUnit.SECONDS,       // 空闲线程存活时间
    new ArrayBlockingQueue<>(100), // 有界队列
    new ThreadPoolExecutor.CallerRunsPolicy()  // 队列满时,谁提交谁执行
);

Callable——有返回值的任务

java
import java.util.concurrent.*;

public class CallableDemo {
    public static void main(String[] args) throws Exception {
        ExecutorService pool = Executors.newFixedThreadPool(3);
        
        // 提交一个有返回值的任务
        Future<Integer> future = pool.submit(() -> {
            System.out.println("计算中...");
            Thread.sleep(1000);
            return 42;
        });
        
        System.out.println("主线程继续做别的事...");
        
        // get() 会阻塞,直到任务完成——类似于 "join()"
        Integer result = future.get();
        System.out.println("计算结果: " + result);  // 42
        
        pool.shutdown();
    }
}

语言:Java 5+ 预期输出

主线程继续做别的事...
计算中...
计算结果: 42

🪟 差异窗口

Python 线程池:

python
from concurrent.futures import ThreadPoolExecutor
import time

def download(url):
    print(f"下载 {url}")
    time.sleep(1)
    return f"{url} 完成"

with ThreadPoolExecutor(max_workers=4) as executor:
    futures = [executor.submit(download, f"file{i}.zip") for i in range(10)]
    for future in futures:
        print(future.result())  # 等价于 Java 的 future.get()

with 块结束时自动 shutdown(),等待所有任务完成。

C++ 线程池: C++ 标准库没有内置线程池。你需要自己实现或用第三方(如 Intel TBB)。这也是为什么 Java 的 Executors 被广泛使用——它是标准库自带的。


常见陷阱

目标:一个多线程银行转账系统。

需求

  1. 有 10 个账户,每个初始余额 1000 元
  2. 启动 5 个线程,每个线程执行 100 次随机转账(金额 1-200 元)
  3. 每次转账:A 账户扣除金额,B 账户增加金额
  4. 转账过程中不能出现"余额变成负数"或"钱凭空消失"
  5. 所有线程结束后,输出所有账户余额,总和应等于 10×1000=10000 元

提示

  • 需要 synchronized 保护转账操作
  • 注意:不要只锁一个账户——要两个账户一起锁(否则可能出现死锁)
  • 转账逻辑:source.balance -= amount; target.balance += amount;

自测

  • 运行 5 次,每次总和都是 10000 吗?
  • 在转账前后添加 System.out.println(amount) 观察是否有中间态被读到?
  • 如果把 synchronized 去掉,结果会怎样?

通关挑战

场景:你被人问到 JVM 中一个经典的并发问题——实现一个"有界阻塞队列"(BlockingQueue)。

java
public class MyBlockingQueue<T> {
    private final Object[] items;
    private int count = 0;
    private int putIndex = 0;
    private int takeIndex = 0;
    
    public MyBlockingQueue(int capacity) {
        items = new Object[capacity];
    }
    
    // 向队列尾放入元素——如果队列满了,阻塞等待
    public void put(T item) throws InterruptedException {
        // 你的代码
    }
    
    // 从队列头取出元素——如果队列空了,阻塞等待
    public T take() throws InterruptedException {
        return null; // 你的代码
    }
    
    public int size() { return count; }
}

// 使用测试:
public class BlockingQueueTest {
    public static void main(String[] args) throws InterruptedException {
        MyBlockingQueue<Integer> bq = new MyBlockingQueue<>(3);
        
        // 生产者
        new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                try {
                    bq.put(i);
                    System.out.println("放入: " + i);
                } catch (InterruptedException e) { break; }
            }
        }).start();
        
        // 消费者(速度慢)
        new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                try {
                    Thread.sleep(500);  // 消费慢
                    Integer val = bq.take();
                    System.out.println("取出: " + val);
                } catch (InterruptedException e) { break; }
            }
        }).start();
    }
}

要求

  • 当队列满时,put() 阻塞,直到有空间
  • 当队列空时,take() 阻塞,直到有数据
  • 使用 wait() / notifyAll() 实现
  • 最终放入 10 个,取出 10 个,一个不少

验收标准

完成本章后,你应该能回答:

  • [ ] 继承 Thread 和实现 Runnable 有什么区别?为什么推荐后者?
  • [ ] 线程有哪六个状态?分别什么时候进入?
  • [ ] start()run() 的区别是什么?
  • [ ] count++ 为什么不是线程安全的?它分解成哪三步?
  • [ ] synchronized 块的作用是什么?它是如何通过锁实现的?
  • [ ] volatile 解决了什么问题?为什么它不能保证原子性?
  • [ ] wait()notify() 为什么必须在 synchronized 块内调用?
  • [ ] notify()notifyAll() 选哪个?
  • [ ] 线程池相比 new Thread() 的好处有哪些?
  • [ ] Executors.newFixedThreadPool(4) 创建的线程池参数是什么?
  • [ ] Future.get() 的作用和注意事项?
  • [ ] 什么是竞争条件?举个实际例子。

常见卡点

"为什么我调了 start() 但 run() 里的代码没有执行?"start() 是新线程的起点,但它要等 CPU 调度。JVM 不会保证线程立刻执行。调 start() 后不能保证线程已在运行——它只是进入了 RUNNABLE 状态等待调度。

"synchronized 方法和非 synchronized 方法能同时执行吗?" → 能。synchronized 只阻塞其他要进入同一个锁的 synchronized 方法/块。普通的非同步方法可以随时执行——这就是问题所在。

java
class Example {
    public synchronized void a() { /* 上锁了 */ }
    public void b() { /* 没锁——可以和 a() 同时执行 */ }
}

"为什么 wait() 一定要放在 while 循环里?" → 因为被 notify 唤醒后,线程不会自动重新检查条件。如果用 if 而不是 while,唤醒后直接往下执行——可是条件可能已经变了(另一个线程也消费了数据)。这叫虚假唤醒(spurious wakeup)。while 重新检查条件,确保万无一失。

"我的程序好像死锁了——线程全卡住了" → 死锁的必要条件:两个线程各自持有对方需要的锁。线程 A 持锁 1 等锁 2,线程 B 持锁 2 等锁 1。解决方法:总是按相同的顺序获取锁(先锁 ID 小的,再锁 ID 大的)。

"我用 volatile 声明了 int 类型,count++ 还是不对"volatile 不保证原子性。count++ 是"读-改-写"三步——三步之间其他线程可能修改。用 AtomicIntegersynchronized

"线程池里的任务抛了异常,我怎么都没看到?" → 线程池的任务抛出的未捕获异常不会在提交处被捕获。用 submit() 时,异常被包装在 Future 里——必须调用 future.get() 才会抛出。用 execute() 提交时,可以设置 UncaughtExceptionHandler


现在不需要理解

  • ReentrantLock / Condition:synchronized 的进阶替代品——功能更强(可中断锁、超时锁、公平锁),但目前 synchronized 够用
  • ReadWriteLock:读读不互斥、读写互斥——适合读多写少
  • AtomicInteger / AtomicReference:基于 CAS 的无锁原子类——性能比 synchronized 好,但原理(CAS / ABA 问题)现在不需要深究
  • Semaphore / CountDownLatch / CyclicBarrier:并发工具类 —— 第 14+ 章补充内容
  • ForkJoinPool:JDK 7+ 的"分治"线程池——适合递归任务拆分。Java 8 的 parallelStream 正是基于它
  • Java 内存模型(JMM)的 happens-before 规则:理解可见性的理论基础——现在知道"synchronized 保证可见性和原子性,volatile 只保证可见性"就够了
  • ThreadLocal:线程局部变量——每个线程有自己的副本
  • CompletableFuture:Java 8 引入的异步编程工具——后续章节会讲

旅人笔记

并发编程不是"会用语法"就能做好的——它是你思维方式的又一次转变。
以前你思考"这段代码怎么执行",现在你要想"多个这段代码同时执行会怎样"。
Thread = 人。Runnable = 人干的事。把人和事分开(Runnable),比让人直接做(继承 Thread)灵活。
NEW → RUNNABLE → BLOCKED/WAITING/TIMED_WAITING → TERMINATED。一生走过,不可逆。
start() 是起跑命令,run() 是跑本身。——调 run() 等于"替跑"(不建新线程)。
count++ 不是一步——是"读-改-写"三步。三个步骤之间别人插进来,数据就坏了。
synchronized = 给代码加锁。锁住的是对象,不是代码。

volatile = 轻量级可见性。"你看不到我改了"的问题,是 JIT 编译器优化出的幻觉。
wait/notify = 线程间的"红灯绿灯"。wait = 我被堵了,你们先走。notifyAll = 绿灯了,大家来试试。
永远用 while 检查等待条件,不用 if。你要防御"虚假唤醒"——系统底层给你的意外惊喜。
new Thread() 很便宜?不。大量短线程 = 性能杀手。用线程池——提前招好工人,来了任务就干。
死锁 = 循环等待。打破循环:锁的顺序——从低到高,一成不变。

多线程 Bug 的特点是:10 次运行对 9 次,剩下 1 次莫名其妙。你的第一反应不应该是"我这代码没问题啊"——而是"这可能是并发 bug"。

一句话总结:并发编程 = 管理多个线程对共享资源的访问顺序和可见性。

下一站预告

你现在已经知道怎么创建线程、怎么用 synchronized 保护数据、怎么用 wait/notify 协调、怎么用线程池了——这些是并发编程的地基。

但实际生产中,你极少直接操作 Thread 和 synchronized。Java 从 1.5 开始引入了 java.util.concurrent 包——这才是生产级的并发武器库。

下一章将进入这些工具:ReentrantLockCountDownLatchSemaphoreAtomicIntegerConcurrentHashMapCompletableFuture……以及更重要的——如何在线程池中优雅地编排异步任务。

下一站:【第14+章:JUC 与异步编程】

Built with VitePress | Software Systems Atlas