当前位置: 首页 > news >正文

Synchronized原理解析

Synchronized 是 Java 中用于保证线程安全的关键字。你可以把它想象成一个“房间的钥匙”:同一时间只有一个线程能拿着这把钥匙进入房间(同步代码块或方法),执行任务,其他线程只能在门口排队等待。

下面我会用通俗的方式,由浅入深地讲解它的用法、原理和优化。

🔐 一、Synchronized 的用法:三种“锁门”方式

Synchronized 主要有三种用法,对应三种不同的“锁门”方式。

1. 修饰实例方法(锁当前实例)

public class Counter {private int count = 0;// 锁住当前实例(this)public synchronized void increment() {count++;}
}

• 锁的是谁:当前对象实例(this)。

• 好比:你家的大门钥匙。只有拿到你家钥匙的人(线程)才能进入你家(执行此方法),但别人可以去其他家(其他实例)。

2. 修饰静态方法(锁类的Class对象)

public class Logger {private static int logCount = 0;// 锁住整个类(Logger.class)public static synchronized void log() {logCount++;}
}

• 锁的是谁:类的 Class 对象(如 Logger.class)。这个对象在 JVM 中只有一个。

• 好比:公司大门的钥匙。所有员工(所有实例)进出公司(调用此静态方法)都必须用这一把钥匙,同一时间只允许一个人进出。

3. 修饰代码块(灵活指定锁对象)

public class Cache {private final Object lock = new Object(); // 专用锁对象private Map<String, String> data = new HashMap<>();public void put(String key, String value) {// 只锁需要同步的代码部分synchronized (lock) {data.put(key, value);}}
}

• 锁的是谁:括号里指定的任意对象(通常是私有 final 对象)。

• 好比:只锁保险柜,而不是锁整个房间。这样别人可以同时进房间做其他事,只有操作保险柜时才需要排队。这是最推荐的方式,因为它粒度最细,性能最好。

为了更直观地理解这三种用法及其锁对象的区别,可以参考下面的表格:

使用方式 锁对象 影响范围 类比

修饰实例方法 当前实例 (this) 所有同步的实例方法(同一个实例) 自家大门钥匙

修饰静态方法 类的 Class 对象 所有同步的静态方法(所有实例) 公司大门钥匙

修饰代码块 指定任意对象 同步的代码块(取决于锁对象) 专用保险柜钥匙

⚙️ 二、Synchronized 的原理:钥匙如何工作

理解了怎么用,我们再来看看它底层是怎么实现的。这涉及到 对象头(Object Header) 和 Monitor(监视器锁) 的概念。

1. 对象头与 Mark Word

在 JVM 中,每个 Java 对象在内存中都包含一个对象头,其中有一部分叫 Mark Word。

• Mark Word 就像是对象的“身份证”,记录了一些运行时信息,例如:

  • 哈希码(HashCode)
  • 垃圾回收(GC)分代年龄
  • 锁的状态标志
  • 持有锁的线程 ID

synchronized 的锁信息就存储在 Mark Word 中。随着竞争加剧,锁的状态会发生变化,Mark Word 里存储的内容也会随之改变。

2. Monitor 机制

你可以把 Monitor 理解为一个结构特殊的“房间”,它只允许一个线程进入。每个 Java 对象都关联着一个 Monitor。这个 Monitor 主要有三个部分:

• Owner(所有者):当前持有锁、正在房间内执行任务的线程。

• EntryList(入口队列):其他想进入房间的线程都在这里排队等待(状态为 BLOCKED)。

• WaitSet(等待集合):已经进入过房间的线程,因为某些条件不满足(调用了 wait() 方法),主动释放锁并在这里等待唤醒(状态为 WAITING)。

当一个线程想执行 synchronized 代码块时,它需要先拿到对应对象的 Monitor。如果 Owner 为空,它就成功成为 Owner。如果 Owner 已有线程,它就进入 EntryList 排队等待。

为了更直观地理解对象头、Mark Word 和 Monitor 之间的关系,以及线程如何获取锁,可以参考下面的示意图:

Java Object
Object Header
Instance Data
Padding
Mark Word
Klass Pointer
Lock Status Flags
HashCode
GC Age
...
Lock Upgrade Path
No Lock
Biased Lock
Lightweight Lock
Heavyweight Lock
Pointer to Monitor

🚀 三、锁升级:JVM 的智能优化

早期的 synchronized 性能较差,一有竞争就直接让线程排队等待(重量级锁)。但在很多情况下,竞争并不激烈。所以 JVM 在 JDK 1.6 之后引入了锁升级机制,根据实际竞争情况,智能地选择最合适的锁。

锁升级是单向的,路径如下:
无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁

1. 偏向锁 (Biased Locking)

• 场景:只有一个线程访问同步块。比如,项目刚开始只有你一个人在做。

• 做法:JVM 会在 Mark Word 中记录这个线程的 ID。以后这个线程再来加锁时,发现是自己,就直接进去,连简单的 CAS 操作都省了,开销极小。

• 升级:一旦有第二个线程来尝试获取锁,偏向锁就立刻撤销,升级为轻量级锁。

2. 轻量级锁 (Lightweight Locking)

• 场景:多个线程交替访问同步块,没有同时竞争。比如,你和同事轮流使用会议室,但从不撞车。

• 做法:线程通过 CAS 操作(一种乐观锁,简单理解为比较并交换)来尝试获取锁。如果成功,就拿到锁;如果失败,并不会立刻阻塞,而是自旋(循环重试 CAS)一小段时间。

• 升级:如果自旋了一定次数后还没拿到锁(说明竞争加剧了),锁就会升级为重量级锁。

3. 重量级锁 (Heavyweight Locking)

• 场景:多线程激烈竞争,多个线程同时想获取锁。

• 做法:这就是最传统的锁。没拿到锁的线程会直接进入阻塞状态(进入 Monitor 的 EntryList 排队),等待操作系统调度唤醒。这涉及到用户态到内核态的切换,开销最大。

• 这是最终兜底的方案,保证了在高并发场景下的正确性。

下图总结了锁升级的全过程及其触发条件:

Yes
No
Lock Status
No Lock
Initial State
First Thread
Accesses (T1)
Biased Lock
Thread ID: T1
Second Thread
Attempts (T2)
Lightweight Lock
CAS Spin
Spin Succeeds?
T2 Acquires Lock
Heavyweight Lock
OS Mutex, Threads Blocked
High Overhead

🛠️ 四、优化建议:写出更高效的同步代码

理解了原理,我们就能更好地使用和优化它。

  1. 减小锁粒度:这是最重要的原则。只锁必要的代码,用同步代码块代替同步方法。就像前面只锁保险柜的例子。

  2. 缩短锁持有时间:同步代码块里不要执行耗时操作(如IO操作),尽快做完事,释放锁。

  3. 选择专用锁对象:不要使用 String、Integer 等常量对象或可能被共享的对象作为锁,容易导致意想不到的阻塞。应该使用 private final Object lock = new Object() 这种专为锁而生的对象。

  4. 考虑替代方案:
    ◦ 如果是读多写少的场景(比如缓存),可以考虑使用 ReadWriteLock,它允许多个线程同时读,比 synchronized 吞吐量更高。

    ◦ 对于简单的原子操作(如自增),可以使用 AtomicInteger 等原子类,它基于 CAS,通常性能更好。

    ◦ 对于复杂并发结构,直接使用 ConcurrentHashMap 等并发容器,它们内部实现了更高效的同步机制。

💎 小结

• 用法:三种方式——实例方法、静态方法、代码块。优先使用同步代码块,指定专用锁对象。

• 原理:基于 JVM 的 Monitor 机制,通过对象头的 Mark Word 记录锁状态。

• 优化:JVM 会自动进行锁升级(无锁 → 偏向锁 → 轻量级锁 → 重量级锁),根据竞争激烈程度智能选择,兼顾性能和安全性。

• 实践:编写代码时牢记减小锁粒度、缩短持有时间的原则,并在合适场景选择更合适的并发工具。

为了让您对 synchronized 如何保证线程安全有一个全面的认识,我准备了一张核心特性图:

synchronized 三大保证
原子性
确保操作不可中断
可见性
保证修改立即可见
有序性
防止重排序破坏逻辑
例如: count++
三个步骤作为一个整体执行
通过 unlock 前
强制刷主内存实现
通过 as-if-serial 语义
和内存屏障实现

上图是 synchronized 能够解决线程安全问题的三大理论基石。JVM 为了实现这些特性,在底层做了大量工作。
一、原子性 (Atomicity)
​​原子性​​ 是指一个或多个操作作为一个不可分割的整体执行。这些操作要么全部成功,要么全部不执行,不会出现中间状态

  • ​​为何需要原子性​​:看似简单的操作,如 count++,实际上由​​读取值、增加、写回值​​多个步骤组成。在没有同步的情况下,多个线程交错执行这些步骤会导致最终结果错误

  • ​​synchronized 如何保证​​:synchronized通过互斥锁确保同一时间​​只有一个线程​​能够执行同步代码块或方法。它将一系列操作“捆绑”成一个原子操作,从而避免了线程交错执行带来的问题

👁️ 二、可见性 (Visibility)
​​可见性​​ 是指当一个线程修改了共享变量的值,其他线程能够立即看到这个修改后的最新值

  • ​​为何需要可见性​​:由于每个线程都有自己的工作内存,它们可能会暂时将共享变量拷贝到本地操作。若一个线程修改了其工作内存中的副本而未及时写回主内存,其他线程就无法感知这一变化,从而操作过期的数据

​​- synchronized 如何保证​​:Java 内存模型为 synchronized规定了以下内存语义

线程在​​进入​​ synchronized代码块(monitorenter)时,会清空工作内存,从主内存中​​重新加载​​共享变量的最新值。

  • 线程在​​退出​​ synchronized代码块(monitorexit)时,会强制将工作内存中对共享变量所做的修改​​刷新到主内存​​。

  • 这一“​​取最新,写回主​​”的机制,保证了随后获得锁的线程能看到前一个线程所做的修改。

🔄 三、有序性 (Ordering)
​​有序性​​ 指的是程序执行的顺序按照代码的先后顺序执行

  • 为何需要有序性​​:编译器和处理器为了优化性能,可能会对指令进行​​重排序​​。在单线程下,这不会影响最终结果,但在多线程环境下,不恰当的重排序可能导致程序逻辑错误

  • ​​synchronized 如何保证​​:synchronized通过 ​​as-if-serial语义​​ 来保证有序性。即,不管编译器和 CPU 如何重排序,都必须保证在​​单线程​​情况下程序的结果是正确的

  • 由于 synchronized保证了同一时刻只有一个线程执行同步代码块,因此在这个代码块内部,线程看到的指令执行顺序总是符合 as-if-serial语义的,从而保证了正确性。需要注意的是,synchronized允许其内部代码发生重排序,但只要不影响单线程的执行结果即可

🧠 五、从 JVM 到 CPU:深入理解 synchronized 的底层实现

synchronized 的底层实现是 JVM 的 Monitor(监视器锁) 和 对象头(Object Header)。

1. 对象头(Object Header)与 Mark Word

在 HotSpot 虚拟机中,每个 Java 对象在内存中的布局都包含一个对象头。它就像是对象的“身份证”,包含了两类信息:
• Mark Word:存储对象自身的运行时数据,如哈希码、GC 分代年龄、锁状态标志、持有锁的线程 ID 等。

• Klass Pointer:指向对象的类元数据的指针,虚拟机通过它来确定这个对象是哪个类的实例。

synchronized 的锁信息就存储在 Mark Word 中。为了在极小的空间内存储这些信息,Mark Word 被设计成一个动态变化的数据结构,它会根据对象的状态(如是否被锁定、是否被偏向)复用相同的存储空间。

为了更直观地理解 Mark Word 在不同锁状态下的结构变化,可以参考下面的示意图:

Mark Word 64位
无锁状态
01
哈希码
分代年龄
0 | 01
偏向锁状态
01
线程ID | 时间戳
分代年龄 | 1 | 01
轻量级锁状态
00
指向栈中锁记录的指针
00
重量级锁状态
10
指向互斥量Monitor的指针
10
GC标记状态
11

11

2. Monitor 机制

你可以把 Monitor 理解为一个结构特殊的“房间”,它只允许一个线程进入。每个 Java 对象都关联着一个潜在的 Monitor。这个 Monitor 在底层(C++实现)主要包含以下部分:
• _owner:当前持有锁、正在房间内执行任务的线程。

• _EntryList:其他想进入房间的线程都在这里排队等待(状态为 BLOCKED)。

• _WaitSet:已经进入过房间的线程,因为某些条件不满足(调用了 wait() 方法),主动释放锁并在这里等待唤醒(状态为 WAITING)。

当一个线程想执行 synchronized 代码块时,它需要先拿到对应对象的 Monitor(成为 _owner)。如果 _owner 为空,它就成功成为所有者。如果 _owner 已有线程,它就进入 _EntryList 排队等待。

字节码层面,synchronized 代码块是通过 monitorenter 和 monitorexit 这一对指令来实现的。而 synchronized 方法则通过方法常量池中的 ACC_SYNCHRONIZED 标志来隐式实现。

⚡ 六、超越锁升级:JVM 的其它优化策略

除了锁升级,JVM 的即时编译器(JIT)还会在编译时进行一些非常聪明的锁优化。

1. 锁消除(Lock Elimination)

JIT 编译器会进行逃逸分析,如果它发现一个对象(如 StringBuffer)的生命周期不会“逃逸”出当前方法,即不可能被其他线程访问到,那么即使你写了 synchronized 代码,JVM 也会自动移除这些无意义的锁操作。

示例:

public String createString() {// sb 是局部变量,不会逃逸出方法,其他线程无法访问StringBuffer sb = new StringBuffer();sb.append("Hello"); // JVM 会消除这里的同步操作sb.append("World");return sb.toString();
}

2. 锁粗化(Lock Coarsening)

如果 JVM 检测到有一连串的操作都在反复对同一个对象加锁和解锁,即使是在循环中,它可能会将多个连续的同步块合并为一个更大的同步块,从而减少锁请求的次数。

示例:

// 优化前:在循环中反复加锁解锁,效率低下for (int i = 0; i < 1000; i++) {synchronized(lock) {doSomething();}
}// 优化后(JVM 可能做的锁粗化):只加锁一次
synchronized(lock) {for (int i = 0; i < 1000; i++) {doSomething();}
}

3. 自适应自旋(Adaptive Spinning)

在轻量级锁竞争时,线程会进行“自旋”(循环重试)。自适应自旋意味着 JVM 不再是固定自旋 10 次,而是根据上次自旋的成功情况来动态调整下次的自旋次数。如果上次自旋很快就成功拿到了锁,那么这次可能会允许它多自旋一会儿;如果很少成功,则可能直接省略自旋过程,避免浪费 CPU。

🔄 七、synchronized 与 ReentrantLock:如何选择?

ReentrantLock 是 java.util.concurrent.locks 包下的一个显式锁。它们都是可重入锁,但在功能上有区别:

特性维度synchronizedReentrantLock
实现机制JVM 内置关键字,自动管理锁的获取与释放 JDKAPI 实现,需要手动 lock() 和 unlock()
灵活性基本,非公平灵活,可指定公平/非公平锁,提供了丰富的 API
尝试获取锁阻塞式,无法中断tryLock() 可尝试非阻塞获取,lockInterruptibly() 可响应中断
条件变量单一等待队列(通过 wait()/notify() 操作整个 _WaitSet)可创建多个 Condition,实现更精细的线程等待与唤醒(如生产者-消费者模型)
性能Java 6 后优化得很好,在大部分常见场景下相差无几在极高竞争环境下可能略有优势

选型建议:
• 优先使用 synchronized:语法简洁,自动释放锁,不易出错,在大部分场景下性能足够好。这是默认选择。

• 考虑 ReentrantLock 当你需要:可中断的锁获取、超时获取锁、公平锁、或者多个条件变量的复杂同步需求时。

🏆 八、最佳实践与常见陷阱

  1. 锁对象的选择:
    ◦ 使用私有 final 对象:private final Object lock = new Object();。这可以防止锁对象被意外修改,也保证了封装性。

    ◦ 避免使用 String 常量或基本类型包装类:如 synchronized(“LOCK”) 或 synchronized(Integer(1))。因为它们可能被其他地方意外共享,导致意想不到的互斥和死锁。

  2. 保持同步块内代码简短:只锁必要的代码,尽快释放锁,减少线程争用时间。

  3. 警惕死锁:
    ◦ 死锁是多个线程互相等待对方持有的锁。

    ◦ 避免方案:按固定的全局顺序获取锁;使用 tryLock() 尝试获取锁。

  4. 明确锁的范围:
    ◦ 实例锁 (synchronized 实例方法/this):锁的是特定对象实例。

    ◦ 类锁 (synchronized 静态方法/X.class):锁的是整个类(Class 对象),所有实例都会受影响。

  5. 异常处理:确保即使在同步块中抛出异常,锁也能被释放。synchronized 会自动释放,但清理其他资源可能需要 finally 块。

💎 总结与核心思想

synchronized 的演进史,就是一部在线程安全和性能之间不断寻求平衡的历史。

• 用法:三种方式——实例方法、静态方法、代码块。优先使用同步代码块,指定专用锁对象。

• 原理:基于 JVM 的 Monitor 机制,通过对象头的 Mark Word 记录锁状态。

• 优化:JVM 会自动进行锁升级(无锁 → 偏向锁 → 轻量级锁 → 重量级锁)和锁消除/粗化,根据竞争激烈程度智能选择。

• 核心:编写代码时牢记减小锁粒度、缩短持有时间的原则。

最终建议:对于大多数业务场景,放心使用 synchronized。它的性能在现代 JVM 上已经非常优秀。只有在需要其不具备的高级特性时,才去考虑更复杂的ReentrantLock。


文章转载自:

http://VkZdKfM5.pwhjr.cn
http://yg1x3yfN.pwhjr.cn
http://H9htbCrW.pwhjr.cn
http://mYo6Iab6.pwhjr.cn
http://t4xDfZKK.pwhjr.cn
http://asPoEIVq.pwhjr.cn
http://V63HV0nN.pwhjr.cn
http://Af0QKVIl.pwhjr.cn
http://N0bb1Fmk.pwhjr.cn
http://DWNCkGmA.pwhjr.cn
http://0xU6NH65.pwhjr.cn
http://Mm03sjRG.pwhjr.cn
http://EJuqd7p5.pwhjr.cn
http://LyCr2EiR.pwhjr.cn
http://9yDvxaRO.pwhjr.cn
http://UXXsVidA.pwhjr.cn
http://3oUHbUdu.pwhjr.cn
http://EAdH9qJb.pwhjr.cn
http://eagvmCR9.pwhjr.cn
http://lmKOYTCN.pwhjr.cn
http://xpdLkF5B.pwhjr.cn
http://nrtn2vqK.pwhjr.cn
http://IqvHCN2f.pwhjr.cn
http://P7vcZ1mB.pwhjr.cn
http://ncOyzgwZ.pwhjr.cn
http://kXeRHXVQ.pwhjr.cn
http://WpSob4Dt.pwhjr.cn
http://OFqbresP.pwhjr.cn
http://hbXkcaQp.pwhjr.cn
http://84fU4Px1.pwhjr.cn
http://www.dtcms.com/a/378511.html

相关文章:

  • Cesium深入浅出之shadertoy篇
  • LoRaWAN网关支持双NS的场景有哪些?
  • BigVGAN:探索 NVIDIA 最新通用神经声码器的前沿
  • SpringTask和XXL-job概述
  • 软考系统架构设计师之软件维护篇
  • 从CTF题目深入变量覆盖漏洞:extract()与parse_str()的陷阱与防御
  • 第五章:Python 数据结构:列表、元组与字典(二)
  • Flow Matching Guide and Code(3)
  • 内存泄漏一些事
  • 嵌入式学习day47-硬件-imx6ul-LED、Beep
  • 【数据结构】队列详解
  • C++/QT
  • GPT 系列论文1-2 两阶段半监督 + zero-shot prompt
  • 昆山精密机械公司8个Solidworks共用一台服务器
  • MasterGo钢笔Pen
  • 【算法--链表】143.重排链表--通俗讲解
  • 数据库的回表
  • 《Learning Langchain》阅读笔记13-Agent(1):Agent Architecture
  • MySQL索引(二):覆盖索引、最左前缀原则与索引下推详解
  • 【WS63】星闪开发资源整理
  • 守住矿山 “生命线”!QB800系列在线绝缘监测在矿用提升机电传系统应用方案
  • Altium Designer(AD)原理图更新PCB后所有器件变绿解决方案
  • DIFY 项目中通过 Makefile 调用 Dockerfile 并使用 sudo make build-web 命令构建 web 镜像的方法和注意事项
  • 联合索引最左前缀原则原理索引下推
  • 平衡车 -- 速度环
  • BPE算法深度解析:从零到一构建语言模型的词元化引擎
  • DIPMARK:一种隐蔽、高效且具备鲁棒性的大语言模型水印技术
  • mysql多表联查
  • 审美积累 | 移动端仪表盘
  • 面阵结构光3D相机三维坐标计算