如何在高并发场景下,保证多个线程安全地修改同一个共享变量,并避免死锁和性能瓶颈?
首先,先明确一个前提:线程安全修改共享变量的核心,是解决三个问题——原子性(操作不可分割)、可见性(一个线程修改后,其他线程能立刻看到)、有序性(避免指令重排导致的逻辑混乱)。死锁源于锁的不合理嵌套和循环等待,性能瓶颈则多来自锁的粒度太大、线程阻塞开销过高。
下面从「方案选择」「避免死锁」「性能优化」三个维度展开,给出可落地的解决方案。
一、核心解决方案:从「悲观锁」到「乐观锁」
根据并发压力和业务场景,优先选择「轻量方案」(无锁),再考虑「重量方案」(有锁),这样能最大程度平衡线程安全和性能。
方案1:悲观锁(独占锁)—— 简单易用,适合低到中并发
悲观锁的核心思想:假设一定会有线程竞争,先获取锁,再执行操作,同一时间只允许一个线程修改共享变量,天然保证原子性、可见性、有序性。
1.1 synchronized 关键字(JVM内置锁)
这是Java中最基础的同步机制,使用简单,无需手动管理锁的释放。
- 原理:属于「可重入悲观锁」,JVM层面实现,会自动完成「加锁-执行-释放锁」(即使发生异常,也会自动释放锁,降低死锁风险)。
- 优化:现代JVM对
synchronized做了大量优化(偏向锁→轻量级锁→重量级锁的升级流程),低并发下性能接近无锁方案。 - 用法:推荐使用「同步代码块」(缩小锁粒度),而非「同步方法」(锁粒度太大,性能差)。
- 线程安全保障:同一时间只有一个线程能进入同步代码块,修改共享变量。
- 死锁风险:低,但如果嵌套
synchronized且锁获取顺序不一致,仍会产生死锁。
示例代码(安全修改计数器):
public class SynchronizedDemo {
// 共享变量
private int count = 0;
// 安全修改共享变量的方法
public void increment() {
// 同步代码块:锁对象推荐使用「当前类的实例」或「类对象」(按需选择)
synchronized (this) {
count++; // 原子操作(在同步块内,避免被其他线程打断)
}
}
// 获取结果
public int getCount() {
// 读取共享变量也建议保证可见性(synchronized 或 volatile)
synchronized (this) {
return count;
}
}
}
1.2 ReentrantLock(显式锁,java.util.concurrent.locks)
这是比synchronized更灵活的悲观锁,API层面实现,适合复杂业务场景(如超时获取锁、可中断锁)。
- 原理:属于「可重入悲观锁」,支持公平锁/非公平锁(默认非公平,性能更高)。
- 核心优势:
- 手动控制锁的获取和释放(必须在
finally块中释放锁,避免锁泄露); - 提供
tryLock(long timeout, TimeUnit unit)方法,支持超时获取锁,可有效避免死锁; - 支持
lockInterruptibly(),可中断等待锁的线程。
- 手动控制锁的获取和释放(必须在
- 性能:高并发下与
synchronized差距不大(JVM优化后),但灵活性远超synchronized。
示例代码(安全修改计数器,避免死锁):
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockDemo {
private int count = 0;
// 创建可重入锁(非公平锁)
private final ReentrantLock lock = new ReentrantLock();
public void increment() {
// 1. 获取锁(可选择tryLock带超时,避免死锁)
boolean isLocked = false;
try {
// 尝试获取锁,超时1秒则放弃,返回false
isLocked = lock.tryLock(1, java.util.concurrent.TimeUnit.SECONDS);
if (isLocked) {
count++;
} else {
// 获取锁失败,可做降级处理(如记录日志、重试)
System.out.println("获取锁超时,放弃本次修改");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
// 2. 必须在finally中释放锁,避免锁泄露
if (isLocked) {
lock.unlock();
}
}
}
public int getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
}
方案2:乐观锁(无锁机制)—— 适合高并发,无死锁
乐观锁的核心思想:假设没有线程竞争,不获取锁,直接尝试修改共享变量,修改前验证变量是否被其他线程修改过,若未被修改则更新,否则重试。
这种方案无线程阻塞,没有死锁风险,高并发下性能远优于悲观锁。
2.1 CAS 机制(Compare-And-Swap,比较并交换)
CAS是乐观锁的底层实现,由CPU硬件提供原子指令支持(无需JVM介入),保证操作的原子性。
- CAS的三个核心操作数:
- 内存值
V(共享变量的当前内存地址值); - 预期值
A(线程读取到的共享变量初始值); - 更新值
B(线程要修改后的目标值)。
- 内存值
- 执行逻辑:当且仅当
V == A时,CPU才会将V更新为B,否则不做任何操作;线程会循环重试(自旋),直到更新成功(或达到自旋上限)。 - 注意事项:
- 需用
volatile修饰共享变量,保证可见性和有序性(禁止指令重排,让线程能读取到最新的内存值); - 存在「ABA问题」(变量被修改为B后又改回A,CAS会误认为未被修改),可通过「版本号」解决(如
AtomicStampedReference); - 自旋次数过多会导致CPU占用过高,高并发竞争激烈时需优化。
- 需用
2.2 Java Atomic 系列类(直接使用CAS,无需手动实现)
Java提供了java.util.concurrent.atomic包,封装了CAS操作,直接使用即可实现无锁线程安全,无需手动处理自旋和volatile。
- 常用类:
AtomicInteger、AtomicLong、AtomicBoolean、AtomicReference(支持复杂对象)。 - 线程安全保障:底层CAS+volatile,保证原子性、可见性、有序性。
- 死锁风险:无(无锁机制,不存在锁的竞争和等待)。
- 性能:高并发下性能优异,无线程阻塞/唤醒开销。
示例代码(无锁安全修改计数器):
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicDemo {
// 共享变量:使用AtomicInteger封装,无需额外加锁
private final AtomicInteger count = new AtomicInteger(0);
// 安全修改共享变量:无锁,底层CAS
public void increment() {
// getAndIncrement():原子性i++,返回修改前的值
count.getAndIncrement();
}
// 获取结果:原子性读取
public int getCount() {
return count.get();
}
}
2.3 LongAdder(超高并发优化,解决Atomic系列自旋竞争)
当并发量极高时,AtomicLong的自旋CAS会因为竞争激烈导致大量重试,CPU占用过高。LongAdder通过「分段累加」优化了这个问题。
- 原理:将一个共享变量拆分为多个「子变量」(
cells数组),每个线程优先修改自己对应的子变量,最后汇总所有子变量的值得到最终结果,大大降低CAS竞争。 - 适用场景:高并发下的「计数场景」(如统计接口访问量、订单数),性能远超
AtomicLong。 - 死锁风险:无。
示例代码(超高并发计数器):
import java.util.concurrent.atomic.LongAdder;
public class LongAdderDemo {
private final LongAdder count = new LongAdder();
public void increment() {
// 累加,底层分段CAS,无锁
count.increment();
}
public long getCount() {
// 汇总所有子变量,返回最终结果
return count.sum();
}
}
二、如何避免死锁?
死锁的四个必要条件:互斥条件、请求与保持条件、不可剥夺条件、循环等待条件。只要破坏其中一个,就能避免死锁,实践中常用以下可落地的原则:
-
统一锁的获取顺序(破坏「循环等待条件」)
多个线程需要获取多个锁时,必须按照「相同的固定顺序」获取(比如按锁对象的哈希值从小到大、按业务ID从小到大),避免循环等待。
示例:线程1和线程2都需要获取锁A和锁B,统一先获取锁A,再获取锁B。 -
避免锁的嵌套使用(减少「请求与保持条件」的触发)
尽量减少「锁中锁」的嵌套逻辑,嵌套层数越多,死锁风险越高,且排查难度越大。如果必须嵌套,严格遵守「统一锁顺序」。 -
使用可超时的锁(破坏「不可剥夺条件」)
优先使用ReentrantLock的tryLock(long timeout, TimeUnit unit),设置合理的超时时间,超时后自动放弃获取锁,避免线程无限等待。 -
减少锁的持有时间
获取锁后,只执行「核心的共享变量修改逻辑」,耗时操作(如IO、网络请求、复杂计算)放在锁外执行,降低锁的竞争时间,间接减少死锁概率。 -
避免使用不可中断的锁
使用ReentrantLock的lockInterruptibly()方法,支持线程中断,当检测到死锁风险时,可以中断等待锁的线程,释放资源。
三、如何避免性能瓶颈?
性能瓶颈的核心来源:锁粒度太大、线程阻塞开销过高、CAS竞争激烈,对应的优化策略如下:
-
优先选择「无锁方案」(CAS/Atomic/LongAdder)
高并发场景下,无锁方案没有线程阻塞/唤醒的开销,性能远优于悲观锁,是避免性能瓶颈的首选。 -
缩小锁粒度(将「大锁」拆分为「小锁」)
不直接锁整个对象或整个方法,只锁「需要修改的共享变量相关逻辑」(如synchronized同步代码块代替同步方法)。
典型案例:ConcurrentHashMap的「分段锁」(JDK1.7)/「CAS+synchronized分段」(JDK1.8),将整个Map拆分为多个段,每个段单独加锁,多线程可同时操作不同段,提升并发性能。 -
减少锁的持有时间
锁内只保留「原子性修改共享变量」的核心代码,耗时操作(如数据库操作、JSON序列化、网络请求)全部移出锁外,降低线程等待锁的时间。 -
避免不必要的共享(使用ThreadLocal)
如果共享变量可以按线程隔离,优先使用ThreadLocal为每个线程创建独立副本,线程修改自己的副本,无需竞争,既保证线程安全,又无性能开销。
示例:Spring的事务管理器、日期格式化工具SimpleDateFormat的线程安全问题,都可以用ThreadLocal解决。 -
优化CAS自旋(避免CPU占用过高)
无限自旋会导致CPU飙升,可通过以下方式优化:- 设置自旋次数上限(超过次数则放弃,或切换为悲观锁);
- 高并发下使用
LongAdder代替AtomicLong,通过分段累加降低CAS竞争; - 结合「自适应自旋」(JVM内置优化,根据前一次自旋是否成功调整自旋次数)。
-
使用并发容器代替自定义同步容器
优先使用JUC提供的并发容器(ConcurrentHashMap、CopyOnWriteArrayList、LinkedBlockingQueue),这些容器已经做了高度优化,比自己用「普通容器+synchronized」性能更好。
总结
- 线程安全修改共享变量的核心:解决原子性、可见性、有序性,优先选择「无锁方案」(CAS/Atomic/LongAdder),低并发场景可选择「悲观锁」(synchronized/ReentrantLock)。
- 避免死锁的关键:统一锁顺序、避免锁嵌套、使用可超时锁,破坏死锁的四个必要条件之一即可。
- 避免性能瓶颈的核心:缩小锁粒度、减少锁持有时间、优先无锁、避免不必要共享,平衡线程安全和并发性能。
| 方案 | 适用场景 | 死锁风险 | 性能表现 |
|---|---|---|---|
| synchronized | 低到中并发、简单场景 | 低 | 较好(JVM优化后) |
| ReentrantLock | 中并发、复杂业务(超时/中断) | 中(可规避) | 较好 |
| Atomic 系列 | 高并发、简单变量(int/long) | 无 | 优秀 |
| LongAdder | 超高并发、计数场景 | 无 | 最优 |
| ThreadLocal | 线程隔离、无需共享副本 | 无 | 极致(无竞争) |
更多推荐


所有评论(0)