多线程与并发基础
Java 中级 · Java 进阶
并发是 Java 最硬的一块骨头。本篇不满足于「会写 synchronized」,而是讲清楚:多线程到底共享了什么、锁是怎么升级的、volatile 靠什么保证可见性、线程池为什么不能乱用 Executors。这些是高级篇 JUC 源码的地基。
一、线程的本质与共享内存
1.1 原生线程模型
Java 线程与操作系统线程一一对应(Thread.start() 底层调用 pthread_create),调度权在 OS。所以「几百个线程」就会带来上下文切换开销。
java
Thread t = new Thread(() -> System.out.println("run in " + Thread.currentThread().getName()));
t.start(); // 启动新线程,进入就绪态
t.join(); // 主线程阻塞等待 t 结束| 状态 | 说明 |
|---|---|
NEW | 已创建未 start |
RUNNABLE | 可运行(含 OS 就绪+运行) |
BLOCKED | 等待进入 synchronized |
WAITING | wait() / join() / park() 无限等待 |
TIMED_WAITING | sleep(n) / wait(n) |
TERMINATED | 结束 |
1.2 共享内存带来的三个问题
java
// 经典可见性 bug:程序可能永不停止
static boolean flag = false;
new Thread(() -> { while (!flag) {} }).start();
Thread.sleep(100); flag = true; // 主线程改了,但子线程可能一直读到旧值(CPU 缓存)原因:每个线程有自己的工作内存(CPU 缓存),不是直接读写主内存。volatile / synchronized 负责在必要时刷新缓存。
二、synchronized:从偏向锁到重量级锁
synchronized 保证原子性、可见性、有序性。它的锁对象信息存在对象头的 Mark Word 里。
2.1 锁升级过程(JDK 15 前)
- 偏向锁:只有一个线程访问时,把线程 ID 写进 Mark Word,之后进入无需 CAS(JDK 15 后已默认禁用/废弃偏向锁)。
- 轻量级锁:多线程交替执行、无真正并发时,用 CAS 自旋抢占,避免线程挂起。
- 重量级锁:竞争激烈时升级为 OS 互斥量,未抢到的线程挂起(用户态→内核态切换,开销大)。
2.2 用法与陷阱
java
public class Counter {
private int count = 0;
public synchronized void incr() { count++; } // 锁 this
public static synchronized void staticIncr() {} // 锁 Counter.class
public void block() {
synchronized (this) { count++; } // 锁代码块,粒度更小
}
}锁的对象别用错
- 锁
this、锁X.class、锁一个private final Object lock是三把不同的锁,混用等于没锁。 - 别锁
String常量或Integer:常量池/缓存会复用对象,不同逻辑锁到同一个对象,互相阻塞。 - 别锁可变引用:锁对象一旦被重新赋值,锁就换了。
三、volatile:可见性与有序性(但不保证原子性)
volatile 让变量的读写直接作用于主内存,并通过内存屏障禁止重排。
java
private volatile boolean running = true;
public void stop() { running = false; } // 其它线程立刻能看到
public void run() { while (running) { /* ... */ } }| 保证 | volatile | synchronized |
|---|---|---|
| 可见性 | ✅ | ✅ |
| 有序性 | ✅(禁止重排) | ✅ |
| 原子性 | ❌(i++ 仍不安全) | ✅ |
java
// volatile 不保证原子性的反例:并发下结果 < 2000
private volatile int count = 0;
void incr() { count++; } // 读-改-写三步,volatile 挡不住交错
// 正解:AtomicInteger 或 synchronized内存屏障(底层实现):
text
volatile 写前:StoreStore 屏障(前面的普通写先落主存)
volatile 写后:StoreLoad 屏障(把写刷回主存,触发其它 CPU 缓存失效)
volatile 读后:LoadLoad / LoadStore 屏障四、JMM 与 happens-before
JMM(Java 内存模型)规定:只要满足 happens-before 规则,前一个操作的可见性与有序性就对后一个操作可见。
| happens-before 规则 | 含义 |
|---|---|
| 程序顺序 | 同一线程内,前面代码 hb 后面代码 |
| 锁规则 | 解锁 hb 后续对该锁的加锁 |
| volatile | 写 hb 后续读 |
| 传递性 | A hb B,B hb C ⇒ A hb C |
start() | 主线程 start 前的操作 hb 子线程 |
join() | 子线程所有操作 hb join 返回 |
五、线程池:别用 Executors
java
ThreadPoolExecutor pool = new ThreadPoolExecutor(
4, // corePoolSize 核心线程数
8, // maximumPoolSize 最大线程数
60L, TimeUnit.SECONDS, // 空闲回收时间(非核心线程)
new LinkedBlockingQueue<>(200), // 任务队列(有界!)
new ThreadFactory() { // 自定义线程名,便于排查
private final AtomicInteger i = new AtomicInteger(1);
public Thread newThread(Runnable r) {
return new Thread(r, "order-pool-" + i.getAndIncrement());
}
},
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略:调用者执行,天然反压
);5.1 执行流程
5.2 四种拒绝策略
| 策略 | 行为 |
|---|---|
AbortPolicy(默认) | 抛 RejectedExecutionException |
CallerRunsPolicy | 提交任务的线程自己执行(降速反压) |
DiscardPolicy | 静默丢弃 |
DiscardOldestPolicy | 丢队列最老任务再提交 |
为什么禁止 Executors.newXxx()
newFixedThreadPool/newSingleThreadExecutor用无界队列LinkedBlockingQueue,任务堆积会 OOM。newCachedThreadPool/newScheduledThreadPool最大线程数是Integer.MAX_VALUE,会无限创建线程耗尽资源。- 生产必须手动
new ThreadPoolExecutor,队列有界、线程命名、拒绝策略明确。
参数怎么定:
- CPU 密集:
核数 + 1 - IO 密集:
核数 × (1 + 平均等待时间/平均计算时间),通常核数 × 2起步,压测微调。
六、最佳实践
- 共享可变状态最小化:能用不可变对象、局部变量、
ThreadLocal就别共享。 - 优先
java.util.concurrent工具(AtomicInteger、ConcurrentHashMap、CountDownLatch),而不是裸synchronized。 - 线程池手动创建 + 监控队列长度/活跃数,接入告警。
ThreadLocal用完必须remove(),否则线程池复用会造成内存泄漏/串数据。- 避免死锁:多把锁按固定顺序获取,或用
tryLock(timeout)。
📚 官方文档与参考资料
- Java SE 21 · java.util.concurrent 包
- Java 教程 · Concurrency
- JSR-133 · Java 内存模型 FAQ
- ThreadPoolExecutor 源码(OpenJDK)
✏️ 练习与自测
- 用
javap或jol观察一个对象加锁前后 Mark Word 的变化,说明锁升级过程。 - 为什么
volatile int count的count++在并发下仍会丢结果?写出两种正确改法。 - 描述
ThreadPoolExecutor提交任务时的完整决策流程,并说明为什么newFixedThreadPool会导致 OOM。 - 写一个会产生死锁的例子,再用「固定加锁顺序」或
tryLock修复它。
本节小结
并发的三大问题——可见性、原子性、有序性,分别靠 volatile(可见+有序)、synchronized/原子类(原子)、内存屏障(有序)解决。synchronized 有偏向→轻量→重量的锁升级;volatile 不保证原子性。线程池手动创建、队列有界、线程命名、拒绝策略明确,禁用 Executors 工厂方法。