# 美团《不可不说的 Java “锁”事》读后笔记

美团《不可不说的 Java “锁”事》读后笔记

原文:美团技术团队《不可不说的 Java “锁”事》
原文链接:https://tech.meituan.com/2018/11/15/java-lock.html


目录


一、文章整体脉络

这篇文章主要讲的是 Java 中常见锁的分类、底层原理和适用场景。

原文不是只讲 synchronizedReentrantLock,而是从“锁的不同特性”出发,把 Java 主流锁分成几组来理解:

  1. 线程要不要先锁住资源:乐观锁 / 悲观锁
  2. 线程拿不到锁要不要阻塞:自旋锁 / 非自旋锁
  3. synchronized 的锁状态变化:无锁 / 偏向锁 / 轻量级锁 / 重量级锁
  4. 多线程抢锁是否排队:公平锁 / 非公平锁
  5. 同一个线程能不能重复拿同一把锁:可重入锁 / 非可重入锁
  6. 一把锁能不能被多个线程共享:独享锁 / 共享锁

重点:Java 锁的分类不是互斥的,而是从不同角度描述同一把锁的特性。

比如 synchronized 可以同时被描述为:

  • 从思想上看:它是悲观锁
  • 从线程是否可重复进入看:它是可重入锁
  • 从资源是否共享看:它是独享锁
  • 从 JVM 优化过程看:它可能经历无锁 → 偏向锁 → 轻量级锁 → 重量级锁

二、Java 锁分类总览

原文给出的 Java 主流锁分类图如下:

Java 主流锁分类图

2.1 我自己的理解

可以把 Java 锁理解成一句话:

Java 锁就是为了解决“多个线程同时操作共享资源”时的数据安全问题,同时还要尽量兼顾性能。

如果不加锁,线程安全可能出问题。
如果一上来就重锁,性能又可能很差。
所以 Java 里才有这么多不同类型的锁,本质上都是在安全性、性能、公平性之间做取舍。

2.2 学习锁时不要死记名字

不要只背:

synchronized、ReentrantLock、CAS、AQS

更应该按问题理解:

问题 对应锁概念
操作共享资源前要不要先锁? 悲观锁 / 乐观锁
拿不到锁时是阻塞还是等一会儿? 阻塞 / 自旋
synchronized 如何优化性能? 偏向锁 / 轻量级锁 / 重量级锁
抢锁是否严格排队? 公平锁 / 非公平锁
同一个线程能不能重复拿锁? 可重入锁 / 非可重入锁
一把锁能不能多人一起持有? 独享锁 / 共享锁

三、乐观锁 VS 悲观锁

3.1 悲观锁

悲观锁的核心思想是:

我认为别人一定会来抢这个资源,所以我操作之前必须先加锁。

Java 中常见的悲观锁:

  • synchronized
  • ReentrantLock

示例:

public synchronized void testMethod() {
    // 操作共享资源
}
private final ReentrantLock lock = new ReentrantLock();

public void modifyPublicResources() {
    lock.lock();
    try {
        // 操作共享资源
    } finally {
        lock.unlock();
    }
}

悲观锁特点:

  • 操作共享资源之前先加锁
  • 没拿到锁的线程会等待
  • 适合写操作多、并发冲突严重的场景

重点:悲观锁不是“性能一定差”,而是它默认认为并发冲突很可能发生,所以先加锁保证安全。


3.2 乐观锁

乐观锁的核心思想是:

我先不加锁,等真正要更新数据时,再检查这期间有没有其他线程修改过。

Java 中常见的乐观锁实现:

  • CAS
  • AtomicInteger
  • AtomicLong
  • AtomicReference
  • 数据库版本号机制

示例:

private final AtomicInteger atomicInteger = new AtomicInteger();

public int add() {
    return atomicInteger.incrementAndGet();
}

乐观锁特点:

  • 不会像 synchronized 那样直接阻塞线程
  • 更新时检查数据有没有被改过
  • 适合读多写少、冲突不激烈的场景

重点:乐观锁不是“不保证线程安全”,而是“不通过传统互斥锁阻塞线程”,它通常通过 CAS 保证更新安全。


3.3 图示理解

乐观锁与悲观锁对比

3.4 对比总结

对比项 悲观锁 乐观锁
核心思想 认为一定会冲突 认为大概率不会冲突
是否先加锁
线程是否阻塞 可能阻塞 通常不阻塞
常见实现 synchronized、ReentrantLock CAS、AtomicInteger
适合场景 写多、冲突多 读多、冲突少
风险 线程上下文切换成本高 CAS 失败后可能一直重试

四、CAS:乐观锁的核心实现

4.1 CAS 是什么?

CAS 全称是:

Compare And Swap

中文叫:

比较并交换

它的核心逻辑是:

我准备把变量从旧值 A 改成新值 B,但是修改之前要先看一下内存中的值是不是还是 A。
如果还是 A,说明没人改过,我就把它更新成 B。
如果不是 A,说明别人已经改过,这次更新失败,可以重新读取后再试。

CAS 涉及三个值:

名称 含义
V 内存中的真实值
A 期望值
B 要写入的新值

伪代码:

if (V == A) {
    V = B;
} else {
    // 更新失败,重试或返回失败
}

重点:CAS 的“比较 + 更新”必须是一个原子操作。不能比较完之后还没更新,别的线程就插进来了。


4.2 AtomicInteger 为什么线程安全?

AtomicInteger 的自增底层不是普通的 i++,而是 CAS 自旋。

原文中 AtomicInteger 相关源码图:

AtomicInteger 源码结构

getAndAddInt() 的核心逻辑类似:

public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        v = getIntVolatile(o, offset);
    } while (!compareAndSwapInt(o, offset, v, v + delta));
    return v;
}

原文源码图:

CAS 自旋源码示意

我的理解:

  1. 先读取当前值 v
  2. 尝试用 CAS 把 v 改成 v + delta
  3. 如果成功,返回旧值
  4. 如果失败,说明别的线程已经改过
  5. 重新读取新值,再次 CAS
  6. 直到成功为止

重点:AtomicInteger 的线程安全不是因为“没有并发问题”,而是因为它用 CAS + 自旋重试保证最终有一个线程能成功更新。


4.3 CAS 的三个典型问题

4.3.1 ABA 问题

ABA 问题指的是:

A -> B -> A

线程 1 一开始看到变量是 A,准备 CAS。
线程 2 把变量从 A 改成 B,又从 B 改回 A。
线程 1 再次检查时发现还是 A,于是认为变量没被改过。

但实际上,变量中间已经被修改过了。

重点:CAS 只判断“当前值是否等于期望值”,但不知道这个值中间有没有变化过。

解决思路:

给变量加版本号。

1A -> 2B -> 3A

Java 中可以使用:

AtomicStampedReference

它不只比较引用值,还比较版本号。


4.3.2 自旋时间长,CPU 开销大

CAS 失败后通常会不断重试。如果竞争非常激烈,线程可能长时间 CAS 失败,一直循环。

重点:CAS 不会阻塞线程,但失败重试会占用 CPU。冲突越激烈,自旋浪费越明显。


4.3.3 只能保证单个变量的原子性

CAS 对单个变量很适合,比如:

AtomicInteger count;

但如果要同时修改多个变量:

int a;
int b;

普通 CAS 很难保证它们整体原子。

解决思路:

把多个变量封装成一个对象,然后用:

AtomicReference

对整个对象引用做 CAS。


五、自旋锁 VS 适应性自旋锁

5.1 什么是自旋锁?

自旋锁的思想是:

线程拿不到锁时,不马上阻塞,而是在原地循环等一会儿。

通俗理解:

一个线程想进入同步代码块,但是锁被其他线程拿着。
如果持锁线程马上就能执行完,那么当前线程直接阻塞就不划算。
所以它可以先“空转”等几次,看看锁会不会很快释放。

流程图:

自旋锁流程图


5.2 为什么要有自旋?

因为线程阻塞和唤醒需要操作系统参与。

如果同步代码块很短,那么:

线程阻塞 + 唤醒的成本 > 同步代码本身执行成本

这种情况下,线程先自旋等一会儿,可能更划算。

重点:自旋锁是用 CPU 空转,换取减少线程阻塞和唤醒的成本。


5.3 自旋锁适合什么场景?

适合:

  • 锁持有时间很短
  • 竞争不激烈
  • 多核 CPU 环境

不适合:

  • 锁持有时间很长
  • 竞争激烈
  • 很多线程同时自旋

因为大量线程一直自旋,会浪费 CPU。


5.4 适应性自旋锁

普通自旋锁的自旋次数可能是固定的。

适应性自旋锁更智能:

JVM 会根据上一次在同一个锁上的自旋结果,判断这次是否自旋,以及自旋多久。

比如:

  • 上一次自旋很快成功,这次可以多自旋一会儿
  • 上一次自旋很久都失败,这次可能直接阻塞

重点:适应性自旋不是固定次数,而是根据历史成功率动态调整。


六、synchronized 的底层基础:对象头与 Monitor

6.1 synchronized 锁住的是什么?

synchronized 可以修饰实例方法:

public synchronized void method() {
}

也可以修饰代码块:

synchronized (obj) {
    // 临界区
}

本质上,它锁的是对象。

  • 修饰实例方法:锁当前对象 this
  • 修饰静态方法:锁当前类的 Class 对象
  • 修饰代码块:锁括号里的对象

重点:Java 中每个对象都可以作为一把锁。


6.2 Java 对象头

HotSpot 虚拟机中,对象头主要包括:

  1. Mark Word
  2. Klass Pointer

Mark Word

Mark Word 会存储对象运行时信息,比如:

  • HashCode
  • 分代年龄
  • 锁标志位
  • 偏向线程 ID
  • 指向锁记录的指针
  • 指向重量级锁 Monitor 的指针

重点:Mark Word 不是固定只存一种东西,它会根据对象当前状态复用自己的存储空间。

例如:

对象状态 Mark Word 可能存储的内容
无锁 hashCode、分代年龄、锁标志位
偏向锁 偏向线程 ID、偏向时间戳、锁标志位
轻量级锁 指向线程栈中 Lock Record 的指针
重量级锁 指向 Monitor 的指针

Klass Pointer

Klass Pointer 是对象指向类元数据的指针。

简单理解:

JVM 通过 Klass Pointer 知道这个对象属于哪个类。


6.3 Monitor 是什么?

Monitor 可以理解为对象关联的一把内部锁,也常被称为监视器锁。

每个 Java 对象都可以和一个 Monitor 关联。
Monitor 中有一个 Owner 字段,用来记录当前持有锁的线程。

如果一个线程拿到了对象的 Monitor,就可以进入 synchronized 代码块。
如果其他线程也想进入,只能等待。

重点:synchronized 的重量级锁底层依赖 Monitor,而 Monitor 又依赖操作系统 Mutex Lock,所以线程阻塞和唤醒成本较高。


七、synchronized 锁升级:无锁、偏向锁、轻量级锁、重量级锁

这是本文最重要的部分。

synchronized 的锁状态从低到高是:

无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁

原文锁升级图:

synchronized 锁升级流程

重点:锁可以升级,但不能降级。


7.1 为什么 synchronized 要锁升级?

早期 synchronized 性能较差,是因为它一开始就依赖操作系统互斥锁。
线程阻塞、唤醒需要操作系统参与,成本高。

JDK 6 之后,JVM 对 synchronized 做了优化:

  • 没有竞争时,尽量减少加锁成本
  • 轻微竞争时,尽量不阻塞线程
  • 竞争严重时,再升级为重量级锁

所以才有锁升级机制。


7.2 无锁

无锁状态下,对象没有被传统意义上加锁。

多个线程都可以访问同一个资源,但同一时刻只有一个线程能够修改成功。

典型实现:

CAS

理解:

大家都可以尝试修改,但最后只有 CAS 成功的线程能真正改成功,失败的线程继续重试。


7.3 偏向锁

偏向锁适合这种场景:

一段同步代码几乎总是被同一个线程访问,没有其他线程竞争。

这时 JVM 会认为:

既然总是同一个线程进来,就没必要每次都执行完整的加锁流程。

于是 JVM 会在对象头 Mark Word 中记录偏向线程 ID。
下次同一个线程再进入同步块,只需要检查 Mark Word 里记录的是不是当前线程。

重点:偏向锁的目的,是在没有多线程竞争的情况下,减少不必要的 CAS 操作。

偏向锁什么时候撤销?

偏向锁不会主动释放。
只有当其他线程尝试竞争这把锁时,偏向锁才会撤销。

撤销后可能变成:

  • 无锁
  • 轻量级锁

难点:偏向锁不是为了处理竞争,而是为了优化“几乎没有竞争,且总是同一个线程反复进入同步块”的情况。


7.4 轻量级锁

轻量级锁适合这种场景:

已经出现线程竞争,但竞争不严重,锁很快就会释放。

轻量级锁的核心是:

CAS + 自旋

大致过程:

  1. 线程进入同步代码块
  2. JVM 在线程栈帧中创建 Lock Record
  3. 把对象头 Mark Word 复制到 Lock Record
  4. 用 CAS 尝试把对象 Mark Word 更新为指向 Lock Record 的指针
  5. 如果 CAS 成功,当前线程获得锁
  6. 如果 CAS 失败,说明有竞争
  7. 线程先通过自旋尝试等待锁释放

重点:轻量级锁不是没有竞争,而是“竞争不激烈时,先别急着阻塞线程”。


7.5 重量级锁

当竞争变严重时,轻量级锁会升级为重量级锁。

例如:

  • 自旋很多次仍然拿不到锁
  • 一个线程持有锁,一个线程自旋,又来了更多线程竞争

重量级锁状态下:

  • Mark Word 中存储指向 Monitor 的指针
  • 没有拿到锁的线程会阻塞
  • 阻塞和唤醒依赖操作系统

重点:重量级锁开销大,但竞争激烈时它更合适,因为不能让大量线程一直自旋浪费 CPU。


7.6 四种锁状态总结

锁状态 适合场景 核心机制 是否阻塞
无锁 没有加锁,CAS 修改 CAS
偏向锁 同一个线程反复进入 Mark Word 记录线程 ID
轻量级锁 有竞争但不激烈 CAS + 自旋 尽量不阻塞
重量级锁 竞争激烈 Monitor + 操作系统互斥锁

7.7 面试回答模板

synchronized 在 JDK 6 以后做了很多优化,它不是一上来就是重量级锁。它会根据竞争程度进行锁升级,大致过程是无锁、偏向锁、轻量级锁、重量级锁。
如果基本没有竞争,并且总是同一个线程访问同步代码块,会使用偏向锁,在对象头 Mark Word 中记录线程 ID,减少加锁成本。
如果出现轻微竞争,会升级为轻量级锁,通过 CAS 和自旋尝试获取锁,避免线程阻塞。
如果竞争比较激烈,自旋也拿不到锁,就升级为重量级锁,其他线程进入阻塞状态。
这样做的目的就是根据竞争程度选择更合适的锁实现,在安全性和性能之间做平衡。


八、公平锁 VS 非公平锁

8.1 公平锁

公平锁的思想是:

多个线程按照申请锁的顺序排队,先来先得。

优点:

  • 等待锁的线程不容易饿死
  • 顺序更公平

缺点:

  • 吞吐量较低
  • 队列中的线程经常需要阻塞和唤醒
  • CPU 唤醒阻塞线程有额外开销

原文公平锁图示:

公平锁示意图


8.2 非公平锁

非公平锁的思想是:

新来的线程可以先尝试抢锁,抢不到再去排队。

也就是说,即使队列中已经有线程在等待,如果锁刚好释放,新来的线程也可能直接抢到。

优点:

  • 吞吐量高
  • 减少线程阻塞和唤醒成本

缺点:

  • 可能导致某些线程等很久
  • 极端情况下可能产生线程饥饿

原文非公平锁图示:

非公平锁示意图


8.3 ReentrantLock 默认是公平锁还是非公平锁?

ReentrantLock 默认是非公平锁。

ReentrantLock lock = new ReentrantLock();

如果想创建公平锁:

ReentrantLock lock = new ReentrantLock(true);

原文源码图:

ReentrantLock 公平锁和非公平锁结构

8.4 公平锁和非公平锁源码差异

公平锁获取锁之前会判断队列中是否有前驱节点:

hasQueuedPredecessors()

非公平锁则是先尝试 CAS 抢锁,抢不到再排队。

源码对比图:

公平锁和非公平锁源码对比

hasQueuedPredecessors 源码

重点:公平锁是“先看有没有人排队”;非公平锁是“先抢一下,抢不到再排队”。


九、可重入锁 VS 非可重入锁

9.1 什么是可重入锁?

可重入锁又叫递归锁。

它的含义是:

同一个线程已经获取了一把锁之后,再次进入需要同一把锁的代码时,不会被自己阻塞。

示例:

public class Widget {

    public synchronized void doSomething() {
        System.out.println("方法1执行...");
        doOthers();
    }

    public synchronized void doOthers() {
        System.out.println("方法2执行...");
    }
}

线程进入 doSomething() 时已经拿到了当前对象的锁。
然后调用 doOthers(),这个方法也需要当前对象的锁。
因为 synchronized 是可重入锁,所以同一个线程可以继续进入。

重点:如果 synchronized 不可重入,那么线程会自己等自己,造成死锁。


9.2 Java 中哪些锁是可重入锁?

常见可重入锁:

  • synchronized
  • ReentrantLock

9.3 可重入锁图示

可重入锁示意图

非可重入锁图示:

非可重入锁示意图


9.4 可重入锁底层怎么实现?

ReentrantLock 为例,它底层基于 AQS。

AQS 中有一个 state 变量,用来记录锁状态。

可以这样理解:

state = 0:没有线程持有锁
state = 1:某个线程持有锁一次
state = 2:同一个线程重入了一次
state = 3:同一个线程重入了两次

加锁过程:

  1. 如果 state == 0,说明锁没人持有,当前线程可以获取锁
  2. 如果 state != 0,判断持有锁的是不是当前线程
  3. 如果是当前线程,说明发生重入,state + 1
  4. 如果不是当前线程,就进入等待

释放锁过程:

  1. 当前线程每释放一次锁,state - 1
  2. 直到 state == 0
  3. 锁才真正释放

源码对比图:

可重入锁与非可重入锁源码对比

重点:可重入锁不是“重复加锁不记录”,而是通过计数器记录重入次数。加锁几次,就要释放几次。


十、独享锁 VS 共享锁

10.1 独享锁

独享锁也叫排他锁。

含义是:

一把锁一次只能被一个线程持有。

常见独享锁:

  • synchronized
  • ReentrantLock
  • ReentrantReadWriteLock 的写锁

独享锁适合:

  • 修改数据
  • 写操作
  • 不允许多个线程同时进入的场景

10.2 共享锁

共享锁含义是:

一把锁可以被多个线程同时持有。

常见共享锁:

  • ReentrantReadWriteLock 的读锁

共享锁适合:

  • 读操作
  • 多个线程可以同时读取
  • 但读的时候不能有线程写

10.3 ReentrantReadWriteLock:读写锁

ReentrantReadWriteLock 里面有两把锁:

  • ReadLock
  • WriteLock

原文源码结构图:

ReentrantReadWriteLock 源码结构

读锁是共享锁。
写锁是独享锁。

规则:

场景 是否互斥 说明
读读 不互斥 多个线程可以同时读
读写 互斥 读的时候不能写
写读 互斥 写的时候不能读
写写 互斥 同一时刻只能一个线程写

重点:读写锁适合读多写少场景。多个读线程可以同时读,提高并发能力;只要涉及写,就必须互斥。


10.4 读写锁中的 state 拆分

AQS 的 state 是一个 int 类型,32 位。

ReentrantReadWriteLock 中,它把 state 分成两部分:

  • 高 16 位:读锁数量
  • 低 16 位:写锁数量

图示:

读写锁 state 拆分

理解:

高 16 位记录读锁状态
低 16 位记录写锁状态

难点:读写锁虽然有两把锁,但底层仍然依赖同一个 AQS 的 state,只是把一个 int 按位拆成了读状态和写状态。


十一、AQS 在锁体系中的位置

AQS 全称:

AbstractQueuedSynchronizer

它不是一把具体的锁,而是 JUC 里很多锁和同步工具的底层框架。

常见基于 AQS 的工具:

  • ReentrantLock
  • ReentrantReadWriteLock
  • Semaphore
  • CountDownLatch

AQS 主要做两件事:

  1. state 表示同步状态
  2. 用同步队列管理等待线程

简单理解:

AQS 就像一个半成品锁框架,具体是独享锁、共享锁、公平锁、非公平锁,由子类通过不同方法实现。

重点:面试中不要说 AQS 是某一把锁。AQS 是实现锁和同步器的基础框架。


十二、易混点总结

12.1 CAS 和自旋是不是一回事?

不是。

概念 含义
CAS 一种原子操作,比较并交换
自旋 一种等待方式,失败后循环重试

它们经常一起出现,比如 AtomicInteger

CAS 失败 -> 自旋重试 -> 再 CAS

重点:CAS 是操作,自旋是等待方式。


12.2 乐观锁一定比悲观锁好吗?

不一定。

乐观锁适合:

  • 读多写少
  • 冲突少
  • CAS 成功率高

悲观锁适合:

  • 写多
  • 冲突多
  • 临界区复杂

重点:没有绝对更好的锁,只有更适合当前场景的锁。


12.3 synchronized 是不是重量级锁?

不能简单这么说。

早期 synchronized 确实比较重。
但是 JDK 6 之后,synchronized 有锁升级机制:

无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁

所以现在说 synchronized,要知道它会根据竞争情况优化。


12.4 偏向锁和轻量级锁的区别

对比项 偏向锁 轻量级锁
适用场景 几乎没有竞争 有竞争但不激烈
核心目的 减少同一线程重复加锁成本 避免线程阻塞
核心机制 Mark Word 记录线程 ID CAS + 自旋
竞争情况 基本无竞争 轻微竞争

一句话区分:

偏向锁解决“同一个线程反复进入”的成本问题;轻量级锁解决“轻微竞争时不想阻塞线程”的问题。


12.5 公平锁一定好吗?

不一定。

公平锁优点:

  • 排队有序
  • 不容易饥饿

公平锁缺点:

  • 吞吐量低
  • 线程唤醒成本高

非公平锁虽然可能插队,但吞吐量通常更高。
所以 ReentrantLock 默认是非公平锁。


12.6 可重入锁是不是不用释放多次?

不是。

示例:

lock.lock();   // state = 1
lock.lock();   // state = 2

lock.unlock(); // state = 1
lock.unlock(); // state = 0,真正释放

重点:可重入锁加锁几次,就要释放几次。否则锁不会真正释放。


十三、面试回答模板

13.1 什么是乐观锁和悲观锁?

悲观锁认为并发冲突很可能发生,所以操作共享资源之前会先加锁,比如 synchronizedReentrantLock
乐观锁认为冲突不一定发生,所以不会先加锁,而是在更新数据时判断数据有没有被其他线程修改过,典型实现是 CAS,比如 AtomicInteger
悲观锁适合写多、冲突多的场景;乐观锁适合读多、冲突少的场景。


13.2 CAS 是什么?有什么问题?

CAS 是 Compare And Swap,比较并交换。它会比较内存中的值和期望值是否一致,如果一致,就把内存值更新成新值;如果不一致,说明数据被其他线程改过,本次更新失败,可以重试。
CAS 的优点是不需要阻塞线程,性能比较高。
但它也有问题:第一是 ABA 问题;第二是高竞争下会一直自旋,浪费 CPU;第三是普通 CAS 只能保证单个变量的原子性。


13.3 synchronized 的锁升级过程是什么?

synchronized 在 JDK 6 以后做了很多优化,不是一上来就是重量级锁。它的锁状态大致是无锁、偏向锁、轻量级锁、重量级锁。
如果没有竞争,或者总是同一个线程进入同步代码块,会使用偏向锁,减少加锁成本。
如果出现轻微竞争,会升级为轻量级锁,通过 CAS 和自旋尝试获取锁,避免线程阻塞。
如果竞争比较激烈,自旋也拿不到锁,就升级成重量级锁,其他线程进入阻塞状态。
锁升级的目的就是根据竞争程度选择更合适的锁实现。


13.4 什么是自旋锁?

自旋锁是指线程拿不到锁时,不马上阻塞,而是循环等待一小段时间。
如果锁很快释放,线程就可以直接拿到锁,避免线程阻塞和唤醒的成本。
但如果锁长时间不释放,自旋线程会一直占用 CPU,所以自旋适合锁持有时间很短、竞争不激烈的场景。


13.5 公平锁和非公平锁有什么区别?

公平锁是按照线程申请锁的顺序排队,先来先得。优点是线程不容易饿死,缺点是吞吐量低。
非公平锁是线程先尝试抢锁,抢不到再进入队列。它可能导致后来的线程先拿到锁,但吞吐量更高。
ReentrantLock 默认是非公平锁,因为性能更好;如果要公平锁,可以使用 new ReentrantLock(true)


13.6 什么是可重入锁?

可重入锁指的是同一个线程已经获取了一把锁之后,再次进入需要同一把锁的代码时,不会被自己阻塞。
synchronizedReentrantLock 都是可重入锁。
可重入锁底层通常会记录重入次数,比如 ReentrantLock 通过 AQS 的 state 记录。每加锁一次 state + 1,每释放一次 state - 1,直到 state = 0 才真正释放锁。


13.7 独享锁和共享锁有什么区别?

独享锁是一次只能被一个线程持有,比如 synchronizedReentrantLock、写锁。
共享锁可以被多个线程同时持有,比如 ReentrantReadWriteLock 的读锁。
读写锁中,读读可以共享,读写、写读、写写互斥,所以它适合读多写少的场景。


十四、最终理解总结

这篇文章表面上是在讲很多种锁,但核心其实是一个问题:

多线程同时访问共享资源时,Java 如何在“线程安全”和“性能”之间做平衡?

我的最终理解:

  1. 悲观锁适合冲突多的场景,先加锁再操作。
  2. 乐观锁适合冲突少的场景,更新时再判断有没有被改过。
  3. CAS是乐观锁的重要实现,但有 ABA、自旋开销和单变量限制。
  4. 自旋锁是不阻塞线程,而是让线程短暂空转等待锁释放。
  5. synchronized不是简单的重量级锁,它有锁升级机制。
  6. 偏向锁优化同一个线程反复进入同步块的场景。
  7. 轻量级锁优化轻微竞争场景,用 CAS + 自旋避免阻塞。
  8. 重量级锁适合竞争激烈场景,让线程阻塞,避免 CPU 空转。
  9. 公平锁更讲究顺序,但吞吐量较低。
  10. 非公平锁允许插队,吞吐量更高,ReentrantLock 默认就是非公平锁。
  11. 可重入锁允许同一线程重复获取同一把锁,但释放次数必须对应。
  12. 读写锁适合读多写少,读读共享,涉及写就互斥。
  13. AQS不是具体锁,而是很多 JUC 锁和同步器的底层框架。

最终一句话:

Java 锁的本质,是根据并发竞争程度和业务场景,在安全性、性能、公平性之间做取舍。


十五、自测题

基础题

  1. Java 为什么需要锁?
  2. 乐观锁和悲观锁的区别是什么?
  3. CAS 的三个操作数是什么?
  4. AtomicInteger 为什么线程安全?
  5. CAS 有哪些缺点?

进阶题

  1. synchronized 的锁升级过程是什么?
  2. 偏向锁和轻量级锁有什么区别?
  3. 自旋锁为什么不能一直自旋?
  4. ReentrantLock 默认公平锁还是非公平锁?为什么?
  5. 什么是可重入锁?底层如何记录重入次数?
  6. 读写锁为什么适合读多写少?
  7. AQS 中 state 的作用是什么?

面试场景题

  1. 如果一个接口高并发下需要统计访问次数,你会用 synchronized 还是 AtomicInteger?为什么?
  2. 如果一个共享资源写操作非常多,还适合用 CAS 吗?为什么?
  3. 如果多个线程只是读缓存数据,很少写,你会考虑什么锁?
  4. 如果 synchronized 竞争激烈,会发生什么?
  5. 如果 ReentrantLock 加锁两次,只释放一次,会发生什么?

十七、我最需要重点背的内容

1. CAS 是比较并交换,涉及内存值 V、期望值 A、新值 B。

2. CAS 的三个问题:ABA、自旋开销大、只能保证单变量原子性。

3. synchronized 锁升级:无锁 → 偏向锁 → 轻量级锁 → 重量级锁。

4. 偏向锁解决同一个线程重复进入同步块的成本问题。

5. 轻量级锁通过 CAS + 自旋避免线程阻塞。

6. 重量级锁会让等待线程阻塞,依赖 Monitor 和操作系统互斥锁。

7. ReentrantLock 默认非公平锁,因为吞吐量更高。

8. 可重入锁通过 state 记录重入次数,加锁几次就要释放几次。

9. 读写锁中读锁共享,写锁独占,适合读多写少。

10. AQS 是很多 JUC 锁和同步工具的底层框架,不是一把具体的锁。

posted @ 2026-05-25 11:24  程序员汪小姐  阅读(1)  评论(0)    收藏  举报