主题
Chapter 17 · 多线程与并发基础
一个程序能同时做多件事 — 现代多核 CPU 时代的必修课。
🎯 本章目标
- 理解 进程 vs 线程 的区别
- 掌握创建线程的 4 种方式
- 学会 synchronized、volatile、ReentrantLock
- 理解 线程池 的必要性
- 知道
wait/notify与await/signal
1. 进程 vs 线程
| 进程(Process) | 线程(Thread) | |
|---|---|---|
| 单位 | 资源分配 | CPU 调度 |
| 内存 | 独立内存空间 | 共享进程内存 |
| 通信 | 进程间通信(IPC,复杂) | 共享变量(简单但要锁) |
| 切换开销 | 大 | 小 |
| 类比 | 一座工厂 | 工厂里的工人 |
💡 一个 JVM 进程里有 N 个线程。Tomcat、Spring、Netty 全是基于多线程。
2. 创建线程的 4 种方式
2.1 继承 Thread
java
class MyThread extends Thread {
@Override
public void run() {
System.out.println("线程跑起来了:" + getName());
}
}
new MyThread().start(); // 注意是 start(),不是 run()!2.2 实现 Runnable(推荐)
java
class MyTask implements Runnable {
public void run() { System.out.println("running"); }
}
new Thread(new MyTask()).start();
// Lambda 简化
new Thread(() -> System.out.println("Lambda task")).start();💡 优先用 Runnable:Java 单继承,继承 Thread 就不能继承别的了。
2.3 实现 Callable + Future(带返回值)
java
Callable<Integer> task = () -> {
Thread.sleep(1000);
return 42;
};
ExecutorService pool = Executors.newFixedThreadPool(2);
Future<Integer> future = pool.submit(task);
Integer result = future.get(); // 阻塞等结果
pool.shutdown();2.4 线程池(生产环境必用)
java
ExecutorService pool = Executors.newFixedThreadPool(4);
for (int i = 0; i < 10; i++) {
int id = i;
pool.submit(() -> System.out.println("任务 " + id + " by " + Thread.currentThread().getName()));
}
pool.shutdown();3. 线程的生命周期
NEW (新建)
│ start()
▼
RUNNABLE (可运行)
│ ←→ 操作系统调度
▼
RUNNING (运行中)
│
├── sleep() / 阻塞 IO ──→ TIMED_WAITING / WAITING / BLOCKED
│ │
│ │ 唤醒/超时
▼ │
TERMINATED (终止) ←────────────────┘| 状态 | 触发 |
|---|---|
| NEW | new Thread() 之后 |
| RUNNABLE | start() 调用后 |
| BLOCKED | 等待 synchronized 锁 |
| WAITING | wait() / join() / park() |
| TIMED_WAITING | sleep(n) / wait(n) / join(n) |
| TERMINATED | run() 结束 |
4. 线程安全:为什么要加锁?
java
class Counter {
int count = 0;
void inc() { count++; } // ❌ 看似一行,实际三步
}
Counter c = new Counter();
// 100 个线程,每个加 1000
// 期望: 100000,实际:可能只有 80000+count++ = 读 + 加 + 写,多线程交叉执行时丢更新。
5. synchronized 关键字
三种写法
java
class Counter {
int count = 0;
public synchronized void inc1() { // 锁 this
count++;
}
public void inc2() {
synchronized (this) { // 同步块
count++;
}
}
public static synchronized void inc3() { // 锁 Counter.class
// ...
}
private final Object lock = new Object();
public void inc4() {
synchronized (lock) { // 锁专用对象(推荐)
count++;
}
}
}几个要点
- 实例方法
synchronized→ 锁 this - 静态方法
synchronized→ 锁 类对象(Counter.class) - 同一把锁是 可重入 的(同一个线程可以反复进入)
6. volatile 关键字
"保证可见性,禁止重排序"
java
class Worker {
private volatile boolean running = true;
public void work() {
while (running) {
// do work
}
}
public void stop() { running = false; }
}不加 volatile 时,JIT 可能优化让 running 一直从寄存器读,主线程改了也看不到 → 死循环。
⚠️ volatile 不保证原子性!
count++加 volatile 仍然不安全。
| 关键字 | 原子性 | 可见性 | 有序性 |
|---|---|---|---|
| volatile | ❌ | ✓ | ✓ |
| synchronized | ✓ | ✓ | ✓ |
| AtomicInteger | ✓ | ✓ | ✓ |
7. ReentrantLock:更灵活的锁
java
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
// 临界区
} finally {
lock.unlock(); // 必须在 finally 里!
}比 synchronized 强在哪?
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 可中断 | ❌ | ✓ lockInterruptibly() |
| 公平锁 | ❌ | ✓ new ReentrantLock(true) |
| 尝试获取 | ❌ | ✓ tryLock() |
| 多 Condition | ❌ | ✓ newCondition() |
| 关键字简洁 | ✓ | ❌ |
8. 线程间通信
8.1 wait / notify(synchronized 配套)
java
synchronized (lock) {
while (条件不满足) lock.wait(); // 释放锁,等
// 处理
lock.notifyAll(); // 唤醒所有
}8.2 Condition(ReentrantLock 配套)
java
ReentrantLock lock = new ReentrantLock();
Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();
// 生产者
lock.lock();
try {
while (满了) notFull.await();
放入();
notEmpty.signal();
} finally { lock.unlock(); }9. 线程池
反复创建线程是最大的性能浪费!
java
// ❌ 不推荐(无限队列,可能 OOM)
ExecutorService pool = Executors.newFixedThreadPool(10);
// ✅ 推荐(自定义参数,可控)
ThreadPoolExecutor pool = new ThreadPoolExecutor(
4, // 核心线程数
8, // 最大线程数
60, TimeUnit.SECONDS, // 空闲存活时间
new ArrayBlockingQueue<>(100), // 工作队列
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);7 大参数
corePoolSize- 核心线程数maximumPoolSize- 最大线程数keepAliveTime- 空闲存活时间unit- 时间单位workQueue- 工作队列threadFactory- 线程工厂(命名)handler- 拒绝策略(队列满 + 线程满)
4 种拒绝策略
| 策略 | 行为 |
|---|---|
| AbortPolicy | 抛 RejectedExecutionException(默认) |
| CallerRunsPolicy | 让调用者自己执行 |
| DiscardPolicy | 默默丢弃 |
| DiscardOldestPolicy | 丢弃最老的,再加入 |
10. 实战练习
| 文件 | 内容 |
|---|---|
ThreadCreate.java | 4 种创建方式 |
UnsafeCounter.java | 不加锁 vs 加锁的差异 |
SynchronizedDemo.java | synchronized 三种写法 |
VolatileDemo.java | volatile 可见性 |
ProducerConsumer.java | 生产者-消费者 |
ThreadPoolDemo.java | 自定义线程池 |
11. 浏览器演示
打开 demo.html:
- 线程生命周期动画
- 竞态条件可视化
- 线程池工作过程
12. 面试可能会问什么?
Q1: 创建线程的方式?
- 继承 Thread
- 实现 Runnable(推荐)
- 实现 Callable + Future(带返回值)
- 线程池(生产用)
Q2: synchronized 和 ReentrantLock 区别?
| synchronized | ReentrantLock | |
|---|---|---|
| 形式 | 关键字 | 类 |
| 释放锁 | 自动 | 手动 unlock(必须 finally) |
| 可中断 | ❌ | ✓ |
| 公平性 | 非公平 | 可公平可非公平 |
| 多 Condition | ❌ | ✓ |
Q3: volatile 和 synchronized 区别?
- volatile:保证可见性 + 禁止重排序,但不保证原子性
- synchronized:全保证,但开销大
Q4: 线程池有哪些参数?
7 大参数(见上文),重点记 corePoolSize、maxPoolSize、queue、handler。
Q5: 为什么 Executors.newFixedThreadPool 不推荐?
底层用 无界队列 LinkedBlockingQueue,任务堆积可能 OOM。生产用 new ThreadPoolExecutor(...) 自己控制。
Q6: wait 和 sleep 区别?
| wait | sleep | |
|---|---|---|
| 来自 | Object | Thread |
| 释放锁 | ✓ | ❌(抱着锁睡) |
| 唤醒 | notify / 超时 | 时间到 / 中断 |
| 必须在 synchronized 里 | ✓ | ❌ |
Q7: 死锁怎么避免?
- 加锁顺序一致
- tryLock + 超时
- 减少锁粒度
- 用 jstack 排查(Found one Java-level deadlock)
🎁 本章小结
✅ 进程是资源单位,线程是调度单位
✅ 4 种创建方式:Thread / Runnable / Callable / 线程池
✅ synchronized 全能但粗暴;ReentrantLock 灵活但繁琐
✅ volatile 保证可见性,不保证原子性
✅ 生产用线程池,不要 new Thread()🔗 导航
- ⬅️ 上一章:Chapter 16 · IO 流
- ➡️ 下一章:Chapter 18 · Lambda 与 Stream API