元数据卡
- 前置知识:第5章(方法与栈)、第6章(数组)
- 预计时间:90 分钟
- 核心难度:
- 阅读模式:🚀 高压模式(建议清醒时读)
- 完成标志:能理解多线程的基本概念并创建线程;能用 synchronized 保护共享数据;理解线程状态流转;了解线程池的基本用法
可选预览章:并发编程是中级到高级话题。 对于编程初学者建议仅扫描阅读——先建立"程序可以同时做多件事"+"共享数据很危险"的概念, 真正的线程同步、
volatile、线程池等内容将在 Vol 3 中系统学习。本章分层
- 必读(仅预览):进程 vs 线程的基本概念、"程序可以同时做多件事"的直观印象、共享数据 + 并发修改 = 危险的警觉
- 选读:线程创建(
Thread/Runnable)、synchronized的基本语义- 深水区:线程六状态流转、
wait/notify的基本模式本章不会要求你掌握
volatile/AtomicInteger/ 线程池 —— 留到 Vol 3ReentrantLock/ReadWriteLock/ConcurrentHashMap—— 留到 Vol 3- 死锁的检测与避免策略(Vol 3 专题)
你在哪
变量村的图书馆系统上线了,阿花一个人用得好好的。可今天大促来了 100 个读者同时登录——系统慢得像蜗牛,有几笔借阅还莫名其妙重复了。
你写了一个图书管理系统。单人使用,完美。
然后你发现,你需要处理 100 个用户的并发访问——凌晨 0 点整,大促开始,100 个人同时抢同一本书的借阅权。你的系统运行在 8 核 CPU 上——可是你只用了 1 个线程先跑查询、再处理借阅、再返回结果——剩下 7 个核在一边凉快。
更糟的是,如果一个线程正在扣减库存,另一个线程同时读到旧库存,两个人都借到了同一本"最后一本书"——物理上只有一本,系统却借出了两本。
老陈师傅靠在椅背上,看着你抓狂的样子:"你学的东西都假设世界上只有一个人在运行程序。但真正的计算机——是同时干很多件事的。CPU 有多个核心,硬盘同时在读写,网络请求同时涌进来……"
"你不是一个人在战斗——你需要学会让代码在多个人手中不打架。"
你的任务
这章是你遇到的第一个"真正的难题"。并发编程的坑比你之前遇到的所有 Bug 加起来都多。但理解这些基础概念,会让你从"只会写单线程玩具"变成"能处理生产级负载的程序员"。
你将学会:
- 线程创建——Thread 类和 Runnable 接口,让你的程序"分身有术"
- 线程状态——从出生到死亡,一个线程经历的六个阶段
- synchronized——锁:让多个人排队操作共享资源
- volatile——轻量级同步:保证线程看到的是最新值
- wait/notify——线程间通信:"你干完了叫我"
- 线程池——不是每次创建新线程,而是用好"已有的劳工"
- 竞争条件与原子性——Bug 之王:为什么
count++在多线程里不是安全的
这不是一个"零基础"主题。如果你觉得跟不上了——后退一下,把前几章再看一遍。因为这些概念是串联的:线程是基础 → 锁是防护 → 线程池才是生产级方案。
遭遇战 → 获得技能
第一幕:分身有术——创建线程
你的 main 方法运行在"主线程"里。要从主线程分出一个新伙计,有两种做法。
方式1:继承 Thread 类
// 定义一个线程类——继承 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。
// 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):
// Runnable 是函数式接口——可以用 Lambda
Thread t = new Thread(() -> {
System.out.println("在线程中执行: " + Thread.currentThread().getName());
});
t.start();语言:Java 8+
🪟 差异窗口
Python 创建线程:
pythonimport 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 线程。
第二幕:生老病死——线程的六个状态
一个线程从创建到终结,总要经历一些阶段。
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线程六状态一览:
| 状态 | 什么时候进入 |
|---|---|
| NEW | new Thread() 之后,start() 之前 |
| RUNNABLE | start() 后——正在运行或等待 CPU 调度 |
| BLOCKED | 等待进入 synchronized 同步块/方法(被锁挡在外面) |
| WAITING | 调用了 wait() / join()(无限期等别人唤醒) |
| TIMED_WAITING | sleep(ms) / wait(timeout) / join(ms)(带超时的等待) |
| TERMINATED | run() 执行完毕,或者因未捕获异常终止 |
┌───────────┐
│ NEW │
└─────┬─────┘
│ start()
▼
┌───────────┐
┌────►│ RUNNABLE │◄──── CPU 重新调度
│ └──┬───┬───┘
│ │ │
│ 获取锁◄─┘ │ 调用 wait/join/sleep
│ 失败 │
│ ┌───────┘
│ ▼
┌───────────┐ ┌──────────────┐
│ BLOCKED │ │ WAITING 或 │
└───────────┘ │ TIMED_WAITING│
└──────┬───────┘
▲ │ 被唤醒/超时
│ ▼
└──────────┐
▼
┌───────────────┐
│ TERMINATED │
└───────────────┘关键理解:RUNNABLE 不意味"正在执行"——它意味着"可以被 CPU 调度"。实际上 CPU 核心数有限(比如 4 核),但可能有 20 个线程在 RUNNABLE 状态。操作系统调度器决定哪个线程真正占用 CPU——这是线程切换。Java 开发者看不到"运行中 vs 等待调度"的区别,JVM 把它们归为 RUNNABLE。
第三幕:锁住——synchronized
问题来了。两个线程同时修改同一个变量,会发生什么?
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++ 实际上分解为三步:
- 从内存读取 count 的值
- CPU 中 +1
- 写回内存
如果两个线程同时执行第 1 步——都读到 100,然后都 +1 写回 101——明明加了两次,实际只加了 1。这叫竞争条件(race condition)。
解决方案:synchronized——给代码加锁
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 预期输出:
安全计数器结果: 20000synchronized 的两种形式:
// 形式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 拿到锁 → 继续
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 的锁:
pythonimport threading lock = threading.Lock() count = 0 def increment(): global count for _ in range(10000): with lock: # 等价于 synchronized count += 1Python 也有 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 是重型武器——它同时保证原子性(操作不被打断)和可见性(一个线程的修改立刻被其他线程看到)。
但有些场景你只需要"可见性":
// 问题代码:线程可能永远看不到 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——轻量级的"共享通知"
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 使用场景:标志位(
running、shutdown)、状态开关——"纯读"和"纯写"的简单场景。涉及"读-改-写"的复合操作,必须用synchronized。
第五幕:线程间通信——wait/notify
你已经可以用锁保护共享数据了。但如果线程之间需要"你干完了,换我来"呢?
经典场景:生产者-消费者问题。一个线程生产数据,另一个线程消费数据——当队列满了,生产者等待;当队列空了,消费者等待。
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 黄金三规则:
- 必须在 synchronized 块/方法中调用 —— 因为
wait()要求当前线程持有锁 - 调用
wait()后,线程释放锁并进入 WAITING 状态 —— 注意:释放锁了 - 被
notify()唤醒后,线程重新竞争锁 —— 拿到锁后才能继续执行
| 方法 | 作用 |
|---|---|
wait() | 释放锁,进入 WAITING,等别人 notify |
wait(timeout) | 释放锁,最多等 timeout ms,超时自动醒 |
notify() | 随机唤醒一个 WAITING 线程 |
notifyAll() | 唤醒所有 WAITING 线程(推荐——避免死等) |
为什么建议用 notifyAll() 而不是 notify()?如果队列里同时有多个生产者和多个消费者在等——notify() 可能唤醒了一个生产者,但实际需要唤醒的是消费者——线程白等。
🧭 本章只做概念引入:线程池的详细参数调优、拒绝策略、
ThreadPoolExecutor构造器在 Vol 3 中系统讲解。
第六幕:线程池——不要每次 new Thread()
每次需要并发就 new Thread(...)——有个问题:创建线程是有开销的(分配栈空间、注册 OS 线程)。大量短生命周期的线程甚至会让系统变慢。另外,如果同时创建了 1000 个线程——系统资源直接爆了。
线程池:提前创建好一组线程,把任务交给它们执行。线程重用,资源可控。
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工厂方法创建的线程池在高压下可能有问题。例如newFixedThreadPool用LinkedBlockingQueue(无界队列)——任务堆积太多会导致 OOM。更安全的做法是用ThreadPoolExecutor构造函数直接控制队列大小和拒绝策略:javanew ThreadPoolExecutor( 2, // 核心线程数 10, // 最大线程数 60, TimeUnit.SECONDS, // 空闲线程存活时间 new ArrayBlockingQueue<>(100), // 有界队列 new ThreadPoolExecutor.CallerRunsPolicy() // 队列满时,谁提交谁执行 );
Callable——有返回值的任务:
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 线程池:
pythonfrom 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被广泛使用——它是标准库自带的。
常见陷阱
目标:一个多线程银行转账系统。
需求:
- 有 10 个账户,每个初始余额 1000 元
- 启动 5 个线程,每个线程执行 100 次随机转账(金额 1-200 元)
- 每次转账:A 账户扣除金额,B 账户增加金额
- 转账过程中不能出现"余额变成负数"或"钱凭空消失"
- 所有线程结束后,输出所有账户余额,总和应等于 10×1000=10000 元
提示:
- 需要
synchronized保护转账操作 - 注意:不要只锁一个账户——要两个账户一起锁(否则可能出现死锁)
- 转账逻辑:
source.balance -= amount; target.balance += amount;
自测:
- 运行 5 次,每次总和都是 10000 吗?
- 在转账前后添加
System.out.println(amount)观察是否有中间态被读到? - 如果把 synchronized 去掉,结果会怎样?
通关挑战
场景:你被人问到 JVM 中一个经典的并发问题——实现一个"有界阻塞队列"(BlockingQueue)。
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 方法/块。普通的非同步方法可以随时执行——这就是问题所在。
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++ 是"读-改-写"三步——三步之间其他线程可能修改。用 AtomicInteger 或 synchronized。
"线程池里的任务抛了异常,我怎么都没看到?" → 线程池的任务抛出的未捕获异常不会在提交处被捕获。用 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 包——这才是生产级的并发武器库。
下一章将进入这些工具:ReentrantLock、CountDownLatch、Semaphore、AtomicInteger、ConcurrentHashMap、CompletableFuture……以及更重要的——如何在线程池中优雅地编排异步任务。
下一站:【第14+章:JUC 与异步编程】