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循环。
所以,轻量级锁适用于那些同步代码块执行的很快的场景,这样,线程原地等待很短很短的时间就能够获得锁了。
经验表明,大部分同步代码块执行的时间都是很短很短的,也正是基于这个原因,才有了轻量级锁这么个东西。

参考:线程安全(中)–彻底搞懂synchronized(从偏向锁到重量级锁)

什么情况下轻量级锁要升级为重量级锁呢?

那我们首先来讨论一下为啥要升级为重量级锁呢,轻量级锁没法应付多线程的竞争吗?
我看到有人这样说到:

轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的。但是,首先需要强调一点的是,轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。在解释轻量级锁的执行过程之前,先明白一点,轻量级锁所适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁。

参考:Java并发编程:Synchronized底层优化(偏向锁、轻量级锁)

所以确实轻量级锁只能应付线程竞争没这么激烈的情况。

那我们下面再来看看有哪些情况会升级为重量级锁呢?

首先我们可以思考的是多个线程的时候先开启轻量级锁,如果它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底层优化(偏向锁、轻量级锁)

谈谈JVM内部锁升级过程

再谈synchronized锁升级

线程安全(中)–彻底搞懂synchronized(从偏向锁到重量级锁)

源代码

https://gitee.com/cckevincyh/java_synchronized_learning

以上是关于Java锁synchronized关键字学习系列之轻量级锁升级的主要内容,如果未能解决你的问题,请参考以下文章

Java锁synchronized关键字学习系列之重量级锁

Java锁synchronized关键字学习系列之批量重偏向和批量撤销

Java锁synchronized关键字学习系列之轻量级锁

Java锁synchronized关键字学习系列之轻量级锁升级

Java多线程系列---“基础篇”04之 synchronized关键字

Java基础加强之并发synchronized关键字