synchronized底层如何实现,锁的升级、降级及其他锁
- synchronized是java实现同步的关键字,使用monitorenter/monitorexit指令实现;现代JVM提供了三种不同的monitor实现,即:偏斜锁、轻量级锁、重量级锁;在JVM检测到不同的竞争状态时会切换到不同的锁实现,这种优化synchronized运行的过程就叫做锁的升级、降级;
- 当没有竞争出现时,默认使用偏斜锁,JVM使用CAS操作在对象头的MarkWord部分设置线程ID,表示当前对象偏向某个线程;并不涉及真正的互斥锁;这样做的原因在于,在很多场景中,大部分对象生命周期内最多会被一个线程锁定,使用偏斜锁可以降低无竞争开销;
- 如果有别的线程试图获取某一个已经偏斜的对象,JVM就需要撤销偏斜锁,并切换到轻量级锁实现。轻量级锁同样使用CAS操作对象头的MarkWord实现,如果成功则获取轻量级锁,否则膨胀为重量级所;
- 锁也存在降级,JVM在进入安全点时,会检查是否有闲置的monitor并试图降级;
对象头:
普通对象对象头分两部分,每个部分在的32位虚拟机上占用32bit,64位虚拟机上占用64bit;
- MarkWord:后面重点介绍;
- Class Metadata:指向该对象类信息的指针;
数组类型则多了一个标记的数组大小的部分,这部分在32位虚拟机上是占用32位,在64位虚拟机上的占用空间是否是64bit则需要再查询资料确定;
- array length:记录数组长度;
MarkWord:
-
MarkWord用来记录对象的运行时数据,如HashCode、GC分代年龄、锁标志等,需要记录的信息超过了32位、64位所能记录的信息,但考虑到该部分是与对象数据无关的额外空间占用,所以根据对象的状态复用存储空间以提高空间利用率;
|-------------------------------------------------------|--------------------| | Mark Word (32 bits) | State | |-------------------------------------------------------|--------------------| | identity_hashcode:25 | age:4 | biased_lock:1 | lock:2 | Normal | |-------------------------------------------------------|--------------------| | thread:23 | epoch:2 | age:4 | biased_lock:1 | lock:2 | Biased | |-------------------------------------------------------|--------------------| | ptr_to_lock_record:30 | lock:2 | Lightweight Locked | |-------------------------------------------------------|--------------------| | ptr_to_heavyweight_monitor:30 | lock:2 | Heavyweight Locked | |-------------------------------------------------------|--------------------| | | lock:2 | Marked for GC | |-------------------------------------------------------|--------------------| |------------------------------------------------------------------------------|--------------------| | Mark Word (64 bits) | State | |------------------------------------------------------------------------------|--------------------| | unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 | Normal | |------------------------------------------------------------------------------|--------------------| | thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | lock:2 | Biased | |------------------------------------------------------------------------------|--------------------| | ptr_to_lock_record:62 | lock:2 | Lightweight Locked | |------------------------------------------------------------------------------|--------------------| | ptr_to_heavyweight_monitor:62 | lock:2 | Heavyweight Locked | |------------------------------------------------------------------------------|--------------------| | | lock:2 | Marked for GC | |------------------------------------------------------------------------------|--------------------|
biased_lock 1bit、lock的2bit位含义:
上表中的不同的state则是使用biased_lock 1bit和lock的2bit来进行标识的:
biased_lock:0 lock:01; 正常状态;
biased_lock:1 lock:01; 可偏向状态;
lock:00;轻量级锁;
lock:10;重量级锁;
lock:11;GC标记;
开启偏斜锁
-XX:+UseBiasedLocking:开启偏向锁
-XX:BiasedLockingStartupDelay=0,立即开启,因为默认偏向锁的开启时在虚拟机运行后延时5秒
锁的升级、降级:
偏向锁-无锁:
- 这里比较有疑问的是正常状态和可偏向状态,有一种观点认为:对象一开始可能出于可偏向状态并非正常状态,此时线程指针为0,称为匿名偏向状态,调用Object的hashCode函数或者System.identityHashCode(Object o)函数,他们是等价的,则会计算identity_hashcode并将其存放至对应的MarkWord部分,对象变为正常状态,一旦存放了identity_hashcode,那么该对象将不再允许偏斜,biased_lock会被设置为0,之后该对象将不会在进入偏向状态;
- 如果在计算hashcode时,该对象不是匿名偏向状态,而是偏向状态,则会直接膨胀为重量级锁;
- 如果没有线程竞争,非匿名偏向锁偏向锁释放后会变回匿名偏向锁状态;
- 正常状态下的对象被获取锁时,会直接进入轻量级锁;
锁升级-轻量级锁-重量级锁;
- 当别的线程试图获取一个已经偏斜的对象时,将会膨胀为轻量级锁,由持有锁的线程在安全点进行,轻量级锁会创建一个LockRecord在当前持有者线程的线程栈中,LockRecord包含一个owner,指向锁对象,锁对象MarkWord也会保存该LockRecord的指针;LockRecord还将保持对象之前的MarkWord内容;
- 升级为轻量级锁后,竞争线程将在一段时间内自旋,尝试获取锁,如果失败,则将MarkWord中lock标志设置为10并进入阻塞状态;
-
持有锁的线程在释放锁时会检查MarkWord是否变化,无变化则释放轻量级锁,将MarkWord替换回去;如果MarkWord变化了,则证明已经升级为重量级锁,线程需要将锁升级为重量级锁,并唤醒阻塞的线程;
锁获取流程图:
锁获取流程.png
concurrent包下的其他锁
- Lock:接口,典型的实现类为ReentrantLock
- ReadWriteLock:接口,典型实现ReentrantReadWriteLock,其逻辑在于并发读之间是不需要互斥的,在应用的并发读多并发写少时,可以考虑使用,因为其本身的复杂性,开销要比lock高;
- StampedLock:类,在提供类似读写锁的同时,还支持优化读模式。优化读基于假设,大多数情况下读操作并不会和写操作冲突,其逻辑是先试着读,然后通过 validate 方法确认是否进入了写模式,如果没有进入,就成功避免了开销;如果进入,则尝试获取读锁;
StampedLock使用示例:
public class StampedSample {
private final StampedLock sl = new StampedLock();
void mutate() {
long stamp = sl.writeLock();
try {
write();
} finally {
sl.unlockWrite(stamp);
}
}
Data access() {
long stamp = sl.tryOptimisticRead();
Data data = read();
if (!sl.validate(stamp)) {
stamp = sl.readLock();
try {
data = read();
} finally {
sl.unlockRead(stamp);
}
}
return data;
}
// …
}
