Java SE 1.6 为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”。因此锁一共有4种状态,从低到高依次是:无锁、偏向锁、轻量级锁、重量级锁
Hotspot 的作者经过以往的研究发现大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程 ID,以后该线程在进入和退出同步块时不需要花费CAS操作来加锁和解锁,而只需简单的测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁,如果测试成功,表示线程已经获得了锁,如果测试失败,则需要再测试下 Mark Word中偏向锁的标识是否设置成 1(表示当前是偏向锁),如果没有设置,则使用 CAS 竞争锁,如果设置了,则尝试使用 CAS 将对象头的偏向锁指向当前线程。
轻量级锁加锁:线程在执行同步块之前, JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用 CAS 将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。如果有两条以上的线程争用同一个锁,那轻量级锁就不再有效,要膨胀为重量级锁,锁标志的状态值变为”10”,Mark Word中存储的就是指向重量级(互斥量)的指针。
轻量级锁解锁:轻量级解锁时,会使用原子的 CAS 操作来将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。
内置锁在Java中被抽象为监视器锁(monitor)。在JDK 1.6之前,监视器锁可以认为直接对应底层操作系统中的互斥量(mutex)。这种同步方式的成本非常高,包括系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等。因此,后来称这种锁为“重量级锁”。
如果一个锁为重量级锁,当一个线程拥有这把锁的时候,其它线程处于阻塞状态。
在此之前,首先粗略认识一下 Java 对象的构成,Java对象头由4部分构成
1 — Java对象头中MarkWord信息(64位虚拟机的MarkWord为64bit)
2 — Java对象头中元数据指针 (User对象没有数组,所以没有数组的长度信息)
3 — 实例数据,以及数据的初始值信息
4 — 填充长度,保证对象的长度是8的整数倍(64位虚拟机),方便寻址,和操作系统内部有关
我们重点关注的是 Java 对象头中 MarkWord 的变化,有关锁的信息都存储在里面,这里直接上图
可以看出,不同的锁对应这不同的标志位
<!--可以查看对象相关的信息-->
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>
public class User {
private int id;
private String name;
// 这里省略构造方法和set、get方法
..........
}
public class LockUpdate {
public static void main(String[] args) throws Exception {
User userTemp = new User();
System.out.println("无状态001: " + ClassLayout.parseInstance(userTemp).toPrintable());
}
}
注意:JVM 默认延时4s开启偏向锁,可通过 -XX:BiasedLockingStartupDelay=0 取消延时。如果不要偏向锁,可以通过**-XX:-UserBiasedLocking=false**来设置
public class LockUpdate {
public static void main(String[] args) throws Exception {
User userTemp = new User();
System.out.println("无状态001: " + ClassLayout.parseInstance(userTemp).toPrintable());
/* JVM 默认延时4s开启偏向锁,可通过 -XX:BiasedLockingStartupDelay=0 取消延时
如果不要偏向锁,可以通过-XX:-UserBiasedLocking=false来设置
*/
Thread.sleep(5000);
User user = new User();
System.out.println("偏向锁101: " + ClassLayout.parseInstance(user).toPrintable());
}
}
运行结果可以看出,经过5秒之后,锁变成了轻量级锁,Java 虚拟机会默认延时开启偏向锁,但是注意,此时还没有在对象头中存储线程的 ID
对这个对象重复加锁
for(int i = 0; i < 2; i++){
synchronized (user){
System.out.println("偏向锁101 (带线程id): " + ClassLayout.parseInstance(user).toPrintable());
}
/* 释放偏向锁,但对象中的线程id不会被改变,因为是偏向的
下次这个对象再次获取锁的时候,只需要判断这个对象头的MarkWord中是否存储着指向该线程的线程ID
*/
System.out.println("释放偏向锁101 (带线程id): " + ClassLayout.parseInstance(user).toPrintable());
}
当线程释放锁(在synchronized代码块外面),对象头里面的线程 ID 不会变化,直到出现另外的线程。
下一步我们开启另外的线程,模拟有线程竞争的情况。
/*
这里开启另外的线程,模拟有线程竞争的情况
*/
new Thread(()->{
synchronized (user){
System.out.println("轻量级锁00: " + ClassLayout.parseInstance(user).toPrintable());
}
}).start();
通过在上面的代码块中加入休眠,因为在sleep的过程中,对象不会释放锁,因此此时如果再开启线程,又会出现线程竞争的情况。轻量级锁变为重量级锁。
new Thread(()->{
synchronized (user){
System.out.println("轻量级锁00: " + ClassLayout.parseInstance(user).toPrintable());
try {
//System.out.println("睡眠4秒钟======================");
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("轻量级-->重量级10: " + ClassLayout.parseInstance(user).toPrintable());
}
}).start();
/*
* 在上一个线程休眠的时候,没有释放锁,此时又有多个线程参与竞争,会变成重量级锁
* 一旦升级成重量级锁,就不会再变成轻量级状态
*/
Thread.sleep(1000);
new Thread(()->{
synchronized (user){
System.out.println("重量级锁10: " + ClassLayout.parseInstance(user).toPrintable());
}
}).start();