synchronized锁,ReentrantLock 锁

目录

synchronized的底层实现:

锁升级

1.无锁状态(初始状态)

2.偏向锁(单线程优化)

3.轻量级锁(低竞争优化)

[4. 重量级锁(高竞争场景)](#4. 重量级锁(高竞争场景))

锁消除:

锁粗化:

ReentrantLock的底层实现

非公平锁获取锁(lock)

公平锁与非公平锁的区别

锁释放

使用场景:适用场景的差异


synchronized的底层实现:

synchronized核心是通过 对象头(Mark Word) 和 管程(Monitor) 控制线程互斥。再在JDK 1.6 引入的 锁升级 机制大幅优化了其性能。

synchronized 的锁机制并非一开始就是重量级锁,而是根据并发竞争强度 动态升级(无锁 → 偏向锁 → 轻量级锁 → 重量级锁),以平衡性能和安全性。


锁升级

1.无锁状态(初始状态)

指对象刚被创建,未被任何线程加锁,可以被任何线程使用~

2.偏向锁(单线程优化)

  • 触发条件 :当第一个线程(线程 A)尝试获取锁时,JVM 会通过 CAS 操作将Mark Word修改为偏向锁状态,并记录线程 A 的 ID(此时无需真正 "加锁",只是标记偏向的线程)。
  • 升级过程
    1. 当第一个线程(线程 A)尝试获取锁时,JVM 通过 CAS 操作将 Mark Word修改为偏向锁状态和 Mark Word 中的 "偏向标志" 设为 1,并写入线程 A 的 ID。
    2. 后续线程 A 再次获取锁时,只需对比 Mark Word 中的线程 ID 是否为自己,无需 CAS 操作(几乎无开销)。
  • Mark Word 变化 :存储偏向线程 ID,状态标记仍为 01(但偏向标志为 1,区分于无锁)。
  • 优化: 优化了单线程重复获取锁 的场景(现实中多数锁在一段时间内仅被一个线程使用)。

3.轻量级锁(低竞争优化)

  • 设计目的 :应对 少量线程短时间竞争 的场景,避免直接进入重量级锁(减少内核态切换开销)。
  • 触发条件 :当有第二个线程(线程 B)尝试获取锁时,发现Mark Word中记录的是线程 A 的 ID(偏向锁状态),此时会触发偏向锁的撤销:
    • 若线程 A 已退出同步块(锁已释放),则将Mark Word重置为无锁状态,线程 B 通过 CAS 将其改为轻量级锁状态(记录线程 B 的锁记录指针)。
    • 若线程 A 仍持有锁,则偏向锁升级为轻量级锁,线程 A 和线程 B 通过自旋(空转等待)尝试获取锁。
  • 升级过程
    1. 线程在自己的 栈帧 中创建 锁记录(Lock Record),存储当前 Mark Word 的副本(Displaced Mark Word)。
    2. 通过 CAS 操作 将对象的 Mark Word 改为 指向当前线程锁记录的指针 (状态标记改为 00)。
    3. 若 CAS 成功,线程获取锁;若失败(说明有竞争),线程进入 自旋(Spin) 重试(循环尝试 CAS,不放弃 CPU)。
  • 自旋意义:短时间竞争下,自旋可能在锁释放前成功获取,避免阻塞(用户态操作,开销小于内核态)。
  • 触发升级:若自旋失败(如自旋次数超过阈值,或竞争激烈),轻量级锁升级为重量级锁。

4. 重量级锁(高竞争场景)

  • 设计目的 :应对 多线程激烈竞争长时间持有锁 的场景,依赖操作系统保证互斥。
  • 实现依赖 :操作系统的 互斥量(Mutex) 和 JVM 的 管程(Monitor)
    • Monitor 是一个 C++ 对象,包含 owner(持有锁的线程)、EntryList(等待锁的线程队列)、WaitSet(调用 wait() 等待的线程队列)。
  • 触发条件:当自旋结束后,线程仍未获取到锁(例如线程持有锁的时间很长,或竞争线程过多),轻量级锁会膨胀为重量级锁:
  • 升级过程
    1. 锁升级为重量级后,Mark Word 改为 指向 Monitor 的指针 (状态标记改为 10)。
    2. 未获取锁的线程进入 EntryList阻塞(放弃 CPU,进入内核态),等待持有锁的线程释放后被唤醒。
  • 特点:开销大(用户态 ↔ 内核态切换),但能保证高竞争场景下的线程安全。
  • 用户态→内核态切换的高开销 :源于上下文保存、页表切换、安全检查等硬件和软件层面的复杂操作,本质是 "权限隔离" 的代价。
    1. 上下文切换的硬件开销

      切换时,CPU 必须保存当前用户态的上下文信息(如程序计数器、栈指针、通用寄存器值等)到内存(通常是内核栈),再加载内核态的上下文。这个过程涉及多次内存读写(内存速度比 CPU 寄存器慢 100 倍以上),且需要严格保证操作的原子性(避免数据错乱),硬件层面的指令执行成本很高。

锁消除:

编译器在编译时会分析代码,如果发现某些锁不可能被多线程访问,会自动消除:

java 复制代码
// 原始代码
public String concatString(String s1, String s2) {
    StringBuffer sb = new StringBuffer();  // 局部变量
    sb.append(s1);
    sb.append(s2);
    return sb.toString();
}

// StringBuffer的append方法是synchronized的
// 但由于sb是局部变量,编译器会消除这些锁

锁粗化:

若多个连续的同步块使用同一把锁,JVM 会将它们合并为一个大的同步块,减少加锁 / 解锁的次数。

java 复制代码
// 原始代码:频繁加锁解锁
for (int i = 0; i < 1000; i++) {
    synchronized(obj) {
        // 操作
    }
}

// 优化后:扩大锁的范围
synchronized(obj) {
    for (int i = 0; i < 1000; i++) {
        // 操作
    }
}

ReentrantLock的底层实现

ReentrantLock 的底层完全依赖 AQS(AbstractQueuedSynchronizer,抽象队列同步器) 实现。AQS 是 Java 并发工具的基础框架,通过两个核心部分管理同步逻辑:

  1. 同步状态(state):一个 volatile int 变量,用于记录锁的持有状态(state=0 表示未锁定,state>0 表示被持有,数值等于重入次数),还有一个exclusiveOwnerThread (持锁线程
  2. 双向阻塞队列:当线程获取锁失败时,会被包装成 Node 节点加入队列,按 FIFO 顺序等待被唤醒(类似 "等待队列")。

非公平锁获取锁(lock)

  1. 首次抢锁 :线程直接通过 CAS 尝试将 state 从 0 改为 1(compareAndSetState(0, 1))。若成功,标记当前线程为锁持有者(setExclusiveOwnerThread)。
  2. 抢锁失败 :若 CAS 失败(锁已被持有),调用 AQS 的 acquire(1) 方法,进入后续流程:
    • tryAcquire 重试 :检查锁是否被当前线程持有(可重入性),若是则递增 state(重入次数 + 1);若不是,则抢锁失败。
    • 入队阻塞 :抢锁失败的线程被包装成 Node 节点,通过 addWaiter 加入队列尾部,再通过 acquireQueued 自旋等待(或阻塞),直到前驱节点释放锁并唤醒自己。

公平锁与非公平锁的区别

公平锁会检查队列中是否有等待的线程,非公平锁直接抢占,这是性能差异的根本原因"

锁释放

  1. 释放逻辑 :调用 unlock() 时,实际调用 AQS 的 release(1) 方法,核心是 tryRelease 方法:
    • 递减 state(重入次数 - 1),若当前线程不是持有者则抛异常(保证安全性)。
    • state 减为 0 时,清除持有者标记,返回 true 表示完全释放锁。
  2. 唤醒线程 :完全释放锁后,通过 unparkSuccessor 唤醒队列中等待的后继线程,使其重新竞争锁。

"释放锁时会检查重入次数,只有state减到0才真正释放,然后唤醒队列中的下一个线程"

当然ReentrantLock 的核心价值在于 灵活性和可控性,其功能覆盖了基础互斥、可重入、公平性选择、中断响应、超时控制、多条件等待等场景,适合复杂并发逻辑的实现。但需注意手动释放锁(通常在 finally 中),避免死锁风险。



使用场景:适用场景的差异

  • 优先用 synchronized 的场景

    • 简单同步需求(如普通方法 / 代码块同步),无需复杂特性。
    • 希望减少手动管理锁的风险(避免忘记释放锁导致死锁)。
    • 依赖 JVM 自动优化(如偏向锁对单线程场景的优化)。
  • 优先用 ReentrantLock 的场景

    • 需要 公平锁(保证线程按等待顺序获取锁)。
    • 需要 中断等待中的线程(如超时放弃或响应外部中断)。
    • 需要 多个条件变量(如生产者 - 消费者模型中区分 "空""满" 条件)。
    • 需要 查询锁状态(如判断锁是否被持有、当前线程重入次数)。
相关推荐
Scott9999HH7 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
码智社8 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海8 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖9 小时前
JDK 26 新特性详解
java·开发语言
马优晨9 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区11 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大12 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai12 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
এ慕ོ冬℘゜12 小时前
前端基础:什么是时间戳?JS获取时间戳三种方法与实战用途
开发语言·前端·javascript
执明wa12 小时前
LayoutInflater详解: XML是如何变成View的?
android·xml·开发语言·android studio