并发

目录


并发


一、什么是并发?

一句话定义

并发是同一时间段内,系统中有多个独立任务交替执行的能力。

并发 vs 并行

维度并发(Concurrent)并行(Parallel)
时间维度同一时间段同一时刻
CPU核心数单核也能实现需要多核硬件支持
执行方式线程交替切换执行多个线程真的同时执行

实现层级

  1. 多进程并发:操作系统中同时运行多个应用程序
  2. 多线程并发:同一进程中运行多个任务
    • 优点:共享进程内存,上下文切换代价小
  3. 协程:单线程内多任务协作切换

二、为什么需要并发?

带来的收益

收益说明
提高吞吐量单位时间内处理更多请求
提高响应性不会因为某一任务阻塞导致整个程序卡住
提高资源利用率避免CPU在等待IO时空闲

带来的问题

问题表现解决方案思路
上下文切换代价大线程切换消耗CPU无锁编程、CAS、协程、减少线程数
资源限制带宽、连接数等瓶颈增加资源(集群)、限制并发度
数据不一致多线程同时修改共享数据synchronized、Lock、Atomic
死锁线程互相等待对方锁正确使用锁顺序、超时机制
调试难度大问题难复现,Bug不稳定日志、压测、线程Dump分析

三、并发三大特性(JMM)

Java内存模型(JMM)定义了多线程环境下如何保证数据一致性。

可见性

问题:一个线程修改了主内存变量,另一个线程看不到最新值

1
2
3
4
5
6
7
      主内存
data = 0
/ \
线程A缓存 线程B缓存
data=0 data=0
| |
data=1 依然读的是0(不可见)

保证手段

  • volatile:修改后立即刷回主内存,读时直接读主内存
  • synchronized / Lock:解锁前刷回主内存,加锁时清空工作内存重新读

原子性

问题:一个操作执行到一半被中断,导致数据不对

常见反例:

1
count++;  // 不是原子操作:1. 读count 2. 加1 3. 写回

保证手段

  • synchronized / Lock:保证同一时刻只有一个线程执行
  • Atomic* 类:CAS + volatile 实现无锁原子操作

有序性

问题:编译器/CPU为了优化会重排指令,导致执行顺序和代码写的不一样

经典例子:

1
2
3
4
5
6
7
8
// 线程 A
a = 1; // 1
flag = true; // 2

// 线程 B
if (flag) {
System.out.println(a); // 可能输出 0 而不是 1
}

重排后线程B可能先看到flag=true但a还没赋值。

保证手段

  • synchronized / Lock:加锁区域内保证单线程有序执行
  • volatile:禁止指令重排序(通过内存屏障实现)

四、as-if-serial 与 happens-before 规则

as-if-serial

编译器和处理器不会对存在数据依赖关系的操作做重排序。

核心:单线程下不管怎么重排,执行结果不能变。

1
2
3
int a = 1;   // 1
int b = 2; // 2
int c = a + b; // 3 (依赖1和2)

1 和 2 可以重排,但 3 必须在 1 和 2 之后。

happens-before

如果操作 A happens-before 操作 B,那么 A 的执行结果对 B 可见,且 A 的执行顺序排在 B 之前。

不是强制顺序:如果重排后结果一致,也可以重排。

happens-before 的 8 条规则

规则名说明
程序次序规则一个线程内,前面的操作 happens-before 后面的操作
监视器锁规则解锁 happens-before 加锁
volatile变量规则对 volatile 的写 happens-before 后续对它的读
线程start规则Thread.start() happens-before 该线程的任何操作
线程join规则线程A join 线程B,B的所有操作 happens-before A从join返回
线程中断规则interrupt() happens-before 被中断线程检测到中断
对象终结规则对象初始化完成 happens-before finalizer() 开始
传递性A happens-before B,B happens-before C → A happens-before C

五、线程同步工具类

synchronized + wait/notify

特性说明
锁对象可以是实例对象、Class对象、this
可重入同一个线程可以多次获取同一把锁
自动释放方法/代码块执行完或抛异常自动释放锁
wait/notify必须在 synchronized 块内调用,释放锁等待

ReentrantLock + Condition

特性说明
和 synchronized 对比显式加锁、解锁,需要在 finally 里释放
可重入支持
公平锁可选公平锁(先到先得),默认非公平
Condition支持多个条件队列,替代 wait/notify
1
2
3
4
5
6
7
8
9
10
11
ReentrantLock lock = new ReentrantLock();
Condition cond = lock.newCondition();

lock.lock();
try {
// 业务逻辑
cond.await(); // 类似 wait
cond.signal(); // 类似 notify
} finally {
lock.unlock();
}

ReadWriteLock / StampedLock

特性说明
ReadWriteLock读锁共享、写锁独占;适合读多写少
StampedLock1.8引入,性能更好;乐观读、读写锁模式切换

Semaphore

作用:控制同时访问某个资源的线程数量(令牌数)

1
2
3
Semaphore semaphore = new Semaphore(3); // 3个许可
semaphore.acquire(); // 获取许可
semaphore.release(); // 释放许可

CountDownLatch / CyclicBarrier / Phaser

工具类作用可复用
CountDownLatch等 N 个线程都完成,主线程再继续❌ 只能用一次
CyclicBarrierN 个线程互相等待,到齐后一起继续✅ 可循环用
Phaser更灵活的阶段同步器,支持动态增减参与者

Exchanger

作用:两个线程在同步点交换数据


六、异步编程:CompletableFuture

特性说明
1. 异步执行supplyAsync() / runAsync()
2. 链式回调thenApply() / thenAccept() / thenRun()
3. 异常处理exceptionally() / handle()
4. 任务组合thenCompose() / thenCombine() / allOf() / anyOf()
5. 线程池支持可传自定义 Executor

七、并发面试高频问题

1. volatile 关键字作用?原理?

  • 作用:保证可见性、禁止指令重排序
  • 原理:通过内存屏障(LoadLoad/StoreStore/LoadStore/StoreLoad)实现

2. CAS 是什么?有什么问题?

  • CAS:Compare And Swap,无锁原子操作
  • 核心:期望值、内存值、更新值
  • 问题:ABA问题、循环时间长CPU开销、只能保证一个变量

3. synchronized 锁升级过程?

  • 无锁 → 偏向锁 → 轻量级锁 → 重量级锁
  • 1.6后默认开启偏向锁

4. synchronized 和 ReentrantLock 区别?

  • 显式 vs 隐式、公平锁可选、Condition 支持、可中断

5. 线程池参数有哪些?拒绝策略?

  • 核心线程数、最大线程数、空闲时间、阻塞队列、线程工厂、拒绝策略
  • 4种拒绝策略:Abort、CallerRuns、DiscardOldest、Discard