并发编程平时零零散散都接触过——volatile、synchronized、线程池、AtomicInteger 都写过,但"它们各自解决什么问题、为什么要一起用"一直没系统梳理。这篇把 Java 并发最核心的几块串起来:线程安全的三大问题、每把"锁/关键字"解决哪个、以及线程池怎么配。
一、解决什么问题:多线程下的共享数据安全
单线程 count++ 没问题,多线程同时 count++ 就出事了:
public class Counter {
private int count = 0;
public void increment() { count++; } // 不是原子操作!
}count++ 底层是三步:读 count → 加 1 → 写回 count。两个线程同时执行,可能都读到 0、都加 1、都写回 1——结果本该是 2,实际是 1。这就是"线程不安全"。
二、线程安全的三大问题
多线程出问题,根源就三个:
| 问题 | 是什么 | 例子 |
|---|---|---|
| 可见性 | 一个线程改了变量,另一个线程看不到 | 线程 A 改 flag=true,线程 B 一直读不到 |
| 原子性 | 一段操作不可分割,中间不能被打断 | count++ 三步被打断,结果丢失 |
| 有序性 | 指令重排,代码执行顺序变了 | 单例双重检查的 instance = new X() 重排 |
Java 的并发工具,就是分别解决这三个问题的——一把工具对一个问题。
三、volatile:解决可见性 + 有序性(不解决原子性)
volatile 保证:一个线程改了变量,其他线程立刻能看到(可见性),且禁止指令重排(有序性)。
// 可见性:线程 A 改 flag,线程 B 立刻看到
volatile boolean flag = false;
// 线程 A:
flag = true;
// 线程 B:能看到 flag 变成 true,退出循环
while (!flag) { /* 等待 */ }关键:volatile 不保证原子性。volatile int count; count++ 依然可能丢数据(三步操作,volatile 管不住中间被打断)。原子性要靠 synchronized 或原子类。
想彻底搞懂 volatile 的可见性、有序性背后的内存模型,看《volatile 关键字详解》。
四、synchronized:解决原子性 + 可见性
synchronized 保证一段代码同一时刻只有一个线程执行(原子性),且执行结果对其他线程可见(可见性)。
private int count = 0;
private final Object lock = new Object();
public void increment() {
synchronized (lock) { // 同一时刻只允许一个线程进入
count++; // 原子 + 可见
}
}synchronized 是"重量级"方案(涉及锁竞争、可能阻塞线程),简单可靠,但性能比 CAS 差。
五、CAS 和原子类:无锁实现原子性
CAS(Compare-And-Swap,比较并交换)是无锁的原子操作:先比较内存值是不是预期值,是才更新,否则重试。
// AtomicInteger 内部就是 CAS + 自旋
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // 原子地 +1,无锁、高性能CAS 的经典应用是"自旋"——更新失败就重试,直到成功。它比 synchronized 性能好(不用阻塞线程),但只能保证单个变量的原子性(多个变量的复合操作还是要锁)。
// CAS 的三个参数:内存值、预期值、新值
// 只有"内存值 == 预期值"才更新,否则重试
boolean ok = atomicValue.compareAndSet(expect, update);六、线程池:复用线程,别每次 new Thread
每次任务都 new Thread() 很浪费(线程创建销毁有开销),线程池复用线程:
ThreadPoolExecutor pool = new ThreadPoolExecutor(
2, // 核心线程数
4, // 最大线程数
60, TimeUnit.SECONDS, // 空闲线程存活时间
new LinkedBlockingQueue<>(100), // 任务队列
new ThreadPoolExecutor.AbortPolicy() // 队列满了的拒绝策略
);
pool.execute(() -> { /* 任务 */ });线程池的工作流程(面试高频):
任务来了 → 核心线程满了没?没满就开核心线程
→ 满了 → 队列满了没?没满就进队列
→ 队列也满了 → 最大线程满了没?没满就开新线程(到最大)
→ 都满了 → 走拒绝策略(AbortPolicy 抛异常等)核心参数记忆:核心线程数、最大线程数、队列、拒绝策略——这四个决定线程池怎么消化任务。
小结
- 线程安全三大问题:可见性、原子性、有序性
volatile管可见性 + 有序性,不管原子性synchronized管原子性 + 可见性(重量级、阻塞)- CAS/原子类管原子性(无锁、高性能,但只限单变量)
- 线程池复用线程,核心是"核心线程 → 队列 → 最大线程 → 拒绝策略"
想了解并发更深的东西(锁的升级、AQS、并发集合),或者 JVM 怎么支撑这些(内存模型、GC),可以继续看《JVM 内存模型与垃圾回收》。
