一、synchronized加锁原理
由于synchronized总体的工作原理是通过操作对象头来实现加锁和解锁,因此在具体的了解synchronized之前,首先需要简单了解一下对象头。
所谓对象头其实是每个对象都存在的一份存储当前对象gc分代年龄、hashcode、锁标记、当前获取到锁的线程ID等信息的一块内存区域。对象头主要分为以下三块:
1.Mark Word
2.指向类的指针
3.数组长度(只有数组对象才有)
synchronized在加锁过程中主要对mark word进行操作,因此主要对mark word的划分,各个划分快存储的内容进行探索。如下图所示mark word的划分以及各个划分块存储的内容:
在64位jvm中mark word的长度为64bit,从图中我们可以看出最后两位用来作为锁标记位,在无锁和偏向锁状态时还存在1bit的偏向锁位用来标记是否是可偏向的。
我们可以使用jol来查看对象头信息,要是有jol需要引入如下包:
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.8</version>
</dependency>
执行以下代码我们可以简单看一下对象头信息:
Object o=new Object();
System.out.println(VM.current().details());
System.out.println(ClassLayout.parseInstance(o).toPrintable());
执行结果:
从执行结果我们可以看到对象头总共占了96bit【空框1】,其中mark Word占了64bit【红框2】,最后三位的状态为001说明当前处于无锁状态。
从上边我们知道了synchronized在加锁解锁时操作的内存空间,那么synchronized是如何操作的,在什么时候进行操作,操作了什么内容呢,下文将进一步了解。
使用javap反编译工具对含有synchronized代码块的代码进行反汇编,执行javap -v App.class的运行结果:
可以看到在被synchronized包起来的代码块前后增加了monitorenter和monitorexit两条汇编指令,当代码执行到monitorenter指令时开始加锁操作,大概执行过程如下:
在代码进入代码块的时候,如果此同步对象没有被锁定(锁标志位为“01”状态),虚拟机首先将在当前线程的栈帧中建立一个名为Lock Record的空间,用于存储锁对象目前的Mark Word的拷贝,然后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为Lock Record的指针。如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且队形Mark Word的锁标志位(Mark Word的最后两个bits)将转变为‘00’,即表示此对象处于轻量级锁定的状态。
—深入理解JVM
二、synchronized锁膨胀过程
锁膨胀是从偏向锁—>轻量级锁---->重量级锁的转换过程。在探究膨胀过程之前,我们先简单了解一下偏向锁、轻量级锁、重量级锁的概念。
①重量级锁:传统重量级锁指使用操作系统互斥量来实现加锁(PV操作),使用这种锁的话,即使没有所竞争的时候还是会调用操作系统级别的互斥量实现加锁,造成性能消耗。
②轻量级锁:通过简单的数据操作实现加锁,不需要调用OS级别的加锁机制。
*在当前线程自己的栈区创建Lock Record用于存储Object的Mark Word的拷贝
*将Mark Word副本存入lock record
*基于CAS的机制将Object的Mark Word跟新为指向Lock record的指针
*如果CAS跟新成功,则当前线程获取到锁,否则检查Mark Word出的指针是否指向自己的栈针
*如果指向自己的栈针,则说明已经获取到锁,则重入代码快执行,否则说明锁已经被占用,则膨胀为重量级锁
③偏向锁:偏向锁可以说是在轻量级锁上的进一步优化,轻量级锁在没有锁竞争的条件下还需要执行CAS操作,偏向锁连CAS都不操作。所谓“偏”就是说偏袒于第一次获得锁的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将不需要执行同步操作,直接获得运行资源。直到另外一个线程到来,偏向锁开始膨胀。
**锁膨胀的大概过程是:如果开启了偏向锁,那么在没有锁竞争的情况下,锁状态为偏向锁,如果出现两个线程竞争同一个锁的情况,锁状态将膨胀为轻量级锁,如果出现2个以上线程竞争同一个锁时,则锁状态膨胀为重量级锁。**接下来我们通过代码演示一下锁膨胀的过程:
为了能够看到偏向锁,首先需要配置虚拟机开启偏向锁,在idea中 run—>Edit Confingutions—>VM options中加入如下配置信息:
-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
1.在不存在多个线程竞争同一个锁的时候,锁状态处于偏向锁状态,锁状态标志位为01,是否偏向锁状态位值为1,其中在Mark work中也存储了当前获取到锁的线程的Id,代码如下:
Object o=new Object();
int i=0;
synchronized (o)
{
i++;
synchronized (o) {
i++;
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
运行结果:
2.当存在1个以上的线程开始竞争同一个锁的时候,锁会膨胀为轻量级锁,锁标记为变为00,并且在Mark work中也会存储指向线程栈中Lock record的指针。代码如下:
Object o=new Object();
int i=0;
synchronized (o)
{
i++;
}
Thread t1=new Thread(()->{
synchronized (o) {
//执行业务逻辑。。。
try {
Thread.sleep(5000);
System.out.println(ClassLayout.parseInstance(o).toPrintable());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
执行结果:
3.当存在三个以上的线程竞争同一个锁的时候,锁会升级为重量级锁,演示代码如下:
Object o=new Object();
Thread t1=new Thread(()->{
synchronized (o) {
//执行业务逻辑。。。
int sum=0;
for (int j = 0; j < 100; j++) {
sum+=j;
}
}
});
t1.start();
Thread.sleep(1000);
Thread t2=new Thread(()->{
synchronized (o) {
//执行业务逻辑。。。
try {
Thread.sleep(5000);
System.out.println(ClassLayout.parseInstance(o).toPrintable());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t2.start();
Thread t3=new Thread(()->{
synchronized (o) {
//执行业务逻辑。。。
int mul=1;
for (int i = 1; i <100 ; i++) {
mul*=i;
}
}
});
t3.start();
运行结果:
锁标志位变成了10,并且在Mark work中存储了执行信号量的指针。