核心定位:这是锁的设计思想与分类维度,不局限于Java语言,是所有并发锁的底层逻辑。 ⚠️ 面试必记:这6组是6个独立的分类角度,不是6种锁。同一个锁(比如synchronized)可以同时属于多个分类。


1. 悲观锁 vs 乐观锁(冲突预判维度,面试第一问)

悲观锁

核心思想:默认一定会发生并发冲突,每次操作前先上锁,拿不到锁就阻塞等待。

  • 通俗例子(PDF原例):同学A找老师问问题,默认老师很忙。先发消息预约(加锁),老师同意了才过去问;老师忙就下次再约。
  • 代表实现:synchronizedReentrantLock、数据库 select ... for update
  • 特点:
    1. 严格互斥,线程安全
    2. 拿不到锁就阻塞,线程挂起
  • 适用场景:并发冲突多、写操作多的场景

乐观锁

核心思想:默认不会发生并发冲突,不上锁直接操作;提交更新时才校验是否冲突。冲突则返回失败,由业务层决定重试或报错。

  • 通俗例子(PDF原例):同学B找老师问问题,默认老师很闲。直接过去找老师;老师有空就问完,老师忙就直接走,下次再来。
  • 代表实现:CAS机制(Atomic原子类)、数据库版本号机制
  • 特点:
    1. 不加锁,无阻塞,并发度高
    2. 冲突频繁时,反复重试会消耗CPU
  • 适用场景:并发冲突少、读操作多的场景

✨ 面试补充

synchronized 不是固定的悲观/乐观:

  • 初始竞争少:偏向乐观思路,尽量不加锁
  • 检测到锁竞争频繁:自动切换为悲观锁策略

面试标准答法

问:谈谈你对乐观锁和悲观锁的理解? 答:悲观锁认为并发冲突概率高,操作前先加锁互斥,典型实现是synchronized;乐观锁认为冲突概率低,不加锁直接操作,提交时校验冲突,典型实现是CAS版本号机制。悲观锁适合写多冲突多的场景,乐观锁适合读多冲突少的场景。


2. 重量级锁 vs 轻量级锁(synchronized底层实现维度)

底层链路:CPU原子指令 → 操作系统mutex互斥锁 → JVM实现synchronized 区分核心:是否重度依赖操作系统内核的mutex互斥锁

重量级锁

核心:加锁重度依赖操作系统提供的mutex互斥锁。

  • 过程:抢锁失败 → 操作系统把线程挂起休眠 → 锁释放后操作系统唤醒线程
  • 代价:大量用户态 ↔ 内核态切换,触发线程调度,开销极高
  • 触发场景:锁竞争激烈、锁持有时间长

轻量级锁

核心:尽量在用户态通过CAS完成加锁,不调用操作系统mutex;实在抢不到才升级为重量级锁。

  • 过程:抢锁失败 → 短暂自旋重试,不挂起线程
  • 代价:少量内核态切换,线程调度少,开销低
  • 触发场景:多个线程交替使用锁,几乎不同时竞争

✨ 通俗理解

  • 用户态:银行大厅自己能办的事,成本可控、速度快
  • 内核态:必须到窗口交给工作人员办,成本不可控、速度慢
  • 轻量级锁:尽量大厅自己解决,少找窗口
  • 重量级锁:全程找窗口办理,来回排队开销大

面试考点

synchronized 锁升级:一开始是轻量级锁;锁冲突严重时,自动膨胀为重量级锁。锁只能升级,不能降级。


3. 自旋锁 vs 挂起等待锁(抢锁失败后的行为维度)

这是「抢锁失败时,线程怎么做」的两种策略

挂起等待锁(阻塞锁)

抢锁失败 → 线程直接挂起休眠,让出CPU资源;锁释放后由操作系统唤醒。

  • 优点:不消耗CPU,锁持有时间长时更划算
  • 缺点:线程挂起/唤醒需要内核态切换,响应慢、开销大
  • 代表:synchronized重量级锁

自旋锁(Spin Lock)

抢锁失败 → 不放弃CPU,原地循环反复尝试抢锁,直到拿到锁为止。

  • 伪代码
while (抢锁() == 失败) {
    // 空转循环,持续重试
}
  • 优点:锁很快释放时,无需线程切换,响应极快,效率高
  • 缺点:锁持有时间长时,循环空转持续消耗CPU,锁冲突不是很激烈的时候高效
  • 代表:synchronized轻量级锁底层、CAS实现的自定义锁

✨ 面试关联

自旋锁是轻量级锁的典型实现方式;synchronized的轻量级锁阶段,就是用自适应自旋锁实现的。

自适应自旋:不会无限自旋,达到一定次数/时间还拿不到锁,就停止自旋,升级为重量级锁。

通俗例子

  • 挂起等待锁:表白被拒后彻底躺平,很久之后女神回头才再尝试
  • 自旋锁:每天早安晚安持续刷存在感,女神一分手立刻上位

4. 公平锁 vs 非公平锁(锁分配顺序维度)

场景:线程A先拿到锁,线程B、C先后抢锁失败,进入等待;A释放锁。

公平锁

遵守先来后到,等待最久的线程优先拿到锁。

  • 实现:需要额外的等待队列,记录线程的先后顺序
  • 优点:不会饿死线程,顺序可控
  • 缺点:需要维护队列,性能略低

非公平锁

不遵守先来后到,释放锁时,所有等待线程随机抢,谁抢到是谁的。

  • 实现:操作系统原生线程调度就是随机的,默认就是非公平
  • 优点:性能更高,不需要维护队列
  • 缺点:可能出现线程饿死(某个线程一直抢不到)

✨ 面试考点

  1. synchronized非公平锁
  2. ReentrantLock 默认非公平锁;构造方法传入true可开启公平锁

5. 可重入锁 vs 不可重入锁(同一个线程能否重复加锁维度)

不可重入锁

同一个线程,没释放锁的情况下再次加同一把锁,会直接阻塞自己,造成死锁

  • 例子:递归函数里加锁,第二次加锁就卡死自己
  • 代表:Linux系统原生mutex互斥锁

可重入锁(递归锁)

允许同一个线程多次获取同一把锁,不会自己卡死自己。

  • 实现原理:锁内部记录「持有锁的线程ID」+「加锁次数计数器」
    • 同一个线程再次加锁:计数器+1,直接放行
    • 解锁:计数器-1,计数器归0时锁才真正释放
  • 代表:synchronizedReentrantLock(Java中所有标准锁都是可重入的)

面试考点

问:synchronized是可重入锁吗? 答:是。内部会记录持有线程和加锁次数,同一个线程多次加锁不会死锁,解锁次数和加锁次数匹配才会真正释放锁。


6. 读写锁(ReadWriteLock)(读写场景优化)

背景

普通互斥锁不管读还是写,一律互斥。但多线程同时读数据,不会有线程安全问题,全互斥会浪费性能。 读写锁就是为了优化「读多写少」场景诞生的。

三条核心规则

  1. 读锁 和 读锁:不互斥(多个线程可以同时读)
  2. 写锁 和 写锁:互斥
  3. 读锁 和 写锁:互斥(读的时候不能写,写的时候不能读)

代表实现

ReentrantReadWriteLock,包含读锁ReadLock和写锁WriteLock,都支持lock/unlock。

适用场景

频繁读、很少写的场景,比如配置信息、学生名册、商品详情查询。

例子:教务系统,每天几十上百次查学生/查作业(读),很久才修改一次名单/布置作业(写)。

面试考点

synchronized 不是读写锁,是普通互斥锁。


例子:synchronized 锁属性全总结(面试必背)

锁策略是从不同维度描述锁的特性,synchronized 不是单一类型的锁,多个维度都支持自适应升级

分类维度synchronized 属性详细说明
悲观锁 / 乐观锁自适应初始预测锁冲突概率低,以乐观锁思路运行;检测到冲突频繁后,自动升级为悲观锁。
重量级锁 / 轻量级锁自适应初始为轻量级锁(用户态CAS实现);锁冲突加剧后,膨胀为重量级锁(依赖操作系统mutex,内核态实现)。
自旋锁 / 挂起等待锁自适应轻量级锁阶段采用自适应自旋策略抢锁;升级为重量级锁后,抢锁失败线程进入挂起等待。
读写锁 / 普通互斥锁不是读写锁属于普通互斥锁,读操作、写操作全部互斥,不支持读读并发。
可重入锁 / 不可重入锁可重入锁允许同一个线程多次获取同一把锁;内部通过「持有线程ID + 加锁计数器」实现,不会自己锁死自己。
公平锁 / 非公平锁非公平锁不遵守先来后到规则,锁释放后所有等待线程随机竞争,可能出现线程饥饿。

锁升级完整流程(只能升级,不能降级)

  1. 无锁 → 偏向锁:只有一个线程使用锁时,仅在对象头做标记,避免加锁开销。
  2. 偏向锁 → 轻量级锁:出现第二个线程竞争锁,撤销偏向锁,升级为轻量级锁,通过CAS+自适应自旋抢锁。
  3. 轻量级锁 → 重量级锁:自旋多次仍抢不到锁、锁持有时间变长,锁膨胀为重量级锁,依赖操作系统mutex,抢锁失败线程挂起等待。

一句话速记

synchronized 自适应升级:偏→轻→重;可重入、非公平、不是读写锁。

📌 第一章 面试核心总结

  1. 按冲突预判分:悲观锁、乐观锁
  2. 按底层实现分:重量级锁、轻量级锁
  3. 按失败行为分:自旋锁、挂起等待锁
  4. 按分配顺序分:公平锁、非公平锁
  5. 按能否重入分:可重入锁、不可重入锁
  6. 按读写区分:普通互斥锁、读写锁

synchronized标签:悲观锁、可重入锁、非公平锁;从轻量级锁起步,竞争激烈升级为重量级锁;轻量级阶段采用自适应自旋。


Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐