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 的场景

    • 需要 公平锁(保证线程按等待顺序获取锁)。
    • 需要 中断等待中的线程(如超时放弃或响应外部中断)。
    • 需要 多个条件变量(如生产者 - 消费者模型中区分 "空""满" 条件)。
    • 需要 查询锁状态(如判断锁是否被持有、当前线程重入次数)。
相关推荐
ankleless1 小时前
Python 数据可视化:Matplotlib 与 Seaborn 实战
开发语言·python
Gavin_9152 小时前
一文速通Ruby语法
开发语言·ruby
搞一搞汽车电子2 小时前
vs studio 2017项目不支持studio vs2022
开发语言
witkey_ak98962 小时前
python 可迭代对象相关知识点
开发语言·python
听风的码3 小时前
Vue2封装Axios
开发语言·前端·javascript·vue.js
素界UI设计4 小时前
建筑行业变革:用Three.js构建BIM数据可视化孪生平台
开发语言·javascript·信息可视化
王廷胡_白嫖帝5 小时前
Qt个人通讯录项目开发教程 - 从零开始构建联系人管理系统
开发语言·qt
疯狂的代M夫6 小时前
C++对象的内存布局
开发语言·c++
mit6.8246 小时前
Linux下C#项目构建
开发语言·c#