👋hi,我不是一名外包公司的员工,也不会偷吃茶水间的零食,我的梦想是能写高端CRUD
🔥 2025本人正在沉淀中... 博客更新速度++
👍 欢迎点赞、收藏、关注,跟上我的更新节奏
📚欢迎订阅专栏,专栏名《在2B工作中寻求并发是否搞错了什么》
前言
聪明的你通过上一篇的学习:【Java并发】【synchronized】适合初学者体质入门的synchronized
一定很想了解synchronized更多更多吧!没问题,主播来咯!3、2、1,上文章📖
sychronzied代码块原理
修饰代码块时
java
public void test(Object obj) {
synchronized (obj) {
System.out.println("Hello");
}
}
我们使用javap -c SynchronizedMethodTest.class
。我们可以看到,编译器在代码块入口插入monitorenter
,在正常退出和异常退出的路径各插一个monitorexit
,确保锁一定会被释放。这个monitorenter
和monitorexit
是干嘛的,不用着急,后面会介绍😄。
shell
0: aload_1 // 加载对象 obj 到操作数栈
1: dup // 复制栈顶值(obj 的引用)
2: astore_2 // 存储到局部变量表(保存锁对象)
3: monitorenter // 进入监视器(获取锁)
4: getstatic #2 // 获取 System.out 静态字段
7: ldc #3 // 加载字符串 "Hello"
9: invokevirtual #4 // 调用 println 方法
12: aload_2
13: monitorexit // 正常退出时释放锁
14: goto 22
17: astore_3 // 异常处理块
18: aload_2
19: monitorexit // 异常退出时释放锁
20: aload_3
21: athrow
22: return
synchronized修饰方法原理
修饰synchronized方法时
java
public class SynchronizedMethodTest {
static int cnt = 0;
public synchronized void cntUpdate() {
cnt++;
System.out.println("修改cnt后的数值是:" + cnt);
}
}
使用javap -v SynchronizedMethodTest.class
反编译后的结果,我们可以看到ACC_SYNCHRONIZED
标识。
shell
public synchronized void cntUpdate();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED // 重点是这个!!ACC_SYNCHRONIZED
Code:
stack=3, locals=1, args_size=1
0: getstatic #2 // Field cnt:I
3: iconst_1
4: iadd
...
monitorenter/monitorexit的作用
写到这里,你是否会好奇,monitorenter和monitorexit是来干嘛的?🤔,不要着急,下面马上开讲解
宝贝来看了,官网文档对monitorenter
的描述,不着急哈,马上用ai翻译下。
monitorenter
The objectref must be of type reference.
翻译 :对对象引用类型要求 objectref
必须是引用类型(即对象实例),不能是基本数据类型。所以,我们使用synchronized
都时候,里面要用对象类型,我们用了基本数据类型就会编译不通过。如下图所示(IDEA的提示):
Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:
● If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.
● If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
● If another thread already owns the monitor associated with objectref, the thread blocks until the monitor's entry count is zero, then tries again to gain ownership.
翻译 :监视器(Monitor)关联机制 每个Java对象都会关联一个监视器(Monitor),该监视器被锁定当且仅当它拥有所有者(即被某个线程持有)。当线程执行monitorenter
指令时,尝试获取与objectref
关联的监视器所有权:
- 首次获取锁 如果监视器的进入计数(
entry count
)为0,线程立即成为监视器所有者,并将计数设为1。 - 重入锁 如果线程已是监视器所有者(如
synchronized
代码块嵌套),则进入计数+1。 - 竞争锁失败 若其他线程持有锁,当前线程会阻塞,直到监视器的计数归零后重新尝试获取。
monitorexit
The objectref must be of type reference.
翻译:objectref必须是引用类型(即对象实例),与monitorenter要求一致。
The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.
翻译 :执行monitorexit的线程必须是该监视器(关联于objectref)的当前所有者。否则会抛出IllegalMonitorStateException
。
The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.
翻译:这里说的是锁计数器递减逻辑和唤醒竞争线程。
锁计数器递减逻辑:
- 线程将监视器的进入计数器(entry count)减1
- 完全释放锁:若计数器减至0,线程退出监视器并失去所有权
- 未完全释放:若计数器仍大于0(重入锁场景),线程仍保持所有权 唤醒竞争线程逻辑 :当计数器归零后,其他阻塞等待该监视器的线程会被唤醒并尝试获取锁。
ACC_SYNCHRONIZED的作用
这个就相当于隐式的synchronized
代码块了,底层还是monitorenter/monitorexit
那套。 JVM 会在方法调用前隐式调用 monitorenter
,在方法返回(包括正常返回和异常返回)时隐式调用 monitorexit
。
注意:ACC_SYNCHRONIZED 并不会在字节码中生成显式的 monitorenter 和 monitorexit 指令,而是由 JVM 在方法调用时自动处理锁操作。
下面的同步方法和同步代码块执行起来是一个效果。 如果是静态的同步方法,那锁就是这个类。
java
// 同步方法
public synchronized void syncMethod() {
System.out.println("Hello");
}
// 同步代码块
public void syncBlock() {
synchronized(this) {
System.out.println("Hello");
}
}
// 静态同步方法
public static synchronized void syncMethod() {
System.out.println("Hello");
}
// 同步代码块
public void syncBlock() {
synchronized(Syn.class) {
System.out.println("Hello");
}
}
并发三大特性synchronized保证原理
原子性
- 原子性的意思:一个操作是不可中断的。
- 保证原理 :通过
monitorenter
和monitorexit
来保证。 - 过程 :一个
线程a
执行monitorenter
后,会对monitor
进行加锁,加锁后其他线程无法获取锁,除非线程a
主动解锁。即使cpu时间片用完,线程a
放弃了cpu,但是线程a
没有释放锁,下次synchronzied
重入的时候,还是只能线程a继续执行。直到代码执行完成。monitorexit
释放锁。
有序性
- 有序性的意思:按照代码的顺序先后执行
- 保证原理 :
synchronized
代码块里是单线程的。虽然同步块内的代码可能被重排序,但由于同一时刻只有一个线程执行,其他线程无法观察到这种重排序,因此逻辑上等同于顺序执行。 - 与volatile的有序性不同 :
synchronized
保证是代码级别的有序,volatile
是指令级别的有序。
可见性
- 可见性的意思:多个线程访问一个共享变量时,一个线程修改了这个变量,其他线程能够立刻看到修改。
- 保证原理 :内存屏障 + JMM规则(
happens-before
)synchronized
修饰的代码块,在执行完释放锁后,JVM 会插入写屏障
(Store Barrier),强制将线程本地内存(如 CPU 缓存)中的修改刷新到主内存。- 在进入
synchronized
代码块时(获取锁时),JVM 插入读屏障
(Load Barrier),强制从主内存重新加载共享变量,丢弃本地缓存中的旧值。 - 对一个锁的解锁操作(unlock)必须发生在后续同一锁的加锁操作(lock)之前,保证修改对其他线程可见。
锁升级
好奇的你一定会问为什么要说锁升级呢?锁升级是什么?
为什么要优化?jdk1.6以前,是直接升级为重量级锁,然后对象头指向monitor。但是直接这样的话这个性能开销很大。
monitor监视器锁,依赖于底层操作系统的Mutex Lock来实现,而操作系统实现线程之间的切换时,需要从用户态转换到内核态,这个切态过程需要较长的时间,并且更方面成本较高,这也是早期的synchronized性能效率低的原因。
所以Java 中的锁升级机制是 JVM 为了优化 synchronized
关键字在 不同竞争场景下的性能 而设计的,通过动态调整锁状态(无锁 → 偏向锁 → 轻量级锁 → 重量级锁),在保证线程安全的前提下,尽可能减少锁操作的开销。 下图为优化后的结果:
对象头里MarkWord
jdk1.6之前,直接就是升级为重量级锁,所以没必要对对象头那么深入的了解。但是有了后续的锁优化之后,我们的对象头中的markword的事情可多了(对象头里面的markword记录锁的状,偏向锁、轻量级锁...),所以这里要多说下对象头,特别注意看markword。
对象头 = Mark Word + 类型指针 + [数组长度](可选)
Mark Word(标记字段):固定长度(32位系统4字节/64位系统8字节)
- 存储运行时数据:哈希码(Identity HashCode)、GC分代年龄(4bit)、锁状态标志(2bit)、线程持有的锁信息、偏向线程ID/epoch、是否可偏向(biased_lock)。
类型指针(Klass Pointer)
- 指向方法区中对象的类元数据
- 开启指针压缩(-UseCompressedOops)时为4字节,否则8字节
数组长度(仅数组对象):4字节存储数组长度
锁升级MarkWord的变化
锁升级流程和变化
前面铺垫了这么久,终于可以细说🔒锁升级流程了,我觉得这是核中核了。
流程其实很简单很快的概括,就是:
shell
无锁 → (单线程访问) → 偏向锁 → (竞争发生) → 轻量级锁 → (自旋失败) → 重量级锁
1.无锁状态(初始状态)
场景:对象刚被创建,未有任何线程竞争。
特点:所有线程均可直接访问同步块,无锁竞争。
2.偏向锁(Biased Lock)
核心优化 :消除无竞争时的同步开销。同一个线程进入同步代码块时,直接检查线程ID,发现
MarkWord
中的线程ID和当前一致。无需任何同步操作(如CAS、操作系统互拆),直接执行代码。
触发条件 :当一个线程进入synchronized
同步代码块时,JVM检查当前对象处于 无锁状态(锁标志为001
)。
流程:
- 检查对象是否可偏向 :JVM检查对象头
MarkWord
,锁标志为是否为001
?是否已偏向过?如果已经偏向过,会走偏向锁的撤销流程,升级为轻量级锁。(不要急,下面会细说偏向锁的撤销) - cas设置偏向锁 :如果对象从未被偏向,JVM执行CAS操作,讲当前线程ID 写入
MarkWord
并将锁标志从001
改为偏向模式101
。如果cas失败,说明有别的线程竞争,升级为轻量级锁。
偏向锁的延迟启用
JVM 默认在程序启动后 4秒(可通过 -XX:BiasedLockingStartupDelay
设置)才启用偏向锁。 目的:避免启动阶段因类加载、初始化等操作导致的频繁偏向锁撤销。
JDK 15 后默认禁用偏向锁(-XX:-UseBiasedLocking)
被移除的原因 :JEP 374: Deprecate and Disable Biased Locking
简单来说就是:偏向锁为整个「同步子系统」引入了大量的复杂度,并且这些复杂度也入侵到了 HotSpot 的其它组件。
3.轻量级锁(Lightweight Lock)
偏向锁升级为轻量级锁,又称为偏向锁的撤销(Revoke)。
触发条件 :JVM 检测到当前锁已偏向其他线程,触发
偏向锁撤销
,并升级为轻量级锁。
核心优化:通过CAS自旋避免线程阻塞。
流程:
- 暂停原持有线程:JVM出发安全点(Safe Point),暂停原持有线程(STW,stop the world,STW 会导致短暂停顿,高并发场景下频繁撤销偏向锁可能降低性能。),确保原持有线程的状态稳定。防止在撤销的过程中修改对象头或执行同步代码块。
- 检查原持有线程锁状态 : a. 原持有线程退出同步代码块:将对象头恢复为 无锁状态,允许其他线程重新竞争;其他线程可以参试CAS直接获取偏向锁。 b. 原持有线程任然在同步代码中:创建
Lock Record
在原持有线程栈帧中分配一个Lock Record
,;CAS更新对象头,将原对象头的Mark Word
复制到Lock Record
中(备份原状态);,并将锁标志位改为 00;最后唤醒原持有线程,继续执行同步代码块。其他线程,通过自旋(Spin)
尝试获取轻量级锁(CAS 替换对象头,对象头指向自己的Lock Record
)。
为什么需要Lock Record?
说人话就是,我们需要保留对象的原始信息。之前的对象头还要存hashcode、分代年龄啥的,升级为轻量级锁之后,这部分要被保存下来,方便后续锁释放时恢复。
当线程释放轻量级锁时,将 Lock Record
中保存的原始 Mark Word
写回对象头,恢复对象为无锁状态。
自旋优化
早期固定自旋(JDK 1.6 之前)
- 固定阈值:默认自旋 10 次(可通过 -XX:PreBlockSpin=10 设置)。
- 问题:无法适应不同场景(如短任务自旋浪费 CPU,长任务自旋不足)。
自适应自旋(JDK 1.6+ 默认)
- 动态调整:根据 上一次自旋的成功率 和 持有锁的线程状态 动态调整次数。
- 规则 :
- 若上次自旋成功获取锁,则允许更长的自旋(逐步增加次数)。
- 若自旋失败率高,则减少次数甚至直接跳过自旋。
- 优势:避免无意义的 CPU 空转,适应不同竞争强度。
4.重量级锁(Heavyweight Lock)
触发条件:线程自旋等待超过阈值,多个线程尝试获取锁,自旋等待期见锁未被释放。
核心机制:依赖操作系统互斥量(mutex)实现阻塞与唤醒。
流程:
- 自旋失败:自旋次数超过动态阈值,或新线程加入竞争。
- 创建重量级线锁:JVM为对象分配一个
Monitor
对象, - 修改对象头:
Mark Word
指向这个Monitor
对象;锁标志位从轻量级锁的00
改为重量级锁的10
。 - 阻塞竞争线程:所有未获取锁的线程进入
_EntryList
队列,由操作系统调度为 阻塞状态。线程从用户态切换到内核态,依赖操作系统的互斥锁(Mutex)实现阻塞。
锁的唤醒机质
当持有锁的线程退出同步块时:
- 将
_owner
字段置为null
。 - 从
_EntryList
中唤醒一个线程(synchronized强制非公平策略,无法调整)。
MarkWord角度观察锁升级
这部分得物的那篇写的很好,这里主播就直接大量参考了😎: 【得物技术】深入理解synchronzied底层原理
我们玩的就是真实,导入下面的pom.xml,跟上主播的节奏,直接看对象头😎。
xml
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
无锁状态
java
public static void main(String[] args) {
Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
markword
shell
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
这里详细说下意思,后续就不再详细说了。
列名 | 说明 | 示例对应值 |
---|---|---|
OFFSET | 内存偏移量(字节),表示该字段在对象内存中的起始位置 | 0 表示对象头从内存起始位置开始 , 8 表示类型指针从第8字节开始 |
SIZE | 占用内存大小(字节),表示该字段占用的内存空间 | 4 表示占用4字节(loss...) 中的4 表示对齐填充占4字节 |
TYPE | 字段类型(JOL输出中此列常为空)可能用于描述数据类型(如对象头、实例字段等) | - |
DESCRIPTION | 字段描述,说明该内存区域的用途 | (object header) 表示对象头(loss...) 表示对齐填充 |
VALUE | 内存中的实际值 ,十六进制字节序列 + 二进制位解释 + 十进制数值(括号内) | 01 00 00 00 表示小端存储的4字节(00000001 ...) 二进制展开(1) 十进制结果 |
不是主播偷懒哈,这后面的内容,其实得物写的就很好了,主播补充下这些基础知识,大伙去原文看就好了,讲的是非常的细了。
锁的其他优化
锁升级只是synchronzied优化的一小部分,下面还会说说别的优化点
锁消除(Lock Elimination):JVM检测到不存在共享数据竞争时(如局部变量),直接消除锁。
java
public void method() {
// 局部变量,不存在共享数据竞争
Object lock = new Object();
synchronized (lock) {
System.out.println("LockEliminationExample: This is a local lock.");
}
}
// JVM优化
public void method() {
System.out.println("LockEliminationExample: This is a local lock.");
}
锁粗化(Lock Coarsening):对连续加锁/解锁操作合并为一次,减少CAS开销。
java
public void method() {
synchronized (lock) {
System.out.println("Operation 1");
}
synchronized (lock) {
System.out.println("Operation 2");
}
synchronized (lock) {
System.out.println("Operation 3");
}
}
// JVM优化
public void method() {
synchronized (lock) {
System.out.println("Operation 1");
System.out.println("Operation 2");
System.out.println("Operation 3");
}
}
Monitor
聊了这么半天了,这个monitor是啥呢?诶嘿嘿,monitor来咯!✋😄✋
当你使用wait()
、notify()
、notifyAll()
的时候,你是否会好奇?为啥会报错IllegalMonitorStateException
,这个monitor
状态是啥?比如下面的代码报错。
java
public static void main(String[] args) throws InterruptedException {
Object o = new Object();
o.wait();
}
Exception in thread "main" java.lang.IllegalMonitorStateException
at java.lang.Object.wait(Native Method)
at java.lang.Object.wait(Object.java:502)
为什么需要放在synchronzied
的代码块里面呢?
java
public static void main(String[] args) throws InterruptedException {
Object o = new Object();
synchronized (o) {
o.wait();
}
}
还有前面说的,升级为重量级锁之后,对象头markword
指向monitor
,这个monitor是个啥呢?不要着急,跟上主播的节奏,扣111,马上开始进入神奇的monitor。
Monitor介绍
Monitor (监视器)并不是直接存储在对象头(Object Header
)中,但对象头中的信息会指向 Monitor。具体来说,当对象被用作锁时,JVM 会根据锁状态(无锁、偏向锁、轻量级锁、重量级锁)在对象头中存储不同的信息。 只有在 重量级锁 状态下,对象头中的指针才会指向一个独立的 Monitor 对象(位于堆内存中)。 我们直接深入到源码,Monitor的具体实现在 objectMonitor.hpp 和 objectMonitor.cpp 中:
cpp
//hotspot/src/share/vm/runtime/objectMonitor.hpp
class ObjectMonitor {
// 核心字段
volatile markOop _header; // 对象头(存储锁状态)
void* volatile _owner; // 持有锁的线程
volatile intptr_t _recursions; // 重入次数
ObjectWaiter* volatile _WaitSet; // 等待队列(调用 wait() 的线程)
ObjectWaiter* volatile _EntryList;// 阻塞队列(竞争锁失败的线程)
volatile intptr_t _count; // 锁计数器(重入时递增)
volatile intptr_t _waiters; // 等待的线程数
ObjectWaiter* volatile _cxq; // 竞争锁的临时队列(内部优化)
// ... 其他字段(如自旋锁、等待策略等)
};
我们重点下面这些就行了:
- _owner:持有锁的线程指针(如0x00007f4b3800e8d0)
- _recursions :记录锁重入次数(
synchronized
嵌套时递增) - _cxq:竞争锁的临时队列(JVM 内部优化用,某些情况下线程会先进入这里)。
- _EntryList:线程因竞争锁失败而进入,等待锁释放后被唤醒。
- _WaitSet :线程主动调用
wait()
释放锁后进入,等待其他线程调用notify()/notifyAll()
唤醒,唤醒后移入Entry Set
重新竞争锁
线程抢monitor流程
在变为重量级锁之前(偏向锁/轻量级锁)不涉及monitor竞争,偏向锁仅通过cas记录线程id,轻量级锁通过用户态的cas自旋。但是咱们这里是说重量级锁的,所以必须来说说这个重量级锁的monitor抢夺😄 🏀题外话:这个_cxq队列,从学生时代就让我想到了一位爱穿背带裤,唱、跳、RAP的明星。
假设 线程A 已持有锁,线程B 和 线程C 尝试获取锁:
步骤 1:线程B尝试获取锁
检查 _owner:
- 如果 _owner 为 NULL(锁未被持有),线程B通过 CAS 操作 将 _owner 设为自己,获取锁成功。
- 如果 _owner 是线程B自己(可重入),计数器递增。
- 如果锁已被线程A持有,线程B进入竞争流程。
步骤 2:线程B竞争失败,进入队列
进入 _cxq 队列 :线程B被封装成一个节点,插入到_cxq
队列的头部。线程B调用 park()
挂起自己,等待唤醒。
步骤 3:线程C竞争失败,同样进入队列
_cxq 队列结构: 线程C的节点插入到 _cxq 头部,队列变为:_cxq -> 线程C -> 线程B。
步骤 4:线程A释放锁
-
清空 _owner:
- 线程A将
_owner
设为NULL
,表示释放锁。
- 线程A将
-
迁移队列:
- 如果
_EntryList
为空,将_cxq
中的线程(线程C、线程B)迁移到_EntryList
。
- 如果
-
唤醒线程:
- 从
_EntryList
头部(线程C)唤醒,线程C尝试通过 CAS 获取锁。
- 从
步骤 5:线程C获取锁成功
- 线程C通过 CAS 将
_owner
设为自己,开始执行同步代码。 - 线程B仍在
_EntryList
中等待下次唤醒。
_WaitSet 相关
等待与唤醒(wait/notify)
- 调用
wait()
:- 线程释放锁,进入 _WaitSet 队列。
- 线程挂起,等待其他线程调用 notify()。
- 调用
notify()
:- 从 _WaitSet 中移出一个线程,放入 _EntryList 或 _cxq。
- 被移出的线程会在下次锁释放时参与竞争。
后话
没想到,这个synchronzied和volatile一样难搞,居然这么多细节,校招的时候,都顾着赶路了,背个锁升级,就是印象深刻了。毕业后再来学习,发现这里居然还有这么多内容,我以前还是太狂妄了。
其实还有很多内容还是可以补充的,也欢迎各位大佬指出我的不足🙇♂️🙇♂️🙇♂️
参考
Java对象头学习指南------实战篇 - 极客子羽 - 博客园
Synchronization (The Java™ Tutorials > Essential Java Classes > Concurrency)