synchronized
:俗称对象锁,它采用互斥的方式让同一时刻至多只有一个线程能持有对象锁,其它线程再想获取这个对象锁时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换
synchronized
实际是用对象锁保证了临界区内代码的==原子性==,临界区内的代码对外是不可分割的,不会被线程切换所打断。
整体锁状态升级流程如下:
-
image.png
Java的对象头
在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
HotSpot虚拟机对象的对象头部分包括两类信息:
第一类是用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,称它 为“Mark Word”,Mark Word被设计成一个有着动态定义的数据结构,以便在极小的空间内存储尽量多的数据,根据对象的状态复用自己的存储空间。
对象头的另外一部分是类型指针,即对象指向它的类型元数据的指针,Java虚拟机通过这个指针
来确定该对象是哪个类的实例。此外,如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据
以32位虚拟机为例:
-
对象头格式
-
数组对象头格式
-
Mark Word格式
- 无锁状态
- 偏向锁状态
- 轻量级锁状态
- 重量级锁状态
- GC标记
- 64位虚拟机下
-
image.png
Monitor
Monitor
被翻译为监视器或管程每个 Java 对象都可以关联一个
Monitor
对象,如果使用synchronized
给对象上锁(重量级)之后,该对象头的Mark Word
中就被设置指向Monitor
对象的指针
在Java虚拟机(HotSpot)中,monitor
是由ObjectMonitor
实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.cpp
文件,C++实现的)
ObjectMonitor() {
_header = NULL;
_count = 0;
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL; //持有者
_WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
Monitor简图:
-
Monitor
- 临界区代码
synchronized(obj){
.......
}
具体流程:
- 最开始
WaitSet
、EntryList
、Owner
都是NULL
- 假设有
Thread-2
想要执行上面代码,回去检查obj
对象是否与monitor
有关联,发现没有,就会将Monitor
的所有者Owner
置为Thread-2
,Monitor
中只能有一个Owner
- 如果
Thread-3
,Thread-4
,Thread-5
也来执行synchronized(obj),依旧先检查obj
是否关联Monitor
,发现关联了然后检查看看Monitor
的owner
是否有关联,有就会进入EntryList
阻塞 -
Thread-2
执行完同步代码块的内容,然后唤醒EntryList
中等待的线程来竞争锁 ,执行哪一个线程由 CPU 来调度 - 图中
WaitSet
中的Thread-0
,Thread-1
是之前获得过锁,但条件不满足进入WAITING
状态的线程
synchronized 优化原理
synchronized
的实现依赖于与某个对象向关联的monitor
实现,而monitor
是基于底层操作系统的Mutex Lock
实现的,而基于Mutex Lock
实现的同步必须经历从用户态到核心态的转换,这个开销特别大,成本非常高。所以频繁的通过synchronized
实现同步会严重影响到程序效率,而这种依赖于Mutex Lock
实现的锁机制也被称为重量级锁,为了减少重量级锁带来的性能开销,JDK对synchronized
进行了种种优化。
1. 轻量级锁
轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。
轻量级锁对使用者是透明的,即语法仍然是synchronized
static final Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块 A
method2();
}
}
public static void method2() {
synchronized( obj ) {
// 同步块 B
}
}
-
在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”)创建锁记录(
Lock Record
)对象,每个线程的栈帧都会包含一个锁记录的结构(不可见),内部可以存储锁定对象的Mark Word
,然后拷贝对象头中的Mark Word
复制到锁记录中。
image.png
-
拷贝成功后,
JVM
将使用 cas 操作尝试将对象的Mark Word
更新为指向Lock Record
的指针,并让锁记录中Object reference
指向锁对象
image.png
- 如果 cas 替换成功,表示由该线程给对象加锁,并且对象
Mark Word
的锁标志位设置为“00”,表示此对象处于轻量级锁定状态。如下图
image.png
- 如果 cas 替换成功,表示由该线程给对象加锁,并且对象
- 如果 cas 失败,有两种情况
- 如果是其它线程已经持有了该
Object
的轻量级锁,这时表明有竞争,进入锁膨胀过程 - 如果是同一线程执行了
synchronized
操作, 锁重入,那么再添加一条Lock Record
作为重入的计数(这种情况建立在是可重入锁的情况)- 按照我们上面的代码,调用
method2()
会再一次进行加锁操作,此时 cas 操作肯定会失败,因为object
里面已经是轻量级锁状态,但是通过object
里面的lock record
地址,会发现是同一个线程,此时操作如下图,锁记录为null
说明存在锁重入,然后继续执行后面代码,有几个null值重入几次,
image.png
- 按照我们上面的代码,调用
- 如果是其它线程已经持有了该
- 轻量级锁解锁
- 当退出
synchronized
代码块(解锁时)如果有取值为null
的锁记录,表示有重入,这时重置锁记录,表示重入计数减一 - 当退出
synchronized
代码块(解锁时)锁记录的值不为 null,这时使用 cas 将Mark Word
的值恢复给对象
头- 成功,则解锁成功
- 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
2. 锁膨胀
如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
![](https://img.haomeiwen.com/i2487049/2815458945a9b442.png)
-
Thread-1
加轻量级锁失败,进入锁膨胀流程- 为
Object
对象申请Monitor
锁,让Object
指向重量级锁地址 - 自己进入
Monitor
的EntryList BLOCKED
-
image.png
- 当
Thread-0
退出同步块解锁时,使用 cas 将Mark Word
的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照Monitor
地址找到Monitor
对象,设置Owner
为null
,唤醒EntryList
中BLOCKED
线程
- 为
3. 自旋优化
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞,一般默认是10次
自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
4. 偏向锁
轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。
Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的Mark Word
头,之后发现
这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有
以上基本来源于B站学习并发编程的笔记地址
网友评论