孤舟笔记 并发篇三十五 Java保证线程安全的方式有哪些?这道高频面试题你答得全吗
文章目录
个人网站
面试官问"Java 保证线程安全的方式有哪些",很多人只能说出 synchronized 和 Lock,再追问"还有呢"就卡住了。这道题考的是你知识面的完整性——从"不共享"到"互斥"到"非阻塞",你得说全。
今天咱们按思路梳理 Java 中所有保证线程安全的方式。
一、先说结论:四种策略一张表
| 策略 | 核心 | 代表 |
|---|---|---|
| 互斥同步 | 同一时刻只有一个线程访问 | synchronized、ReentrantLock |
| 非阻塞同步 | 不加锁,CAS 乐观更新 | Atomic类、ConcurrentHashMap |
| 无同步 | 不需要共享或不需要同步 | ThreadLocal、局部变量、不可变对象 |
| 最小同步 | 缩小同步范围 | volatile、读写锁 |
一句话记住:线程安全四条路——互斥锁住、CAS 乐观、不共享最爽、最小同步最巧。
二、互斥同步:你用完了我再用
核心思想: 同一时刻只允许一个线程进入临界区。
synchronized
// 修饰实例方法——锁 this
public synchronized void method() { /* ... */ }
// 修饰静态方法——锁 Class 对象
public static synchronized void method() { /* ... */ }
// 修饰代码块——锁指定对象
synchronized (lock) {
// 临界区 👈
}
| 特性 | 说明 |
|---|---|
| 锁获取 | 自动获取,异常自动释放 |
| 可重入 | ✅ 同一线程可重入 |
| 公平性 | ❌ 非公平 |
| 可中断 | ❌ 不可中断 |
ReentrantLock
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
// 临界区 👈
} finally {
lock.unlock(); // 必须手动释放!
}
| 特性 | 说明 |
|---|---|
| 锁获取 | 手动获取和释放 |
| 可重入 | ✅ |
| 公平性 | 可选公平/非公平 |
| 可中断 | ✅ lockInterruptibly() |
| 超时 | ✅ tryLock(timeout) |
| 条件变量 | ✅ Condition |
选择: 简单场景用 synchronized(JVM 优化后性能很好),需要公平锁、可中断、超时、多条件队列用 ReentrantLock。
三、非阻塞同步:乐观地试,失败了重来
核心思想: 不加锁,先操作,如果冲突了就重试(CAS)。
Atomic 类
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // CAS 操作 👈
// 底层:比较当前值 == 期望值 → 是则更新,否则重试
// CAS 的伪代码
do {
int old = count.get();
int next = old + 1;
} while (!CAS(count, old, next)); // 👈 冲突则重试
ConcurrentHashMap
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
// CAS + synchronized 优化(JDK 8+)
// 桶为空 → CAS 插入
// 桶不为空 → synchronized(头节点) 插入 👈
map.put("key", "value");
CAS 的局限性——ABA 问题: 值从 A 变成 B 又变回 A,CAS 认为没变。AtomicStampedReference 用版本号解决。
生活类比: 互斥同步像等红灯——所有人排队,一个一个过;CAS 像抢红包——大家同时抢,抢到了最好,没抢到重新看。
四、无同步方案:没有共享就没有伤害
最好的并发编程是不需要同步。
局部变量
public void process() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); // ✅ 局部变量,每个线程独立 👈
sdf.format(new Date());
}
局部变量在栈上,线程私有,天然安全。
ThreadLocal
private static ThreadLocal<SimpleDateFormat> tl = ThreadLocal.withInitial(
() -> new SimpleDateFormat("yyyy-MM-dd")
);
// 每个线程独立副本
SimpleDateFormat sdf = tl.get(); // ✅ 👈
不可变对象
final String name = "hello"; // ✅ 不可变
final List<String> list = List.of("a", "b"); // ✅ 不可变
不可变对象创建后状态不变,任何线程看到的一样,天然安全。
生活类比: 无同步方案就像每人一份讲义——不用抢,不用等,各看各的,互不影响。
五、最小同步:只保护必要的
volatile
volatile boolean running = true; // ✅ 只保证可见性,不加锁 👈
适用: 一个线程写、多个线程读的简单场景。不需要互斥,只需要可见性。
读写锁
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
// 读锁——多个线程可以同时读
rwLock.readLock().lock();
try { /* 读取数据 */ } finally { rwLock.readLock().unlock(); }
// 写锁——独占
rwLock.writeLock().lock();
try { /* 修改数据 */ } finally { rwLock.writeLock().unlock(); }
读写分离: 读多写少场景下,读读不互斥,性能远优于独占锁。
生活类比: 读写锁像图书馆——看书的人可以很多(读共享),但有人要改书架就得等所有人看完(写独占)。
Java 保证线程安全的方式 全景
Java 保证线程安全的方式 全景
互斥同步
├── synchronized ── 自动加锁释放,简单
├── ReentrantLock ── 公平/可中断/超时/条件
└── 读写锁 ── 读共享写独占
非阻塞同步
├── Atomic 类 ── CAS 乐观更新
├── ConcurrentHashMap ── CAS + 局部 synchronized
└── ABA 问题 ── AtomicStampedReference
无同步方案
├── 局部变量 ── 栈上私有,天然安全
├── ThreadLocal ── 每个线程独立副本
└── 不可变对象 ── final + 不可变集合
最小同步
├── volatile ── 可见性 + 有序性
└── 读写锁 ── 读多写少优化
口诀:互斥同步最常见,CAS 乐观不阻塞,
无同步是最高境界,volatile 保可见,
读写分离提性能,选对方式最关键。
回答技巧与点评
标准回答
Java 保证线程安全有四种策略:互斥同步(synchronized、ReentrantLock),同一时刻只有一个线程访问临界区;非阻塞同步(Atomic 类、ConcurrentHashMap),基于 CAS 乐观更新,冲突时重试;无同步方案(局部变量、ThreadLocal、不可变对象),消除共享或可变,从根本上避免并发问题;最小同步(volatile、读写锁),只保护必要的可见性或读写分离。选择时优先无同步,其次最小同步,再次非阻塞同步,最后互斥同步。
加分回答
- JIT 对 synchronized 的优化:JDK 6 引入了偏向锁→轻量级锁→重量级锁的锁升级机制。无竞争时偏向锁几乎零开销,有轻度竞争时自旋(轻量级锁),竞争激烈才膨胀为重量级锁。所以简单场景下 synchronized 性能不比 ReentrantLock 差
- StampedLock 的乐观读:Java 8 引入的 StampedLock 提供了"乐观读"模式——先不加锁读取,然后验证读取期间是否有写操作。如果验证通过,读取有效;如果验证失败,再升级为读锁。在读远多于写的场景下性能优于 ReentrantReadWriteLock
- 不可变设计的系统性:不可变不仅是 final,还包括防御性拷贝(返回 Collections.unmodifiableList)、不可变集合(List.of/Map.of)、记录类(Java 16 record)。函数式编程的核心就是不可变性
面试官点评
这道题考的是你知识面的完整性和选型能力。只说出 synchronized 和 Lock 是不够的,能覆盖四种策略才算及格。如果能从"优先无同步"的角度给出选型建议、提到锁升级、StampedLock 乐观读等进阶内容,面试官会认为你对并发编程有系统性的认识,而不是只会用锁。
内容有帮助?点赞、收藏、关注三连!评论区等你 💪
更多推荐

所有评论(0)