🔒 Java 锁全景分类与演进逻辑


一、为什么需要锁?

在多线程的世界里,如果没有规则,事情很容易“乱套”。想象一下:几个人同时往同一个储蓄罐里存钱和取钱,如果没人管理,很可能出现“多拿少放”的状况。Java 程序中也是类似:多个线程同时访问共享资源,如果不加以控制,就会出现各种怪异问题。

请添加图片描述

1.1 数据竞争(Race Condition)

当多个线程同时读写同一个变量时,最终结果可能完全取决于谁先执行。

例子:

int count = 0;

Thread t1 = new Thread(() -> count++);
Thread t2 = new Thread(() -> count++);
t1.start();
t2.start();

理论上 count 应该变成 2,但实际上可能只加到 1,因为 count++ 并不是原子操作,它包含 读取、加 1、写回 三步。如果两个线程交替执行,就可能“覆盖”彼此的修改。

想象两个厨师同时往锅里加盐,一个把 1 克加进去,另一个也加 1 克,但锅里最后只加了 1 克…这就是数据竞争。

1.2 内存可见性问题

即使变量被修改了,其他线程也不一定立即看到最新值。

  • Java 内存模型(JMM)允许线程把变量缓存在寄存器或 CPU 缓存中

  • 如果没有同步措施,线程可能看到的是“老数据”,就像有人更新了共享的 Excel 表格,你却还在看前一天的版本。

1.3 操作不可分割性问题

很多操作看起来像一步,但实际上是由多步组成:

  • 例子:i++ → 读取变量 i → 加 1 → 写回内存

  • 如果没有同步,多线程同时执行时,这三步可能被“打断”,最终结果出乎意料。

就像你在排队买票,同时两个售票员都给你出票,结果可能出票两次或者漏掉一张。


1.4 线程安全(Thread Safety)

线程安全的核心目标就是:

不管多少线程同时操作,程序的行为都像只有一个线程在运行一样一致。

  • 换句话说,线程安全就像是在一个拥挤的自助餐厅里,大家都排队取餐,互不干扰。

🔒 解决方案:锁机制

Java 提供了锁机制来保证线程安全:

  1. 控制访问顺序

    • 确保某一时刻,只有允许的线程访问共享资源
  2. 保证可见性

    • 修改立即对其他线程可见
  3. 保证原子性

    • 防止操作被中断或交叉执行

在 Java 中,锁机制经历了一条“从小白到高手”的演化之路:

时代 锁类型 特点
早期 synchronized 内置锁 简单、安全,但性能一般
现代 Lock 系列显式锁 灵活、可中断、可公平、条件等待
高级 读写锁 / StampedLock / CAS 提升读多写少场景吞吐量

简单总结:Java 的锁就像排队神器,它既能让大家有序取餐,又能兼顾速度,避免有人插队或吃到两份。


二、 最早期:内置锁(synchronized)

2.1 背景

synchronized 是 Java 最早提供的锁机制,就像 Java 世界里的守门员

  • 每个对象都有一个门锁(对象监视器 Monitor

  • 当线程想进入同步代码块时,必须先“拿到钥匙”

  • 拿不到钥匙的线程只能排队等候

这意味着:同一时刻,只有一个线程可以访问被保护的共享资源


2.2 使用方式

synchronized 有两种常用形式:

  1. 方法锁(锁住整个方法)
public synchronized void increment() {
    count++;
}

好比把整个餐厅锁起来,顾客只能一个一个进去吃饭。

  1. 代码块锁(锁住部分代码)
public void increment() {
    synchronized(this) {
        count++;
    }
}

就像锁住餐厅的一角,只允许一个人使用那部分自助餐台,其余区域不受影响,提高效率。


2.3 特性

特性 描述 比喻
独占锁 同一时刻只有一个线程能持有锁 守门员只允许一个人通过门口
可重入 同一线程可多次获取同一把锁 自己已经进门,可以自由出入,不会被阻止
非公平 线程获取顺序无法保证 排队时有人可能插队
不可中断 获取锁过程中无法打断 人正在门口排队,不能被请出去

2.4 优缺点

优点:

  1. 简单易用,语法直接支持

  2. JVM 内部优化较多(偏向锁、轻量级锁)

  3. 可保证线程安全,防止数据竞争

缺点:

  1. 粗粒度,竞争激烈时线程阻塞/唤醒开销大

  2. 不支持尝试获取锁(tryLock)或中断等待

  3. 编程灵活性有限,复杂场景难以控制

小结:synchronized 就像是“最早的排队守门员”,简单可靠,但面对高峰期(高并发),效率就不够理想了。


2.5 内部原理(Monitor + 对象头)

  1. 对象头(Mark Word)

    • 每个对象都带一个对象头,用来存储锁状态、线程 ID、偏向标志等信息
  2. Monitor

    • JVM 为对象分配一个 Monitor,用来管理锁的状态和等待队列
  3. 获取锁流程

    • 偏向锁:如果线程是第一次获得锁,偏向该线程,后续直接获取,无需 CAS

    • 轻量级锁:多线程尝试 CAS 更新对象头,成功则获得锁,否则进入自旋等待

    • 重量级锁:自旋失败后,线程挂起,等待唤醒

可以把对象头想象成钥匙包,Monitor 是门口的守门员。不同的锁优化策略就是守门员的不同工作模式:偏向锁偷懒、轻量级锁谨慎、重量级锁严格。


2.6 适用场景

  • 小临界区,低并发访问

  • 线程简单,递归调用较多

  • 不需要公平性和中断控制

典型例子:累加器、对象状态更新、简单缓存等场景。


2.7 小结

synchronized 是 Java 锁的起点:

  1. 解决了线程安全问题:保证同一时间只有一个线程访问共享资源

  2. 可重入:避免递归调用死锁

  3. JVM 优化:偏向锁、轻量级锁、重量级锁、自适应自旋

  4. 限制:编程灵活性低,高并发性能不足

可以理解为 Java 锁的“入门款守门员”,功能基础、可靠,但面对高并发,需要升级版(显式锁)来提升效率和灵活性。


三、JVM 内置锁升级机制(锁演进之道)

synchronized 乍一看很简单,但它背后隐藏了一套智能优化机制。JDK1.6 以后,JVM 对内置锁进行了多级升级,以提高性能。

理解这些升级机制,有助于我们写出高效的多线程代码。

3.1 偏向锁(Biased Locking)

概念

  • 偏向锁就是 JVM 给“唯一竞争线程”的一种特殊待遇:默认认为该锁大部分时间只有一个线程会访问

  • 拿锁时不用 CAS 或阻塞,直接偏向第一个获取锁的线程。

原理

  1. 对象头的 Mark Word 存储锁标志和线程 ID

  2. 第一个获取锁的线程“拿钥匙”,锁偏向该线程

  3. 后续同一线程再次访问,直接进入临界区,不做 CAS

  4. 如果其他线程也来竞争,偏向锁升级为轻量级锁

可以理解为:就像一家咖啡店的常客卡,门口守门员知道你是老顾客,不用排队,直接放行。
如果突然来了新顾客,守门员就得认真判断谁先到,这时偏向锁就“升级”。

优点

  • 大幅减少 CAS 操作,提高单线程访问性能

  • 低竞争场景下几乎零开销

缺点

  • 多线程竞争时需要撤销偏向锁,成本略高

适用场景

  • 对象主要被单线程访问

  • 临界区较小,频繁获取锁


3.2 轻量级锁(Lightweight Lock)

概念

  • 当偏向锁失效(出现竞争)时,JVM 升级为轻量级锁

  • 使用 CAS 自旋而非挂起线程,尝试在短时间内获取锁

原理

  1. 线程尝试通过 CAS 将对象头 Mark Word 的锁状态标记为自己

  2. 如果成功 → 获取锁

  3. 如果失败 → 自旋等待一段时间

  4. 自旋仍失败 → 升级为重量级锁,线程挂起

就像门口守门员允许短时间内几个人“抢先一步”进去试试看,如果大家同时冲门,守门员就会叫大家排队。

优点

  • 避免线程阻塞/唤醒开销

  • 适合短时间锁竞争

缺点

  • 自旋消耗 CPU

  • 锁持有时间长时效果不好

适用场景

  • 临界区很短,锁竞争不激烈

  • 高并发但线程执行时间短


3.3 重量级锁(Heavyweight Lock)

概念

  • 当轻量级锁竞争激烈、自旋失败时,JVM 升级为重量级锁

  • 线程获取不到锁就挂起,进入操作系统队列等待唤醒

原理

  1. CAS 自旋失败后,锁升级

  2. 阻塞等待,直到锁释放

  3. 锁释放时通知队列中的线程

就像守门员严格控制门口,大家排好队,有序入场,不能插队。
如果门口排队太多人,大家只能耐心等候。

优点

  • 保证安全,避免 CPU 空转

  • 可应对高并发锁竞争

缺点

  • 挂起/唤醒开销大

  • 性能不如轻量级锁

适用场景

  • 高竞争,临界区较长

  • 多线程同时访问共享资源


3.4 自适应自旋(Adaptive Spinning)

概念

  • JVM 根据历史锁的获取情况,动态决定线程是否自旋

  • 优化轻量级锁和重量级锁之间的平衡

原理

  1. 线程获取锁时,JVM 检查最近锁持有时间

  2. 如果锁释放快 → 线程选择自旋

  3. 如果锁释放慢 → 线程挂起,减少 CPU 消耗

就像守门员有“经验”:如果上次有人排队很快就轮到自己,他就让你先试一试;如果上次排队太久,他就叫你乖乖排队。

优点

  • 提高 CPU 利用率

  • 动态适应不同并发场景

缺点

  • 自旋仍然有耗费,长锁依然不适合

3.5 锁升级流程总结

  • 偏向锁:单线程访问时零开销

  • 轻量级锁:短时间竞争自旋,减少挂起

  • 重量级锁:长时间竞争,保证安全

  • 自适应自旋:智能选择自旋或阻塞,平衡性能和安全

偏向锁(Biased Locking)
   ↓ 竞争出现
轻量级锁(Lightweight Lock / 自旋)
   ↓ 自旋失败
重量级锁(Heavyweight Lock / 阻塞)

JVM 的锁升级机制是一套“智慧守门员策略”,自动在 低竞争、高性能 vs 高竞争、安全性之间切换,让我们用最少的成本保证线程安全。


四、JUC 显式锁体系:从黑盒到定制化

JDK1.5 引入了 java.util.concurrent.locks 包(简称 JUC),给了开发者一套比 synchronized 更灵活的“锁工具箱”。

如果说 synchronized 是“一键安全”的自动挡车,那么 Lock 接口体系 就是“手动挡 + 多种驾驶模式”的专业赛车,让你自己决定怎么开。

4.1 ReentrantLock(可重入独占锁)

4.1.1 为什么会出现它?

大家都知道 Java 有个“原生锁”——synchronized,好用是好用,但有几个“硬伤”:

  • 没啥个性:只能“要么锁定,要么等”,不能插队,不能超时,不能中断。

  • 不能排队叫号:它不关心你是不是第一个来的,后到的线程有时能“插队”成功。

  • 一条“等待通道”:所有线程只能在一条“等待区”干等,没办法分组等待。

于是,JUC(java.util.concurrent)里的大佬们造了个更灵活的家伙 —— ReentrantLock,它就像升级版的“锁王”,给了你更多玩法。

📌 典型应用场景:

  • 需要 公平锁(排队叫号,先来先服务)

  • 需要 响应中断 或者 设置超时时间

  • 一个锁里要有 多个等待区(Condition)


4.1.2 内部原理(锁是怎么玩的?)

ReentrantLock 的底层靠 AQS(AbstractQueuedSynchronizer) 打工。

理解它,就像看一把“可重入的锁”的生命周期:

  1. 尝试抢锁

    • CASstate 从 0 改成 1 → 成功 = 我占到锁了!
    • 失败 → 乖乖去 等待队列(FIFO 队列,按顺序排队)。
  2. 排队等号

    • 队列里的线程一个个排好队,等前面释放锁再尝试获取。
  3. 可重入

    • 如果当前线程已经拿过锁,再次请求 → state++(计数器+1)。

    • 释放时 → state--,直到归零才真正释放。

这就像进电梯。一个人(线程)进去把门关上(lock),如果还是他自己要进(重入),电梯不会拒绝,但“电梯占用次数”要
+1。只有等他把所有“占用次数”清零(unlock),电梯才算真正空出来。


AQS(抽象队列同步器)是什么

  1. 一句话概括

AQS 是 Java 并发包中实现锁和同步器的基础框架,通过一个状态值表示资源情况,并用 FIFO
队列管理获取失败的线程,实现高效、可控的线程阻塞与唤醒。像 ReentrantLock、Semaphore 等都是基于 AQS 构建的。


  1. AQS 在底层干了什么

1️⃣ 用 state 表示资源状态

  • 资源有没有被占、还能用几个
  • 具体含义由实现类决定(锁、信号量、倒计时)

2️⃣ 获取失败的线程进入 FIFO 队列

  • 不自旋、不忙等
  • 线程被安全地挂起等待

3️⃣ 释放资源时精准唤醒下一个线程

  • 只唤醒队首
  • 避免无效竞争和惊群

  1. AQS 的特点
  • 通用性强:一套机制支持多种同步工具

  • 性能友好:CAS + 阻塞,不浪费 CPU

  • 扩展性好:只需定义“什么时候算获取成功”

  • 语义可定制:独占 / 共享 都能支持


  1. AQS 用在了哪些地方

ReentrantLock、Semaphore、CountDownLatch、ReadWriteLock 等, 本质都是在用 AQS
这套排队和状态控制机制。


  1. 和 synchronized 的一句话对比

synchronized 是 JVM 内置的固定锁,
AQS 是 Java 层可扩展的同步框架,灵活性和可控性更强。



4.1.3 公平 vs 非公平

ReentrantLock 提供了两种“排队模式”:

  • 非公平锁(默认)

    • 线程来了先“插个队”试试,有空就直接上。

    • 优点:效率高,吞吐量大。

    • 缺点:有些老实人线程可能永远排不到(饥饿)。

  • 公平锁

    • 严格按队列顺序,先来先服务。

    • 优点:避免饥饿,保证公平。

    • 缺点:效率低点,因为要排队。

👉 就像买奶茶:

  • 非公平模式:谁敢冲到柜台前谁先买。

  • 公平模式:必须老老实实站队,先来后到。


4.1.4 常用方法示例

ReentrantLock lock = new ReentrantLock(true); // true = 公平锁

try {
    lock.lockInterruptibly(); // 获取锁,但能被中断
    // 临界区逻辑:大家不能同时改的共享资源
} catch (InterruptedException e) {
    System.out.println("线程被中断,放弃排队!");
} finally {
    lock.unlock(); // 千万别忘了释放锁
}

常见方法小抄:

  • lock():普通获取锁(不可中断)。

  • lockInterruptibly():获取锁时可以被中断。

  • tryLock():尝试获取一次,失败就拉倒。

  • tryLock(time, unit):尝试获取锁,超过时间自动放弃。

  • unlock():释放锁。

  • newCondition():创建一个条件对象,用来实现“多个等待区”。


4.1.5 优缺点一览

✅ 优点 ❌ 缺点
灵活:能超时、能中断、能公平 代码比 synchronized 麻烦
支持多条件等待(Condition) 手动加解锁,容易忘记 unlock
可重入,控制粒度更细 低竞争时性能略低于 synchronized

👉 总结:ReentrantLock 就像一辆配置高的车:功能多、能玩花活,但得熟悉操作,否则容易“忘打方向盘”。


4.1.6 适用场景

  • 高并发,必须严格控制 锁的顺序

  • 线程可能需要 超时放弃中途撤退

  • 一个锁需要 多个等待区(Condition),比如:

    • “厨师等菜切好”
    • “服务员等菜出锅”
    • “顾客等菜上桌”

这在 synchronized 里只能硬塞一个“等待区”,而 ReentrantLock 可以优雅拆分。

ReentrantLock = 可插队、可排队、可撤退、可多候场的“高级版 synchronized”。


4.2 ReentrantReadWriteLock(读写锁)

4.2.1 背景(为什么要用它?)

在很多系统里,读操作 >>> 写操作
比如:

  • 配置中心:大家都在查配置,偶尔才有更新

  • 商品详情页:成千上万次查询,偶尔改一次价格

如果用 独占锁(ReentrantLock / synchronized)

👉 写的时候确实安全,但 读也被串行化了,大大浪费性能。

所以就需要 读写锁

  • 读共享:多个读线程可以并发执行,互不干扰

  • 写独占:写线程来了,必须清场,避免读写冲突

这就像一个 图书馆

  • 多个读者(读线程)可以同时看书
  • 但管理员(写线程)要整理书架时,必须赶走所有读者

4.2.2 内部原理(它怎么玩?)

ReentrantReadWriteLock 还是靠 AQS(AbstractQueuedSynchronizer) 打工,不过 state 被“一分为二”:

  • 高 16 位:读锁计数(支持多个线程同时读,存储读锁的持有次数)(理论最大值 = 2¹⁶ = 65536)

  • 低 16 位:写锁计数(只能 0 或 1,一个线程独占)

规则:

  1. 读锁(共享锁)

    • 多个线程可以同时持有

    • 只要没有写线程在操作,就能直接读

  2. 写锁(独占锁)

    • 同一时刻只能有一个线程持有

    • 写线程存在时,读线程也要阻塞

  3. 锁降级(支持)

    • 写锁 → 读锁 可以无缝降级

    • 读锁 → 写锁 不允许(避免死锁)

👉 比喻:

  • 写锁像管理员清场:必须先赶走所有人,再独自操作。
  • 锁降级 = 管理员整理完后,顺手坐下读书(写→读)。

4.2.3 方法示例

ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();

// 读操作
rwLock.readLock().lock();
try {
    // 多线程可并发执行
    System.out.println("读取数据...");
} finally {
    rwLock.readLock().unlock();
}

// 写操作
rwLock.writeLock().lock();
try {
    // 独占执行,期间读写都被阻塞
    System.out.println("修改数据...");
} finally {
    rwLock.writeLock().unlock();
}

锁降级示例:

rwLock.writeLock().lock();
try {
    // 修改数据
    rwLock.readLock().lock(); // 写锁持有时再加读锁(锁降级)
    try {
        // 继续以读的身份持有
    } finally {
        rwLock.readLock().unlock();
    }
} finally {
    rwLock.writeLock().unlock();
}

4.2.4 优缺点(盘点一下)

✅ 优点 ❌ 缺点
读多写少场景性能高 写线程可能饿死(读优先模式)
支持锁降级(写→读) 编程复杂,要小心锁的获取顺序
保证线程安全 读锁和写锁可重入,嵌套不当容易死锁

⚠️ 注意点:

  • 写锁优先 or 读锁优先:默认是 非公平,可能导致写线程长期等不到机会

  • 锁升级不支持:读锁 → 写锁 会死锁

  • 组合使用要小心:读里加写、写里加读,必须搞清楚锁降级规则


4.2.5 适用场景

最典型的:读多写少 的业务逻辑。

  • 缓存:读数据很频繁,偶尔才更新

  • 配置中心:查询多,更新少

  • 商品/用户详情页:查询远多于修改

  • 本地数据结构:例如 MapList 的并发访问

ReentrantReadWriteLock = 图书馆的借阅规则:读书人可以一起看,但管理员整理时必须清场。
适合读多写少场景,能极大提升并发性能。


4.3 StampedLock(JDK8 乐观读锁)

4.3.1 背景(为啥还要再造轮子?)

虽然 ReentrantReadWriteLock 已经解决了“读多写少”的问题,
但是它依然是 悲观锁

  • 只要有写操作想进来,读操作都得停下排队。

  • 在读操作 远多于写操作 的场景下,这还是有点浪费。

于是 JDK8 又整了个“狠活” ——
👉 StampedLock,引入了 乐观读,进一步榨干性能。


4.3.2 特性(它有什么不同?)

  1. 乐观读(Optimistic Read)

    • 获取一个 stamp(戳记),相当于给数据打上版本号

    • 直接无锁读取数据(不阻塞写线程)

    • 读完后校验 stamp:

      • ✅ stamp 没变 → 数据安全

      • ❌ stamp 变了 → 有写操作 → 升级为悲观读锁

  2. 不可重入

    • 和 ReentrantLock 不同,StampedLock 不可重入

    • 必须手动管理好 stamp,否则容易死锁

  3. 写锁(独占锁)

    • 和 ReentrantReadWriteLock 一样,写操作仍然独占

👉 打个比方:

  • 悲观锁:你看书时,管理员怕有人改书,直接把书馆锁上。

  • 乐观读:你先拿书看(不加锁),结束后再问“有人改书没?”

  • 没人改 → 直接用 * 有人改 → 只能再借一次(升级成悲观锁)


4.3.3 方法示例(代码小抄)

StampedLock lock = new StampedLock();

// 乐观读
long stamp = lock.tryOptimisticRead();
try {
    int val = sharedData; // 无锁读取
    if (!lock.validate(stamp)) { // 校验 stamp
        // 有写操作干扰,回退到悲观读
        stamp = lock.readLock();
        try {
            val = sharedData;
        } finally {
            lock.unlockRead(stamp);
        }
    }
    System.out.println("读取值: " + val);
} finally {
    // 乐观读不需要 unlock
}

// 写锁
long wStamp = lock.writeLock();
try {
    sharedData++;
    System.out.println("更新数据...");
} finally {
    lock.unlockWrite(wStamp);
}

4.3.4 优缺点(盘点一下)

✅ 优点 ❌ 缺点
乐观读几乎无阻塞,吞吐量高 不可重入,使用复杂
写操作少时性能极佳 乐观读失败要回退,逻辑更啰嗦
支持写锁、悲观读 必须熟悉 stamp 的用法

⚠️ 注意:

  • 不能和 ReentrantLock 一样随便重入,否则 stamp 管理混乱 → 直接翻车
  • 适合 读非常多、写极少 的系统,否者回退成本高

4.3.5 适用场景

  • 缓存系统(查询远多于更新)

  • 统计类数据(读操作占绝大多数)

  • 性能敏感,需要进一步压榨读性能的系统


总结(锁家族大对比)

锁类型 独占/共享 可重入 公平性 可中断 优点 缺点 典型场景
ReentrantLock 独占 可选 灵活、支持 Condition 编程复杂 高并发互斥、可中断任务
ReentrantReadWriteLock 读共享/写独占 可选 读多写少性能高 写线程可能饿死 缓存、读多写少数据结构
StampedLock (JDK8) 读共享/写独占 乐观读高性能 不可重入、逻辑复杂 高并发读、统计系统

🔑 总结:

  • ReentrantLock:灵活、可中断、可插队

  • ReentrantReadWriteLock:读多写少,性能提升

  • StampedLock:乐观读,进一步压榨性能


五、按分类视角看锁

5.1 独占锁 vs 共享锁

独占锁(Exclusive Lock)

  • 定义:同一时刻只能被一个线程持有

  • 代表synchronizedReentrantLockStampedLock.writeLock()

  • 优点:简单、安全,保证线程安全

  • 缺点:并发度低,读多写少场景性能差

  • 适用场景:更新共享资源、临界区操作

💡 比喻:独占锁像 电梯:一次只能一个人占用电梯,别人得排队。

共享锁(Shared Lock)

  • 定义:允许多个线程同时持有

  • 代表ReentrantReadWriteLock.readLock()StampedLock.readLock()

  • 优点:读操作并发度高

  • 缺点:写线程可能饿死

  • 适用场景:缓存读多写少、查询频繁的数据结构

💡 比喻:共享锁像 图书馆:多个人可以同时看书,但管理员整理书架时必须清场。


5.2 公平锁 vs 非公平锁

特性 公平锁 非公平锁
获取顺序 FIFO,先到先得 当前线程可插队
优点 避免饥饿 吞吐量高
缺点 性能略低 可能导致线程饥饿
典型锁 ReentrantLock(true) synchronizedReentrantLock(false)
适用场景 顺序敏感、严格控制 高吞吐量并发任务

💡 总结:公平性牺牲性能,非公平锁更常用。


5.3 可重入锁 vs 不可重入锁

  • 可重入锁:同一线程可多次获取锁

    • 代表:synchronizedReentrantLockReentrantReadWriteLock

    • 优点:递归调用/嵌套调用不会死锁

  • 不可重入锁:每次获取必须释放

    • 代表:Semaphore(默认)

    • 缺点:嵌套使用要小心,否则容易死锁

💡 比喻:可重入锁像 钥匙圈,同一把钥匙可以多次开门;不可重入锁像 一次性门票,进一次得出门再买票。


5.4 悲观锁 vs 乐观锁

特性 悲观锁 乐观锁
核心假设 冲突概率大 冲突概率小
获取方式 先加锁 不加锁,提交时验证 CAS
代表 synchronizedReentrantLock AtomicIntegerStampedLock.tryOptimisticRead()
优点 安全性高 高吞吐量
缺点 阻塞、性能低 失败重试增加复杂性
典型场景 写操作多、强一致性 读多写少、高并发统计

💡 比喻

  • 悲观锁:担心别人抢你的东西,先把它锁起来
  • 乐观锁:先拿着用,用完再检查有没有被修改

5.5 自旋锁 vs 阻塞锁

  • 自旋锁(Spin Lock)

    • 线程短时间忙等,不挂起

    • 代表:JVM 轻量级锁、CAS

    • 优点:避免挂起/唤醒开销

    • 缺点:CPU 消耗高,长时间自旋效率低

  • 阻塞锁(Blocking Lock)

    • 获取不到锁就挂起

    • 代表:重量级锁、ReentrantLock

    • 优点:节省 CPU

    • 缺点:挂起/唤醒开销大

💡 自适应自旋:JVM 会根据历史成功率决定是否自旋。


5.6 可中断锁 vs 不可中断锁

  • 可中断锁

    • 代表:ReentrantLock.lockInterruptibly()

    • 优点:响应中断,避免死锁

  • 不可中断锁

    • 代表:synchronized

    • 优点:语法简单

    • 缺点:线程等待不可打断

💡 比喻

  • 可中断锁 = 等锁时能随时喊“我不等了!”
  • 不可中断锁 = 一旦等,就得等到底

总结

  • 独占/共享:控制并发度

  • 公平/非公平:控制顺序

  • 可重入/不可重入:控制嵌套

  • 悲观/乐观:控制冲突策略

  • 自旋/阻塞:控制等待方式

  • 可中断/不可中断:控制响应能力


六、特殊锁机制 & 工具类

6.1 Semaphore(信号量)

  • 作用:控制同时访问某资源的线程数

  • 典型场景:连接池、限流

  • 原理:就像 公共厕所的隔间,同时只能 N 个人上

Semaphore semaphore = new Semaphore(3); // 同时允许3个线程访问
semaphore.acquire();
try {
    // 临界区
} finally {
    semaphore.release();
}

6.2 CountDownLatch(倒计时锁)

  • 作用:等待一组操作完成后再继续

  • 场景:线程启动协调、并行任务等待

  • 原理:像 倒计时炸弹,等 N 个线程完成才触发

CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
    new Thread(() -> {
        // 执行任务
        latch.countDown();
    }).start();
}
latch.await(); // 等待3个线程完成

6.3 CyclicBarrier(循环屏障)

  • 作用:多个线程在某一点等待,然后同时继续

  • 场景:并行计算分段合并

  • 原理:像 大家在起跑线上一起等待发令枪

CyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println("所有线程到达屏障"));
for (int i = 0; i < 3; i++) {
    new Thread(() -> {
        // 执行任务
        barrier.await();
    }).start();
}

6.4 Phaser(可重用分阶段屏障)

  • 特性:多阶段、动态注册、可重复使用

  • 场景:复杂任务流水线

  • 原理:比 CyclicBarrier 更灵活的分段任务调度器


6.5 分段锁(Segmented Lock)

  • 场景:提高哈希表的并发度

  • 代表:JDK7 ConcurrentHashMap

  • 原理:将数据拆分为多个段,每段独立加锁 → 并发访问效率提升

💡 比喻:像图书馆分区域管理,不同区域的读者互不影响


6.6 锁粗化 / 锁消除

  • 锁粗化:JIT 将多次连续加锁合并 → 减少锁操作开销

  • 锁消除:JIT 发现锁无竞争 → 自动去掉锁

  • 目标:减少不必要的加锁/解锁,提高性能


6.7 应用场景与最佳实践

场景 推荐锁类型 原因
高并发读多写少 ReentrantReadWriteLock / StampedLock 读共享提高吞吐量
高并发独占操作 ReentrantLock 可中断/tryLock控制
严格顺序执行 公平锁 避免线程饥饿
资源限制 Semaphore 控制并发访问数量
任务等待 CountDownLatch / CyclicBarrier / Phaser 线程协调
小临界区 synchronized 简单、JVM优化性能高

实践建议

  1. 锁粒度尽量小,避免长时间持锁
  2. 优先考虑乐观锁或 CAS,减少阻塞
  3. 读多写少场景用读写锁,提升吞吐量
  4. 复杂场景使用显式锁,搭配 Condition 控制多条件等待

锁类型对比总表

锁类型 独占/共享 可重入 公平性 可中断 优点 缺点 典型场景
synchronized 独占 简单、JVM优化 低并发性能差 小临界区、低竞争
ReentrantLock 独占 可选 灵活、Condition 编程复杂 高并发互斥、可中断
ReentrantReadWriteLock 共享/独占 可选 读多写少性能高 写饥饿、复杂 缓存、读多写少数据
StampedLock 共享/独占 乐观读高性能 不可重入、逻辑复杂 高并发读、统计数据
Semaphore 独占 控制并发数 无可重入 资源限流、连接池
CountDownLatch - 线程等待 只能用一次 任务协调
CyclicBarrier - 循环等待 注册线程固定 分段任务协调
Phaser - 多阶段灵活 编程复杂 多阶段任务流水线

工具类锁 = 锁的“百变工具箱”
小资源用 synchronized、高并发互斥用 ReentrantLock、读多写少用读写锁、超高并发读用 StampedLock、线程协调/资源限制用 Semaphore/Barrier/Phaser。


🔄 Java 锁家族总结

  1. synchronized

    • 安全、简单、JVM 内置

    • 缺点:性能有限,读多写少容易浪费

  2. 显式锁(ReentrantLock)

    • 功能升级:可中断、可公平、支持 Condition

    • 适合高并发互斥、需要灵活控制的场景

  3. 读写锁(ReentrantReadWriteLock / StampedLock)

    • 读多写少场景神器

    • StampedLock 引入 乐观读,吞吐量进一步提升

  4. JVM 锁优化

    • 偏向锁、轻量级锁、自适应自旋

    • 在低竞争场景下自动提高性能

  5. 工具类锁

    • Semaphore:控制同时访问的线程数

    • CountDownLatch:等待一组任务完成

    • CyclicBarrier:线程同步到某个点

    • Phaser:多阶段、可重复使用的任务屏障

  6. 实践原则

    • 锁粒度尽量小 → 避免长时间持锁

    • 优先使用 乐观锁 / CAS → 减少阻塞

    • 读多写少 → 用 读写锁 提升吞吐

    • 显式锁适合复杂场景,搭配 Condition 多条件等待

💡 一句话记忆

“先内置锁,再显式锁,再分读写,再靠 JVM 优化,再用工具锁,锁要小、读优先、复杂场景显式管理。”

更多推荐