Skip to content

并发编程平时零零散散都接触过——volatilesynchronized、线程池、AtomicInteger 都写过,但"它们各自解决什么问题、为什么要一起用"一直没系统梳理。这篇把 Java 并发最核心的几块串起来:线程安全的三大问题、每把"锁/关键字"解决哪个、以及线程池怎么配。

一、解决什么问题:多线程下的共享数据安全

单线程 count++ 没问题,多线程同时 count++ 就出事了:

java
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 保证:一个线程改了变量,其他线程立刻能看到(可见性),且禁止指令重排(有序性)。

java
// 可见性:线程 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 保证一段代码同一时刻只有一个线程执行(原子性),且执行结果对其他线程可见(可见性)。

java
private int count = 0;
private final Object lock = new Object();

public void increment() {
    synchronized (lock) {   // 同一时刻只允许一个线程进入
        count++;            // 原子 + 可见
    }
}

synchronized 是"重量级"方案(涉及锁竞争、可能阻塞线程),简单可靠,但性能比 CAS 差。

五、CAS 和原子类:无锁实现原子性

CAS(Compare-And-Swap,比较并交换)是无锁的原子操作:先比较内存值是不是预期值,是才更新,否则重试

java
// AtomicInteger 内部就是 CAS + 自旋
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet();   // 原子地 +1,无锁、高性能

CAS 的经典应用是"自旋"——更新失败就重试,直到成功。它比 synchronized 性能好(不用阻塞线程),但只能保证单个变量的原子性(多个变量的复合操作还是要锁)。

java
// CAS 的三个参数:内存值、预期值、新值
// 只有"内存值 == 预期值"才更新,否则重试
boolean ok = atomicValue.compareAndSet(expect, update);

六、线程池:复用线程,别每次 new Thread

每次任务都 new Thread() 很浪费(线程创建销毁有开销),线程池复用线程:

java
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 内存模型与垃圾回收》。