Java3yのsynchronized
一、 Monitor 概念
1、 Java 对象头 (重点)
(重点)
对象头
对象头包含两部分:
运行时元数据(Mark Word)
和
类型指针 (Klass Word)
-
运行时元数据
-
,可以看作是堆中对象的地址哈希值(HashCode)
-
(用于新生代from/to区晋升老年代的标准, 阈值为15)GC分代年龄(年龄计数器)
- 锁状态标志 (用于JDK1.6对synchronized的优化 -> 轻量级锁)
- 线程持有的锁
- 偏向线程ID (用于JDK1.6对synchronized的优化 -> 偏向锁)
- 偏向时间戳
-
-
类型指针
- 指向
。指向的其实是方法区中存放的类元信息类元数据InstanceKlass,确定该对象所属的类型
- 指向
说明:如果对象是数组,还需要记录数组的长度
- 以 32 位虚拟机为例,普通对象的对象头结构如下,其中的
为Klass Word
,指向类型指针
对应的方法区
;Class对象

- 数组对象
Java并发编程(三) : synchronized底层原理、优化Monitor重量级锁、轻量级锁、自旋锁(优化重量级锁竞争)、偏向锁一、 Monitor 概念 - 其中 Mark Word 结构为:
无锁(001)、偏向锁(101)、轻量级锁(00)、重量级锁(10)
Java并发编程(三) : synchronized底层原理、优化Monitor重量级锁、轻量级锁、自旋锁(优化重量级锁竞争)、偏向锁一、 Monitor 概念 - 所以一个对象的结构如下:
Java并发编程(三) : synchronized底层原理、优化Monitor重量级锁、轻量级锁、自旋锁(优化重量级锁竞争)、偏向锁一、 Monitor 概念
2、 Monitor 原理 (Synchronized底层实现-重量级锁)
多线程同时访问临界区: 使用重量级锁
- JDK6对Synchronized的优先状态:
偏向锁–>轻量级锁–>重量级锁
-
被翻译为Monitor
或者监视器
管程
每个Java对象
都可以
关联一个(操作系统的)Monitor
,如果使用
synchronized
给
对象上锁(重量级)
,该
对象头的MarkWord
中就被设置为
指向Monitor对象
的
指针
下图原理解释:
- 当
访问到
Thread1
中的
synchronized(obj)
的时候
共享资源
- 首先会将synchronized中的
中
锁对象
的
对象头
去尝试指向
MarkWord
的
操作系统
对象. 让锁对象中的MarkWord和Monitor对象相关联. 如果关联成功, 将obj对象头中的
Monitor
的
MarkWord
从01改为10。
对象状态
- 因为Monitor没有和其他的obj的MarkWord相关联, 所以
就成为了该
Thread1
的Owner(所有者)。
Monitor
- 又来了个
执行synchronized(obj)代码, 它首先会看看能不能执行该
Thread1
的代码; 它会检查obj是否关联了Montior, 此时已经有关联了, 它就会去看看该Montior有没有所有者(Owner), 发现有所有者了(Thread2);
临界区
也会和该Monitor关联, 该线程就会进入到它的
Thread1
;
EntryList(阻塞队列)
- 当
执行完
Thread2
代码后, Monitor的
临界区
就空出来了. 此时就会
Owner(所有者)
Monitor中的EntryList阻塞队列中的线程, 这些线程通过
通知
, 成为新的
竞争
所有者
![]()
Java并发编程(三) : synchronized底层原理、优化Monitor重量级锁、轻量级锁、自旋锁(优化重量级锁竞争)、偏向锁一、 Monitor 概念
- 刚开始时
中的Monitor
Owner为null
- 当Thread-2 执行synchronized(obj){}代码时就会将Monitor的所有者Owner 设置为 Thread-2,上锁成功,Monitor中同一时刻只能有一个Owner
- 当Thread-2 占据锁时,如果线程Thread-3,Thread-4也来执行synchronized(obj){}代码,就会进入
中变成EntryList
BLOCKED状态
- Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,
竞争时是非公平的 (仍然是抢占式)
-
图中 WaitSet 中的Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程,后面讲wait-notify 时会分析
注意:
- synchronized 必须是进入同一个锁对象的monitor 才有上述的效果; —> 也就要使用同一把锁
- 不加 synchronized的锁对象不会关联监视器,不遵从以上规则
![]()
Java并发编程(三) : synchronized底层原理、优化Monitor重量级锁、轻量级锁、自旋锁(优化重量级锁竞争)、偏向锁一、 Monitor 概念
它加锁就是依赖底层操作系统的相关指令实现, 所以会造成
mutex
, 非常耗性能 !
用户态和内核态之间的切换
- 在JDK6的时候, 对synchronized进行了优化, 引入了
, 它们是在JVM的层面上进行加锁逻辑, 就没有了切换的消耗~
轻量级锁, 偏向锁
3、synchronized原理
代码如下
static final Object lock = new Object();
static int counter = 0;
public static void main(String[] args) {
synchronized (lock) {
counter++;
}
}
- 反编译后的部分字节码
Java并发编程(三) : synchronized底层原理、优化Monitor重量级锁、轻量级锁、自旋锁(优化重量级锁竞争)、偏向锁一、 Monitor 概念
注意:方法级别的 synchronized 不会在字节码指令中有所体现
4、synchronized 原理进阶
- 小故事
5、轻量级锁 (用于优化Monitor这类的重量级锁)
通过 锁记录
的方式, 场景 : 多个线程交替进入临界区
-
: 如果一个对象虽然有多个线程要对它进行加锁,但是加锁的时间是错开的(也就是没有人可以竞争的),那么可以使用轻量级锁的使用场景
。轻量级锁来进行优化
- 轻量级锁对使用者是透明的,即语法仍然是
(jdk6对synchronized的优化),假设有两个方法同步块,利用同一个对象加锁synchronized
-
线程A来操作临界区的资源,eg:
的过程, 没有线程来竞争, 此时就可以使用给资源加锁,到执行完临界区代码,释放锁
; 如果轻量级锁
有线程来竞争的话, 就会这期间
升级为重量级锁(synchronized)
static final Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块 A
method2();
}
}
public static void method2() {
synchronized( obj ) {
// 同步块 B
}
}
- 每次指向到
时,都会在synchronized代码块
创建栈帧中
,锁记录(Lock Record)对象
,锁记录内部可以储存每个线程都会包括一个锁记录的结构
和对象的MarkWord
锁对象引用reference
Java并发编程(三) : synchronized底层原理、优化Monitor重量级锁、轻量级锁、自旋锁(优化重量级锁竞争)、偏向锁一、 Monitor 概念 Java并发编程(三) : synchronized底层原理、优化Monitor重量级锁、轻量级锁、自旋锁(优化重量级锁竞争)、偏向锁一、 Monitor 概念
- 让
中的锁记录
,并且尝试用Object reference指向锁对象地址
将CAS(compare and sweep)
替换栈帧中的锁记录的(lock record 地址 00)
,将Mark Word 的值(01)存入锁记录(lock record地址)中 ------Object对象的Mark Word
相互替换
- 01 表示
无锁
(看Mark Word结构, 数字的含义)
- 00表示
轻量级锁
Java并发编程(三) : synchronized底层原理、优化Monitor重量级锁、轻量级锁、自旋锁(优化重量级锁竞争)、偏向锁一、 Monitor 概念
- 01 表示
重点:
- 如果
, 获得了轻量级锁,那么cas替换成功
的对象
,如下所示对象头储存的就是锁记录的地址和状态00
- 线程中锁记录, 记录了锁对象的锁状态标志; 锁对象的对象头中存储了锁记录的地址和状态, 标志哪个线程获得了锁
- 此时
中存储了栈帧
中的对象的对象头
,年龄计数器,哈希值等;锁状态标志
就存储了对象的对象头中
, 这样的话栈帧中锁记录的地址和状态00
就知道了是对象
。哪个线程锁住自己
Java并发编程(三) : synchronized底层原理、优化Monitor重量级锁、轻量级锁、自旋锁(优化重量级锁竞争)、偏向锁一、 Monitor 概念
- 如果
: ① 锁膨胀 ② 重入锁失败cas替换失败,有两种情况
- 1、如果是
已经持有了其它线程
,那么表示有竞争,将进入该Object的轻量级锁
锁膨胀阶段
- 此时
对象头中已经存储了别的线程的对象Object
,指向了其他线程;锁记录地址 00
- 此时
- 2、如果是
,那么自己的线程已经执行了synchronized进行加锁
– 线程多次加锁, 锁重入再添加一条 Lock Record 作为重入锁的计数
- 在上面代码中,
又调用了临界区中
, method2中又进行了一次method2
, 此时就会在synchronized加锁操作
中再开辟一个method2方法对应的栈帧(栈顶), 该栈帧中又会存在一个虚拟机栈
的独立
, 此时它发现Lock Record
; 加锁也就失败了. 这种现象就叫做对象的对象头中指向的就是自己线程中栈帧的锁记录
; 线程中有多少个锁记录, 就能表明该线程对这个对象加了几次锁 (锁重入计数)锁重入
Java并发编程(三) : synchronized底层原理、优化Monitor重量级锁、轻量级锁、自旋锁(优化重量级锁竞争)、偏向锁一、 Monitor 概念
- 在上面代码中,
- 1、如果是
轻量级锁解锁流程 :
- 当
的时候,如果获取的是线程退出synchronized代码块
,表示有取值为 null 的锁记录
,这时重置锁记录,锁重入
表示重入计数减一
Java并发编程(三) : synchronized底层原理、优化Monitor重量级锁、轻量级锁、自旋锁(优化重量级锁竞争)、偏向锁一、 Monitor 概念 - 当线程退出synchronized代码块的时候,如果
,那么使用cas将Mark Word的值恢复给对象, 将直接替换的内容还原。获取的锁记录取值不为 null
- 成功则解锁成功 (轻量级锁解锁成功)
- 失败,表示有竞争, 则
或说明轻量级锁进行了锁膨胀
,进入重量级锁解锁流程 (Monitor流程)已经升级为重量级锁
6、锁膨胀
- 如果在尝试
的过程中,加轻量级锁
,这是有一种情况就是其它线程已经为这个对象加上了轻量级锁,这是就要进行cas替换操作无法成功
,锁膨胀(有竞争)
将轻量级锁变成重量级锁。
- 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁, 此时发生
锁膨胀
Java并发编程(三) : synchronized底层原理、优化Monitor重量级锁、轻量级锁、自旋锁(优化重量级锁竞争)、偏向锁一、 Monitor 概念 - 这时Thread-1加轻量级锁失败,进入锁膨胀流程
- 因为
线程加轻量级锁失败, 轻量级锁没有阻塞队列的概念, 所以此时就要Thread-1
,让为对象申请Monitor锁(重量级锁)
,然后Object指向重量级锁地址 10
自己进入Monitor 的EntryList 变成BLOCKED状态
- 因为
- 当
执行完Thread-0 线程
时,使用cas将Mark Word的值恢复给对象头, 肯定恢复失败,因为对象的对象头中存储的是synchronized同步块
之前的是00, 肯定恢复失败。那么会重量级锁的地址,状态变为10了
,即按照进入重量级锁的解锁过程
Monitor的地址找到Monitor对象,将Owner设置为null,唤醒EntryList中的Thread-1线程
7、自旋锁优化 (优化重量级锁竞争)
- 当发生
,还可以使用重量级锁竞争的时候
,如果当前线程自旋成功(即在自旋的时候持锁的线程释放了锁),那么当前线程就可以不用进行上下文切换自旋来进行优化 (不加入Monitor的阻塞队列EntryList中)
就获得了(持锁线程执行完synchronized同步块后,释放锁,Owner为空,唤醒阻塞队列来竞争,胜出的线程得到cpu执行权的过程)
锁
- 优化的点: 不用将
加入到阻塞队列, 减少cpu切换.线程
-
自旋重试成功的情况
Java并发编程(三) : synchronized底层原理、优化Monitor重量级锁、轻量级锁、自旋锁(优化重量级锁竞争)、偏向锁一、 Monitor 概念 -
,自旋了一定次数还是没有等到 持锁的线程释放锁, 线程2就会加入Monitor的阻塞队列(EntryList)自旋重试失败的情况
Java并发编程(三) : synchronized底层原理、优化Monitor重量级锁、轻量级锁、自旋锁(优化重量级锁竞争)、偏向锁一、 Monitor 概念
- 自旋会
,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。占用 CPU 时间
- 在
的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。Java 7 之后不能控制是否开启自旋功能Java 6 之后自旋锁是自适应
8、偏向锁 (biased lock) (用于优化轻量级锁重入)
场景: 没有竞争的时候, 一个线程中多次使用需要重入加锁的情况; (只有一个线程进入临界区)
synchronized
- 在经常需要竞争的情况下就不使用偏向锁, 因为偏向锁是默认开启的, 我们可以通过JVM的配置, 将偏向锁给关闭
- 将进入临界区的线程的ID, 直接设置给锁对象的Mark word, 下次该线程又获取锁, 发现线程ID是自己, 就不需要CAS了
- 在
中,我们可以发现,如果同一个线程对同一个对象进行轻量级的锁
时,也需要执行CAS替换操作,这是有点耗时。重入锁
- 那么java6开始引入了
,将进入临界区的线程的ID, 直接设置给锁对象的Mark word, 下次该线程又获取锁, 发现线程ID是自己, 就不需要CAS了偏向锁
- 升级为轻量级锁的情况 (会进行偏向锁撤销) : 获取偏向锁的时候, 发现线程ID不是自己的, 此时通过CAS替换操作, 操作成功了, 此时该线程就获得了锁对象。(
)此时是交替访问临界区, 撤销偏向锁, 升级为轻量级锁
- 升级为重量级锁的情况 (会进行偏向锁撤销) : 获取偏向锁的时候, 发现线程ID不是自己的, 此时通过CAS替换操作, 操作失败了, 此时说明发生了锁竞争。(
)此时是多线程访问临界区, 撤销偏向锁, 升级为重量级锁
- 升级为轻量级锁的情况 (会进行偏向锁撤销) : 获取偏向锁的时候, 发现线程ID不是自己的, 此时通过CAS替换操作, 操作成功了, 此时该线程就获得了锁对象。(
例如:
public class Test {
static final Object obj = new Object();
public static void m1() {
synchronized (obj) {
// 同步块A
m2();
}
}
public static void m2() {
synchronized (obj) {
// 同步块B
m3();
}
}
public static void m3() {
synchronized (obj) {
// 同步块C
}
}
}
8.1、偏向锁状态 (了解)
-
的结构如下:运行时元数据(Mark Word)
Java并发编程(三) : synchronized底层原理、优化Monitor重量级锁、轻量级锁、自旋锁(优化重量级锁竞争)、偏向锁一、 Monitor 概念
-
Normal:一般状态,没有加任何锁
,前面62位保存的是对象的信息,最后2位为状态(01),倒数第三位表示是否使用偏向锁(未使用:0)
-
Biased:偏向状态,使用偏向锁
,前面54位保存的当前线程的ID,最后2位为状态(01),倒数第三位表示是否使用偏向锁(使用:1)
-
Lightweight:使用轻量级锁
,前62位保存的是锁记录的指针,最后2位为状态(00)
-
Heavyweight:使用重量级锁
,前62位保存的是Monitor的地址指针,最后2位为状态(10)
- 如果开启了偏向锁(默认开启),在创建对象时,对象的Mark Word后三位应该是101
- 但是偏向锁默认是有延迟的,不会再程序一启动就生效,而是会在程序运行一段时间(几秒之后),才会对创建的对象设置为偏向状态
- 如果没有开启偏向锁,对象的Mark Word后三位应该是001
一个对象的创建过程
- 如果开启了
偏向锁(默认是开启的)
,那么对象刚创建之后,Mark Word 最后三位的值101,并且这是它的
ThreadId
,
epoch
,
age(年龄计数器)
都是
,在加锁的时候进行设置这些的值.
-
偏向锁默认是延迟
的,不会在程序启动的时候立刻生效,如果想避免延迟,可以添加虚拟机参数来禁用延迟:
-XX:BiasedLockingStartupDelay=0
来禁用延迟
- 注意 :
处于偏向锁的对象解锁后,线程id仍存储于对象头中
输出结果:
- 测试
:如果禁用偏向锁
,那么对象创建后最后三位的值为没有开启偏向锁
,这时候它的hashcode,age都为0,hashcode是第一次用到001
时才赋值的。在上面测试代码运行时在添加 VM 参数hashcode
禁用偏向锁-XX:-UseBiasedLocking
,退出(禁用偏向锁则优先使用轻量级锁)
状态变回 001synchronized
- 禁止偏向锁, 虚拟机参数
; 优先使用-XX:-UseBiasedLocking
轻量级锁
- 输出结果: 最开始状态为001,然后加轻量级锁变成00,最后恢复成001
- 禁止偏向锁, 虚拟机参数
8.2、撤销偏向锁-hashcode方法 (了解)
- 测试
:当hashCode
的时候就会调用对象的hashcode方法
,因为使用偏向锁时没有位置存撤销这个对象的偏向锁
的值了hashcode
8.3、撤销偏向锁-发生锁竞争 (升级为重量级锁)
小故事: 线程A门上刻了名字, 但此时线程B也要来使用房间了, 所以要将偏向锁升级为轻量级锁. (线程B要在线程A使用完房间之后,再来使用; 否则就成了竞争获取锁对象, 此时就要升级为
(执行完synchronized代码块)
了)
重量级锁
偏向锁、轻量级锁的使用条件, 都是在于多个线程没有对同一个对象进行的前提下, 如果有
锁竞争
,此时就使用重量级锁。
锁竞争
- 这里我们演示的是
撤销, 变成偏向锁
的过程,那么就得满足轻量级锁的使用条件,就是没有线程对同一个对象进行轻量级锁
,我们使用锁竞争
和wait
来辅助实现notify
- 虚拟机参数
确保我们的程序最开始使用了-XX:BiasedLockingStartupDelay=0
偏向锁
- 输出结果,最开始使用的是
,但是第二个线程尝试获取对象锁时(前提是: 线程一已经释放掉锁了,也就是执行完synchroized代码块),发现本来对象偏向锁
,那么偏向锁就会失效,加的就是偏向的是线程一
轻量级锁
Java并发编程(三) : synchronized底层原理、优化Monitor重量级锁、轻量级锁、自旋锁(优化重量级锁竞争)、偏向锁一、 Monitor 概念
8.4、撤销偏向锁 - 调用 wait/notify (只有重量级锁才支持这两个方法)
(调用wait方法会导致锁膨胀而使用重量级锁)
- 会使对象锁变成重量级锁,因为
wait/notify方法之后重量级锁才支持
9、批量重偏向
- 如果
, 这时对象被多个线程访问,但是没有竞争 (上面撤销偏向锁就是这种情况: 一个线程执行完, 另一个线程再来执行, 没有竞争)
偏向T1的对象仍有机会重新偏向T2
- 重偏向会重置Thread ID
- 当
超过撤销偏向锁101 升级为 轻量级锁00
,JVM会觉得是不是偏向错了,这时会在20次后(超过阈值)
给对象加锁时,重新偏向至加锁线程 (T2)。
9.1、批量撤销偏向锁
- 当 撤销偏向锁的阈值超过40以后 ,就会将整个类的对象都改为不可偏向的
9.2、同步省略 (锁消除)
同步省略
- 线程同步的代价是相当高的,同步的后果是
。降低并发性和性能
- 在动态编译同步块的时候,
可以JIT编译器
来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程。借助逃逸分析
- 如果没有,
这个取消同步的过程就叫同步省略,也叫锁消除。那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步。这样就能大大提高并发性和性能。
- 例如下面的智障代码,
根本起不到锁的作用
public void f() {
Object hellis = new Object();
synchronized(hellis) {
System.out.println(hellis);
}
}
- 代码中对hellis这个对象加锁,但是hellis对象的生命周期只在f()方法中,并不会被其他线程所访问到,所以在JIT编译阶段就会被优化掉,优化成:
public void f() {
Object hellis = new Object();
System.out.println(hellis);
}
字节码分析
- 代码
public void f() {
Object hellis = new Object();
synchronized(hellis) {
System.out.println(hellis);
}
}
- 注意:字节码文件中并没有进行优化,可以看到加锁和释放锁的操作依然存在,
同步省略操作是在解释运行时发生的