Java 并发编程实战指南
Java 并发是后端工程师的必修课,也是大厂面试必考"三件套"(JVM、并发、MySQL)之一。本文从线程基础讲起,覆盖 synchronized、volatile、JUC 工具、线程池、锁、CAS、ThreadLocal、并发设计模式与常见坑,全部带代码和实战场景。
一、为什么需要并发
1.1 并发解决什么问题
- 吞吐:一个请求一个线程,10 万请求要能并发处理;
- 响应:一个任务拆多个子任务并行,快 N 倍;
- 充分利用 CPU:多核机器不用并发就是浪费。
1.2 并发带来的问题
并发不是免费的------引入三大问题:
- 原子性 :
count++不是原子操作(读-改-写三步); - 可见性:线程 A 改了变量,线程 B 看不到;
- 有序性:指令重排导致结果意外。
这三大问题贯穿本文所有内容。
1.3 创建线程的三种方式
java
// 方式一:继承 Thread
class MyThread extends Thread {
@Override
public void run() { System.out.println("thread run"); }
}
new MyThread().start();
// 方式二:实现 Runnable(推荐)
Runnable task = () -> System.out.println("runnable run");
new Thread(task).start();
// 方式三:Callable + Future(能拿返回值)
ExecutorService pool = Executors.newFixedThreadPool(2);
Future<Integer> future = pool.submit(() -> 1 + 2);
int result = future.get(); // 阻塞等待结果
企业实践:几乎不用 new Thread,一律用线程池(见第六章)。
二、线程安全核心机制
2.1 synchronized
作用:保证原子性 + 可见性(释放锁时强制刷新内存)。
java
public class Counter {
private int count = 0;
// 锁住实例(this)
public synchronized void increment() { count++; }
// 静态方法锁 Class 对象
public static synchronized void staticMethod() { }
// 代码块(锁对象精确控制)
public void add() {
synchronized (this) { count++; }
}
}
锁的是什么:
- 实例方法 → 锁 this;
- 静态方法 → 锁 Class;
- 代码块 → 锁指定对象。
synchronized 原理(JVM 层面):Monitor(监视器锁),每个对象关联一个 monitor。锁升级:无锁 → 偏向锁 → 轻量级锁(CAS 自旋)→ 重量级锁(OS 互斥量)。JDK6+ 自动优化,不用手动干预。
2.2 volatile
作用 :保证可见性 + 禁止指令重排,不保证原子性。
java
// 典型用法:状态标志
public class StopFlag {
private volatile boolean running = true;
public void stop() { running = false; }
public void run() {
while (running) { /* 干活 */ }
}
}
为什么必须有 volatile:没有它,线程 B 可能一直用 CPU 缓存的旧值,while 循环停不下来(可见性问题)。
volatile 不能做的:
java
private volatile int count = 0;
count++; // 仍然线程不安全(读-改-写不是原子)
正确姿势 :AtomicInteger(见第四章)。
2.3 synchronized vs volatile
| 维度 | synchronized | volatile |
|---|---|---|
| 原子性 | 保证 | 不保证 |
| 可见性 | 保证 | 保证 |
| 有序性 | 保证 | 部分(禁止重排) |
| 开销 | 较大 | 极小 |
| 适用 | 复合操作 | 状态标志 |
三、JMM:Java 内存模型
3.1 什么是 JMM
Java 内存模型规定:线程操作变量要先拷贝到自己的工作内存,操作完刷回主内存。
主内存(共享变量)
↑ 读/写
线程A 工作内存 线程B 工作内存
问题:线程 A 改了值没刷回,线程 B 读的是旧值------这就是可见性问题。
3.2 happens-before 原则(面试重点)
JMM 保证这些关系下,前面操作对后面可见:
- 程序次序规则:单线程内代码顺序;
- 锁规则:unlock 对后面的 lock 可见;
- volatile 规则:volatile 写对后面的读可见;
- 传递性:A 先于 B、B 先于 C → A 先于 C;
- 线程启动/结束规则:start() 之前对 run() 可见;线程结束对 join() 之后可见。
面试常问:"为什么单例的双重检查要加 volatile?"------因为对象 new 有指令重排(分配内存→引用赋值→初始化),volatile 禁止重排,防止拿到"半初始化"对象。
四、JUC 原子类与 CAS
4.1 原子类
java
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // 线程安全自增
count.get();
支持的类型:AtomicInteger / AtomicLong / AtomicBoolean / AtomicReference / LongAdder。
LongAdder(高并发计数首选):
java
LongAdder count = new LongAdder();
count.increment();
long total = count.sum();
为什么 LongAdder 更快:AtomicLong 高并发时 CAS 失败重试多;LongAdder 分多个 cell 累加,最后求和------减少竞争。
4.2 CAS 原理
CAS = Compare And Swap:
CAS(内存值 V, 期望值 A, 新值 B):
如果 V == A,则 V = B,成功
否则失败,重新读再试
优点 :无锁,性能好。
缺点:
- ABA 问题:值 A→B→A,CAS 判断不出来(AtomicStampedReference 加版本号解决);
- 自旋开销:竞争激烈时一直空转;
- 只能保证一个变量。
4.3 实战:自旋锁
java
public class SpinLock {
private final AtomicReference<Thread> owner = new AtomicReference<>();
public void lock() {
Thread t = Thread.currentThread();
while (!owner.compareAndSet(null, t)) {
// 自旋等待
}
}
public void unlock() {
Thread t = Thread.currentThread();
owner.compareAndSet(t, null);
}
}
五、Lock:可中断的锁
5.1 ReentrantLock vs synchronized
java
Lock lock = new ReentrantLock();
lock.lock();
try {
// 临界区
} finally {
lock.unlock(); // 必须手动释放!
}
ReentrantLock 额外能力:
- 可中断 :
lock.lockInterruptibly(); - 超时 :
lock.tryLock(3, TimeUnit.SECONDS)------拿不到锁不等死; - 公平锁 :
new ReentrantLock(true); - 多个条件:Condition 精准唤醒。
5.2 Condition
java
Lock lock = new ReentrantLock();
Condition notEmpty = lock.newCondition();
Condition notFull = lock.newCondition();
// 生产者
lock.lock();
try {
while (queue.isFull()) notFull.await(); // 满了等待
queue.put(item);
notEmpty.signal(); // 唤醒消费者
} finally { lock.unlock(); }
注意 :await() 必须在 while 循环里(防止虚假唤醒)。
5.3 ReadWriteLock 读写锁
读读不互斥、读写互斥、写写互斥------读多写少场景大幅提升性能:
java
ReadWriteLock rwLock = new ReentrantReadWriteLock();
public Object get(String key) {
rwLock.readLock().lock();
try { return cache.get(key); }
finally { rwLock.readLock().unlock(); }
}
public void put(String key, Object val) {
rwLock.writeLock().lock();
try { cache.put(key, val); }
finally { rwLock.writeLock().unlock(); }
}
六、线程池(必考+必用)
6.1 为什么用线程池
- new Thread 开销大(创建销毁要系统调用);
- 无上限创建线程会 OOM(
unable to create new native thread); - 线程池:复用线程 + 控制并发 + 任务队列缓冲。
6.2 核心参数
java
new ThreadPoolExecutor(
corePoolSize, // 核心线程数(常驻)
maximumPoolSize, // 最大线程数
keepAliveTime, // 非核心线程空闲存活时间
TimeUnit.SECONDS,
workQueue, // 任务队列
threadFactory, // 线程工厂(命名,便于排查)
handler // 拒绝策略
);
执行流程:
提交任务
→ 核心线程没满?→ 新建核心线程
→ 队列没满?→ 进队列
→ 最大线程没满?→ 新建非核心线程
→ 满了 → 拒绝策略
拒绝策略:
AbortPolicy:抛异常(默认);CallerRunsPolicy:调用者线程执行(推荐,不丢任务);DiscardPolicy:静默丢弃;DiscardOldestPolicy:丢弃最老任务。
6.3 为什么不推荐 Executors
java
Executors.newFixedThreadPool(10); // 队列无界 → 内存爆
Executors.newCachedThreadPool(); // 线程无上限 → 线程爆
生产标准:手动 new ThreadPoolExecutor,参数明确可控:
java
ThreadPoolExecutor pool = new ThreadPoolExecutor(
4, 8, 60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000),
new ThreadFactoryBuilder().setNameFormat("order-pool-%d").build(),
new ThreadPoolExecutor.CallerRunsPolicy()
);
6.4 线程池参数怎么定(面试高频)
- CPU 密集 :
核心线程 = CPU 核数 + 1; - IO 密集 :
核心线程 = CPU 核数 × 2(或按公式CPU/(1-阻塞系数)); - 队列大小:按峰值任务量 × 缓冲系数;
- 拒绝策略:生产用 CallerRunsPolicy(不丢任务)。
6.5 线程池监控
java
pool.getPoolSize(); // 当前线程数
pool.getActiveCount(); // 活跃线程
pool.getQueue().size(); // 队列积压
pool.getCompletedTaskCount(); // 已完成任务
生产建议:线程池单独配置类,统一管理、可监控、可动态调参。
七、ThreadLocal
7.1 是什么
每个线程独立一份变量副本------线程隔离。
java
ThreadLocal<String> traceId = new ThreadLocal<>();
// 请求进来设置
traceId.set("req-12345");
// 任意地方取
String id = traceId.get();
// 用完必须清理!
traceId.remove();
典型应用:链路追踪 ID、用户上下文、SimpleDateFormat(非线程安全)。
7.2 内存泄漏(大坑)
ThreadLocal 用 ThreadLocalMap 存储,key 是弱引用。线程池的线程不销毁,value 强引用一直留着------不 remove 就泄漏。
java
// 错误:set 了不 remove
public void handle() {
traceId.set(id);
doBiz(); // 忘 remove
}
// 正确:finally 清理
public void handle() {
try {
traceId.set(id);
doBiz();
} finally {
traceId.remove(); // 必须清理
}
}
Spring 里怎么用:用框架自带的(如 MDC、RequestContextHolder),或自己封装工具类强制清理。
八、并发容器
8.1 常用容器对比
| 场景 | 线程安全 | 说明 |
|---|---|---|
| HashMap | ConcurrentHashMap | 分段锁/CAS,读快写快 |
| ArrayList | CopyOnWriteArrayList | 写时复制,读多写极少 |
| Set | ConcurrentSkipListSet / CopyOnWriteArraySet | 有序/写少 |
| 队列 | LinkedBlockingQueue / ArrayBlockingQueue | 阻塞队列 |
| 有序 Map | ConcurrentSkipListMap | 跳表实现 |
ConcurrentHashMap 要点:
- 不允许 null key/value(HashMap 允许);
size()是近似值(并发下不准);- 遍历时不会抛 ConcurrentModificationException(弱一致性)。
8.2 阻塞队列
java
BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(1000);
// 三组方法(面试必问)
queue.add(x); // 满则抛异常
queue.offer(x); // 满则返回 false
queue.put(x); // 满则阻塞等待(生产用这个)
典型应用:生产者-消费者模式(见第九章)。
九、并发设计模式
9.1 生产者-消费者
java
BlockingQueue<Task> queue = new LinkedBlockingQueue<>(100);
// 生产者(多个线程往队列放)
public void produce(Task task) { queue.put(task); }
// 消费者(线程池消费)
ExecutorService consumers = Executors.newFixedThreadPool(4);
for (int i = 0; i < 4; i++) {
consumers.submit(() -> {
while (!Thread.currentThread().isInterrupted()) {
Task task = queue.take(); // 阻塞取
process(task);
}
});
}
优点:解耦 + 削峰(生产快消费慢时队列缓冲)。
9.2 双重检查单例
java
public class Singleton {
private static volatile Singleton instance; // volatile 必须有
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查(避免加锁开销)
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查(真正创建)
instance = new Singleton();
}
}
}
return instance;
}
}
为什么 volatile :防止 new Singleton() 的指令重排(分配内存→赋值→初始化),别的线程拿到未初始化对象。
9.3 异步结果:Future / CompletableFuture
java
CompletableFuture<Integer> f1 = CompletableFuture.supplyAsync(() -> queryDb());
CompletableFuture<Integer> f2 = CompletableFuture.supplyAsync(() -> callApi());
// 并行等两个结果
Integer sum = f1.thenCombine(f2, Integer::sum).get();
对比 Future:CompletableFuture 支持回调、组合、编排(thenApply/exceptionally/allOf),是异步编排的首选。
十、常见并发坑
10.1 集合修改异常
java
List<String> list = new ArrayList<>();
for (String s : list) {
if (s.equals("x")) list.remove(s); // ConcurrentModificationException
}
解决 :Iterator.remove()、removeIf、用并发容器。
10.2 死锁
java
// 线程A持有锁1等锁2,线程B持有锁2等锁1 → 死锁
排查:
bash
jstack <pid> | grep -A 15 "deadlock"
预防:加锁顺序一致、用 tryLock 超时、缩小锁范围。
10.3 线程池用完不关
java
// 应用关闭时
pool.shutdown(); // 不接收新任务,跑完已提交的
pool.awaitTermination(10, TimeUnit.SECONDS);
不关闭线程池 → 应用退不掉(非 daemon 线程)。
10.4 锁的粒度
java
// 错误:锁整个方法(并发全串行)
public synchronized void updateAll() { ... }
// 正确:锁最小范围
public void update() {
synchronized (this) { onlyCritical(); }
otherWork(); // 不用锁
}
10.5 静态变量的竞争
java
// 多个实例共享静态 SimpleDateFormat(非线程安全)→ 乱
解决:DateTimeFormatter(线程安全)或 ThreadLocal。
十一、并发排查工具箱
CPU 高:top -Hp → jstack 看线程栈
死锁:jstack 搜 "deadlock"
线程数爆炸:jstack | grep "java.lang.Thread.State" | sort | uniq -c
内存泄漏(ThreadLocal):MAT 分析
并发 Bug:加日志验证(打印线程名)
压测:JMeter / wrk 模拟并发
本章小结
并发编程的三大支柱:synchronized/volatile 保证线程安全、JUC 工具提供高效组件、线程池管理资源。面试和实战的核心:CAS 原理、线程池参数、锁升级、ThreadLocal 泄漏、CompletableFuture 异步编排。
五篇 CSDN 热门主题文档全部完成:Kafka、Git、JVM、Nginx、Java 并发------覆盖后端开发的核心知识面。
(全文完)