这篇文章主要介绍了JVM中Synchronized锁实现的机制。
主要分为几个部分:
了解虚拟机类文件结构的同学们一定知道,对于synchronzied方法块而言,虚拟机在块内的方法前后会增加moniterenter
和moniterexit
两个指令,而对于synchronized方法来说,在方法的ACCESS_FLAG
中会出现一个ACC_SYNCHRONIZED
的标志位,虚拟机会根据该标识位隐式的执行同步过程。
这两种都是由管程(Monitor)支持实现的(应该说是在虚拟机未对锁优化前)。一个线程在上锁的时候会尝试获取对象关联的monitor
,如果该monitor
未被其他线程获取,那么该线程将会获得此monitor
,将ownership改为自己,并将锁的计数器加1。否则线程将进入monitor
的等待队列,等待monitor
被释放后再尝试获取。
整个过程是基于mutex互斥量来实现的,因此需要涉及用户态和内核态的切换,会消耗很多处理器时间。因此,基于该方式实现的synchronized锁也被称为重量级锁。
JDK1.6之后对传统的Synchronized的锁做了很多优化,尽量避免重量级锁的直接使用,提高线程在上锁和释放锁时的效率。
上文已经介绍了传统的synchronzied锁是基于mutex互斥量的,其主要的缺点是是在上锁过程中可能需要挂起线程,涉及用户态和内核态的切换,浪费处理器时间。
轻量级锁的轻量级是相对于基于mutex互斥量实现的重量级锁而言。
在我们大部分的程序中,线程间的竞争并不激烈,且线程并不会长时间的持有锁。如果在不存在竞争并且锁将立被释放的情况下,也通过重量级锁去上锁和释放锁,那么对锁的操作浪费的时间可能比代码执行的时间更多。
轻量级锁通过CAS设置加自选等待的方式解决了上述这种场景下重量级锁低效的问题。
在使用轻量级锁时,线程会尝试通过CAS更新锁对象的对象头,如果更新成功,说明成功标记对象。如果更新失败,则说明该对象已经被其他线程持有,线程会进入自选等待,因为通常一个线程不会长时间的持有锁,因此很可能尝试获取锁的线程只需要几次自旋获取锁。
如果一段时间自选后,线程依旧无法获取锁,那么轻量级锁才会被升级成为重量级锁。
虽然轻量级锁已经极大的提升了锁的效率,但是线程每次上锁和释放锁依然会产生时间的浪费。而一种极端的情况下,一个锁可能都是由某个线程去获取的(也就是其他线程不太会去获取这个锁,也就是不存在竞争的情况)。
偏向锁就是出于对上述这种情况而进行的优化,希望将无竞争下的同步过程消除。
偏向锁会偏向第一个获取他的线程,之后就算该线程退出同步方法,偏向锁对该线程的标记依旧在,这样做的好处是该线程之后获取锁和释放锁都不需要进行CAS更新操作。只需要对比偏向锁的标记是否未自己。
直到有其他线程获取该锁时,发现该锁标记的对象不是自己,则会要求该锁升级。
除了上述锁实现机制的优化外,编译器还通过自旋,锁消除,锁粗化的方式对锁进行优化。
自旋在介绍轻量级锁时也介绍到了,当线程发现锁被持有时,线程不会立即挂起,而是尝试自选等待。
这样做的好处是,避免了操作系统在用户态和内核态的来回切换。但是缺点是自旋等待会白白占用处理器的运行时间。
锁消除是指在一些不存在竞争的情况下,编译器会取消掉同步的过程。
锁粗化是指某一线程在一个方法内频繁的上锁和释放锁,编译器会主动扩大一次上锁覆盖的范围,减少上锁和释放锁的次数。
上文介绍了虚拟机对Synchronized锁做了优化。
在开启了偏向锁的情况下,先会使用偏向锁,当有线程竞争偏向锁时,会发生锁的升级,偏向锁会升级为轻量级锁。
如果轻量级锁超过一定自旋次数,仍旧无法获取,那么会发生锁膨胀,变成重量级锁,通过mutex的方式实现互斥。
并且这一过程中会造成对象头中Mark Word
的改变,或者说对象头中的Mark Word
会记录着这一过程的变化。
那么什么是Mark Word
?OpenJDK中给出的定义如下:
mark word:The first word of every object header. Usually a set of bitfields including synchronization state and identity hash code. May also be a pointer (with characteristic low bit encoding) to synchronization related information. During GC, may contain GC state bits.
Mark Word:是每个对象头中第一个字。用一组位表示同步锁状态和哈希值等,也可能指向同步锁相关的信息(如遇字符,则用小端)。在GC阶段,还包含了GC状态。
从这里我们基本可以了解到 MarkWord 是一个标志对象诸多状态的一字长的数据(32位虚拟机和64位虚拟机所占位数不同)。
更具体的,我们可以通过下图(原图出处)了解下对象头中Mark Word
在各种锁状态下的结构(这里以64位虚拟机为例,32位虚拟机基本类似)。
处于节约内存的目的考虑,MarkWord的一字长的数据会在不同状态下用来表示不同的信息。
从右往左看,最后两位是锁状态的标志位。结合锁标志位和偏向锁标识位,我们就可以区分当前对象锁的状态,其余的位可能会用来记录线程或是其他相关的指针信息。
大致了解完 Mark Word结构后,我们通过另一张图片(原图出处)了解锁膨胀的过程,结合过程中Mark Word
的改变。关于Mark Word
的详细说明将在下文介绍。
这幅图非常详细,我们可以拆成三部分逐个过程分析:
这部分流程比较简单,不再赘述。
上面从理论上介绍了锁的升级过程,但是对于对象头这种看不见摸不着的信息,可能依然有同学看的懵里懵懂。
好在openjdk提供了一个利器帮助我们打印对象头信息——jol-core
库。
可以通过 maven添加到我们的库中:
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
在开始测试前,有一点需要明确:32位虚拟机和64位虚拟机对象头的结构是有一些差异的(本文测试均是基于64位虚拟机),且结构是以小端的方式存储数据。
以下是我们测试的一些基础类:;Monitor类作为对象锁的类,主要是验证MarkWord关于HashCode的内容:
保存上锁的方法
public class Foo {
private Monitor lock = new Monitor();
public void sync(){
synchronized (lock){
System.out.println("------------in sync()-------------");
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
}
}
public void syncAndSleep() throws InterruptedException {
synchronized (lock){
System.out.println("------------take time sync()-------------");
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
Thread.sleep(5000);
}
}
public void printLockObjectHeader(){
System.out.println("Thread:" + Thread.currentThread().getName() + ";" +ClassLayout.parseInstance(lock).toPrintable());
}
public void calculateHashAndPrint(){
System.out.println("Calculate Hash:" +Integer.toHexString(lock.hashCode()));
System.out.println("After invoke hashcode, print Object again");
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
}
}
Monitor类是测试中用作对象锁的类简单的继承了Object类,仅可能会对hashCode方法做一些修改(用来验证MarkWord中哈希值的相关信息)。
public class Monitor {
//我们可能处于特定的测试目的考虑,会注释掉这个方法,使用仍使用父类的方法
@Override
public int hashCode() {
//必须要调用父类的hashCode方法 mark work中才会存hashCode
return 0xff;
}
}
测试主入口,内部的几个方法之后几个测试的内容,之后我们将会依次运行这些方法对比对象头的信息:
public class SynchronizedUpgradeTest {
static Foo foo = new Foo();
public static void main(String[] args) throws InterruptedException {
hashCodeTest();
// biasedLock();
// biasedLockInvalidAfterCalculate();
// biasedLockUpgradeToLightLock();
// lightLockToWeightLock();
}
protected static void hashCodeTest(){
foo.printLockObjectHeader();
foo.calculateHashAndPrint();
}
/**
* JVM OPTIONS: -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
*/
protected static void biasedLock(){
foo.printLockObjectHeader();
foo.sync();
System.out.println("Exit sync()");
foo.printLockObjectHeader();
}
/**
* JVM OPTIONS: -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
*/
protected static void biasedLockInvalidAfterCalculate(){
foo.printLockObjectHeader();
foo.calculateHashAndPrint();
foo.sync();
System.out.println("out sync()");
foo.printLockObjectHeader();
}
/**
* JVM OPTIONS: -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
*/
protected static void biasedLockUpgradeToLightLock(){
foo.printLockObjectHeader();
foo.sync();
System.out.println("out sync()");
foo.printLockObjectHeader();
System.out.println("---Another Thread Use Biased Lock");
Thread thread = new Thread(()->{
foo.sync();
System.out.println("---Another Thread Out sync");
foo.printLockObjectHeader();
});
thread.start();
}
/**
* JVM OPTIONS: -XX:UseBiasedLocking -XX:BiasedLockingStartupDelay=10
*/
protected static void lightLockToWeightLock() throws InterruptedException {
foo.printLockObjectHeader();
new Thread(()->{
try {
foo.syncAndSleep();
foo.printLockObjectHeader();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
Thread.sleep(1000L);
foo.sync();
foo.printLockObjectHeader();
}
}
以上全部测试代码都可以在我的GitHub中找到。同时为了保证测试的正确性,需要确保虚拟机运行参数和测试方法上的配置一致!
可以从测试的图片中看到,对象锁MarkWord中关于锁的那几位确实是101,说明是偏向锁没错。但是在无论在调用hashCode前还是后的打印,MarkWord中都没有记录对象的Hash值。这似乎和我们值钱了解到的不太一样。其实这是因为我们重写了hashCode()方法。只有调用原生的hashCode()才会将哈希值记录在MarkWord中!
为了验证我们的猜测,我们注释掉Monitor类中的hashCode()方法,在测试一次。
当我们使用Object中的hashCode()方法时,MarkWord确实保存了哈希值。但是,另一个有趣的事情发生了,偏向锁直接升级成了轻量级锁。
从这里结果我们可以看出,解释线程释放了偏向锁,偏向锁依旧保存着线程的ID。
当偏向锁被标记过后,另一个线程再去获取锁时,锁会被升级成轻量级锁。并且在解锁后,也没有重新回到偏向锁的状态。
测试开始前,我们通过JVM参数设置让偏向锁一开始先不生效。
从测试结果中,我们可以看到轻量级锁膨胀为重量锁的过程。并且MarkWord中记录的信息也由栈帧的指针改为了monitor的指针。
原文:https://www.cnblogs.com/insaneXs/p/13378994.html