个人网站

面试官问"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、读写锁),只保护必要的可见性或读写分离。选择时优先无同步,其次最小同步,再次非阻塞同步,最后互斥同步。

加分回答
  1. JIT 对 synchronized 的优化:JDK 6 引入了偏向锁→轻量级锁→重量级锁的锁升级机制。无竞争时偏向锁几乎零开销,有轻度竞争时自旋(轻量级锁),竞争激烈才膨胀为重量级锁。所以简单场景下 synchronized 性能不比 ReentrantLock 差
  2. StampedLock 的乐观读:Java 8 引入的 StampedLock 提供了"乐观读"模式——先不加锁读取,然后验证读取期间是否有写操作。如果验证通过,读取有效;如果验证失败,再升级为读锁。在读远多于写的场景下性能优于 ReentrantReadWriteLock
  3. 不可变设计的系统性:不可变不仅是 final,还包括防御性拷贝(返回 Collections.unmodifiableList)、不可变集合(List.of/Map.of)、记录类(Java 16 record)。函数式编程的核心就是不可变性
面试官点评

这道题考的是你知识面的完整性和选型能力。只说出 synchronized 和 Lock 是不够的,能覆盖四种策略才算及格。如果能从"优先无同步"的角度给出选型建议、提到锁升级、StampedLock 乐观读等进阶内容,面试官会认为你对并发编程有系统性的认识,而不是只会用锁。

原文阅读


内容有帮助?点赞、收藏、关注三连!评论区等你 💪

更多推荐