Java锁synchronized关键字学习系列之轻量级锁升级
Posted c.
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java锁synchronized关键字学习系列之轻量级锁升级相关的知识,希望对你有一定的参考价值。
Java锁synchronized关键字学习系列之轻量级锁升级
这篇博文我们来讲一下轻量级锁的升级到重量锁。
我们先开快速回顾一下如何升级到轻量级锁的。
回顾轻量级锁的加锁过程
(1)在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。
(2)拷贝对象头中的Mark Word复制到锁记录中。
(3)拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。如果更新成功,则执行步骤(3),否则执行步骤(4)。
(4)如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态。
(5)如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程,超过一定的自旋次数之后就会升级为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。
如果对这部分有疑惑的伙伴可以去看我的上一篇博文《Java锁synchronized关键字学习系列之轻量级锁》
自旋
上面提到了关于自旋,这里就来简单解释一下什么是自旋了。
所谓自旋,就是指当有另外一个线程来竞争锁时,这个线程会在原地循环等待,而不是把该线程给阻塞,直到那个获得锁的线程释放锁之后,这个线程就可以马上获得锁的。
注意,锁在原地循环的时候,是会消耗cpu的,就相当于在执行一个啥也没有的for循环。
所以,轻量级锁适用于那些同步代码块执行的很快的场景,这样,线程原地等待很短很短的时间就能够获得锁了。
经验表明,大部分同步代码块执行的时间都是很短很短的,也正是基于这个原因,才有了轻量级锁这么个东西。
什么情况下轻量级锁要升级为重量级锁呢?
那我们首先来讨论一下为啥要升级为重量级锁呢,轻量级锁没法应付多线程的竞争吗?
我看到有人这样说到:
轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的。但是,首先需要强调一点的是,轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。在解释轻量级锁的执行过程之前,先明白一点,轻量级锁所适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁。
所以确实轻量级锁只能应付线程竞争没这么激烈的情况。
那我们下面再来看看有哪些情况会升级为重量级锁呢?
首先我们可以思考的是多个线程的时候先开启轻量级锁,如果它carry不了的情况下才会升级为重量级。那么什么情况下轻量级锁会carry不住。1、如果线程数太多,比如上来就是10000个,那么这里CAS要转多久才可能交换值,同时CPU光在这10000个活着的线程中来回切换中就耗费了巨大的资源,这种情况下自然就升级为重量级锁,直接叫给操作系统入队管理,那么就算10000个线程那也是处理休眠的情况等待排队唤醒。2、CAS如果自旋10次依然没有获取到锁,那么也会升级为重量级。
总的来说2种情况会从轻量级升级为重量级,10次自旋(自适应自旋的话)或等待cpu调度的线程数超过cpu核数的一半,自动升级为重量级锁。
参考:谈谈JVM内部锁升级过程
轻量级锁升级重量级锁
然后我们这篇博文主要就是来详细讲一下上面的第五个步骤,轻量级锁升级为重量级锁的过程了.
在jdk1.6以前,默认轻量级锁自旋次数是10次,如果超过这个次数或自旋线程数超过CPU核数的一半,就会升级为重量级锁。这时因为如果自旋次数过多,或过多线程进入自旋,会导致消耗过多cpu资源,重量级锁情况下线程进入等待队列可以降低cpu资源的消耗。自旋次数的值也可以通过jvm参数进行修改:
-XX:PreBlockSpin
jdk1.6以后加入了自适应自旋锁 (Adapative Self Spinning),自旋的次数不再固定,由jvm自己控制,由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定:
- 对于某个锁对象,如果自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而允许自旋等待持续相对更长时间
- 对于某个锁对象,如果自旋很少成功获得过锁,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。
总之jvm比我们想象中还要智能,避免了很多资源的浪费。
下面通过代码验证轻量级锁升级为重量级锁的过程:
public class Main16 {
public static void main(String[] args) throws InterruptedException {
//-XX:-UseBiasedLocking 关闭偏向锁,只研究轻量级锁和重量级锁
User user = new User();
System.out.println("--MAIN--:" + ClassLayout.parseInstance(user).toPrintable());// 无锁
Thread thread1 = new Thread(() -> {
synchronized (user) {
System.out.println("--THREAD1--:" + ClassLayout.parseInstance(user).toPrintable()); //THREAD1获取锁之后,直接从无锁升级到轻量级锁
try {
TimeUnit.SECONDS.sleep(5); //开始休眠五秒,这个时候THREAD1还没有走完同步代码块,所以还没有释放锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread thread2 = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(2); //THREAD2 先睡眠2秒,保证THREAD1已经获取到锁,造成锁对象的资源竞争。
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (user) { //升级为重量级锁
System.out.println("--THREAD2--:" + ClassLayout.parseInstance(user).toPrintable());
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
TimeUnit.SECONDS.sleep(3);
System.out.println(ClassLayout.parseInstance(user).toPrintable());
//在线程1持有轻量级锁的情况下,线程2尝试获取锁,导致资源竞争,使轻量级锁升级到重量级锁。
// 在两个线程都运行结束后,可以看到对象的状态恢复为了无锁不可偏向状态,在下一次线程尝试获取锁时,会直接从轻量级锁状态开始。
//上面在最后一次打印前将主线程休眠3秒的原因是锁的释放过程需要一定的时间,如果在线程执行完成后直接打印对象内存布局,对象可能仍处于重量级锁状态。
}
}
class User {
private String username;
private int age;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
运行结果
--MAIN--:org.example.User object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 48 72 06 00 (01001000 01110010 00000110 00000000) (422472)
12 4 int User.age 0
16 4 java.lang.String User.username null
20 4 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
--THREAD1--:org.example.User object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 60 f3 3f 05 (01100000 11110011 00111111 00000101) (88077152)
4 4 (object header) bf 00 00 00 (10111111 00000000 00000000 00000000) (191)
8 4 (object header) 48 72 06 00 (01001000 01110010 00000110 00000000) (422472)
12 4 int User.age 0
16 4 java.lang.String User.username null
20 4 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
--THREAD2--:org.example.User object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 02 8f 08 84 (00000010 10001111 00001000 10000100) (-2079813886)
4 4 (object header) f0 01 00 00 (11110000 00000001 00000000 00000000) (496)
8 4 (object header) 48 72 06 00 (01001000 01110010 00000110 00000000) (422472)
12 4 int User.age 0
16 4 java.lang.String User.username null
20 4 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
org.example.User object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 48 72 06 00 (01001000 01110010 00000110 00000000) (422472)
12 4 int User.age 0
16 4 java.lang.String User.username null
20 4 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
一开始Main主线程中打印出来的是无锁
然后THREAD1上锁之后,就从无锁升级到轻量级锁了
在THREAD1还没有解锁之后,THREAD2进来又进行加锁,这个时候发生了竞争,所以升级到重量级锁了。
最后线程都跑完之后,又变为无锁了
总结
最后再来看看整体锁升级的流程图吧
参考
Java并发编程:Synchronized底层优化(偏向锁、轻量级锁)
线程安全(中)–彻底搞懂synchronized(从偏向锁到重量级锁)
源代码
以上是关于Java锁synchronized关键字学习系列之轻量级锁升级的主要内容,如果未能解决你的问题,请参考以下文章
Java锁synchronized关键字学习系列之批量重偏向和批量撤销
Java锁synchronized关键字学习系列之轻量级锁升级