返回 Java / Kotlin
Java / Kotlin
8 分钟阅读

Java Lock

synchronized、ReentrantLock、CAS——接 HashMap 面试链里「并发」那一棒的个人笔记。

为什么写这篇

关键词链走到 HashMapConcurrentModificationException分段锁 之后,下一棒通常就落到锁上了。

HashMap → 红黑树 → ConcurrentModificationException →
synchronized → AQS → CAS → ConcurrentHashMap → 分段锁 → ...

比如先问:ArrayList 能不能在 forEach 里一边遍历一边 remove?答「不可以」之后,引出 ConcurrentModificationException,再往下就自然问到——那怎么处理并发?synchronizedLock?乐观锁和悲观锁又是什么?

我自己对锁的理解,谈不上能去和 Doug Lea 聊「重新设计 Java 并发框架」。但日常写 Android / Java 业务、读 ConcurrentHashMap 源码笔记、应付大部分面试追问,把下面这张「锁的地图」握住,心里会踏实一些。这篇就是按这个粒度整理的。


锁在防什么

多线程同时改同一份数据,结果可能乱套——这就是线程安全问题。锁的作用说穿了就一句:让临界区同一时刻只有一个线程(或一组合规线程)在动

Java 里和锁相关的入口很多,但面试和日常最常碰到的就三类:

层级机制说明
语法级synchronized关键字,JVM 内置,隐式加解锁
API 级java.util.concurrent.locks.Lock显式锁,如 ReentrantLock
原子类java.util.concurrent.atomic.*基于 CAS,乐观锁思路

下面按这个顺序过一遍,最后再收束「悲观 / 乐观」「公平 / 非公平」这些分类标签。


synchronized:最老派、也最常用

synchronized 是很多人接触的第一把锁。块锁:

synchronized (obj) {
    // 临界区
}

方法锁:

public synchronized void add() { ... }

底层靠对象头里的 Monitor(监视器锁),字节码里对应 monitorenter / monitorexit。它默认是 悲观锁——默认假设会打架,先占坑再干活。

优点是简单,JVM 帮你管加解锁;缺点是灵活度不如后面的 Lock 接口——不能 tryLock、不能中断等待、也不能自己选公平策略。分段锁 里 JDK 1.7 的 Segment 继承 ReentrantLock,和 synchronizedHashMap 整表锁,也是同一条线上的不同解法。


Lock 接口:显式、可配置

JDK 1.5 起有 java.util.concurrent.locks.Lock,典型用法:

Lock lock = new ReentrantLock();
lock.lock();
try {
    // 临界区
} finally {
    lock.unlock();  // 必须放 finally,别漏
}

synchronized 多出来的能力:tryLock()lockInterruptibly()、可选公平 / 非公平。ReentrantLock 底层通常挂在 AQS(AbstractQueuedSynchronizer) 上——面试若继续深挖,关键词链就拐进 AQS 了;这篇只点到为止,知道「显式锁常常走 AQS」够用了。


CAS 与原子类:乐观锁的底子

另一条路是不抢传统意义上的「互斥锁」,而是用 CAS(Compare-And-Swap) 做无锁更新:

AtomicInteger i = new AtomicInteger(0);
i.incrementAndGet(); // 内部 CAS

CAS 是硬件级的「比较再交换」:内存里的旧值和期望值一样才更新,否则重试。ConcurrentHashMap JDK 1.8 里不少地方也是 CAS + 细粒度 synchronized 组合拳,和 分段锁 那篇能连上。

手写一个自增的 CAS 循环,逻辑是这样的:

while (true) {
    int oldValue = atomicInt.get();
    int newValue = oldValue + 1;
    if (atomicInt.compareAndSet(oldValue, newValue)) {
        break;
    }
    // 冲突了,重试
}

冲突少时很快;冲突猛了会一直自旋,CPU 飙高——这是乐观锁的代价。


各种「锁的标签」怎么记

面试里名词很多,我按自己的归类方式列一下,不追求教科书完备,只求对话时能接得上。

悲观锁 vs 乐观锁

  • 悲观锁:觉得竞争一定会来,先锁再说。代表:synchronizedReentrantLock
  • 乐观锁:觉得冲突是少数,先改再比对,冲突就重试。代表:CAS、AtomicInteger
synchronized (this) {
    count++;
}

安全,但等锁的时候线程会堵着。乐观锁快,但写不对或竞争太激烈,反而更折腾。

公平锁 vs 非公平锁

  • 公平锁:先来先得,排队
  • 非公平锁:允许插队,吞吐往往更高

ReentrantLock 默认非公平,可以显式要公平的:

new ReentrantLock(true);  // 公平

可重入锁

同一线程对同一把锁重复获取,不会把自己锁死:

public synchronized void a() {
    b(); // 再次进入同一把锁,OK
}
public synchronized void b() { }

synchronizedReentrantLock 都是可重入的。可重入不等于可乱用,只是避免「自己调自己」时死锁。

读写锁

读多写少时,用 ReadWriteLock 把读和写分开——读可以并发,写独占:

ReadWriteLock rwLock = new ReentrantReadWriteLock();
rwLock.readLock().lock();
// ...
rwLock.writeLock().lock();

自旋锁

等锁的时候不立刻挂起线程,而是循环看锁放没放。竞争不激烈时能少几次上下文切换;竞争激烈时纯自旋会烧 CPU。Java 里 CAS 的重试就带这种味道。


它们之间什么关系

我自己复习时画过一张简图,帮助把「语法锁 → 显式锁 → AQS → CAS」串起来:

锁的层级:
┌─────────────────────────────┐
│ synchronized (悲观锁,内置锁) │
│ ReentrantLock (显式锁,可重入) │
│ ReadWriteLock (细粒度锁)     │
│                              │
│ → AQS (底层实现框架)          │
│ → CAS (底层硬件指令)          │
└─────────────────────────────┘
         ↑
         └─ 乐观锁思想(基于 CAS)

不是严格继承关系,而是「从上到下越来越细、越来越灵活」的一条演化线。


一张表快速扫一眼

类型特点代表实现
悲观锁先锁后干synchronized, ReentrantLock
乐观锁干完再比对,冲突重试AtomicInteger, CAS
公平锁排队ReentrantLock(true)
非公平锁可插队ReentrantLock() 默认
可重入锁同线程可重复获取synchronized, ReentrantLock
读写锁读并发、写独占ReentrantReadWriteLock

收尾

锁这块的内容,深度可以差很多。友善一点的面试,问到 synchronizedLock 区别、乐观悲观各适合什么场景,差不多就能判断基本功了;要是奔着「捡漏架构师」去,一路追到 AQS 队列、CLH、公平锁实现细节,那我大概率也得和 HashMap 那篇一样——翻开笔记,接上那一棒

日常写业务,我的习惯是:能先用高层工具(ConcurrentHashMapviewModelScope、框架自带的线程安全结构)就别手写锁;真到了要自己 synchronizedReentrantLock 的时候,先想清楚锁的粒度——整表一把锁 还是只锁必要的一小段——往往比背多少锁的分类名词更管用。

相关文章

Java / Kotlin
8 分钟
HashMap in Java
Java 7 的数组+链表,Java 8 的红黑树——我自己翻笔记整理的一份 HashMap 底层笔记。
Java / Kotlin
7 分钟
Java 分段锁 (Segmented Lock)
接在 CAS、AQS 之后——ConcurrentHashMap 如何把整把 Map 的大锁拆成多把小锁。