Java锁synchronized关键字学习系列之偏向锁

Java锁synchronized关键字学习系列之偏向锁

synchronized 锁升级

在多线程并发编程中synchronized一直都是常用的,以前很多人都会称呼它为重量级锁。但是随着Java SE 1.6 对synchronized 进行了各种优化之后,有些情况下它就并不那么重,Java SE 1.6 中为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁轻量级锁

在Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但一般情况下不能降级。这种策略,目的是为了提高获得锁和释放锁的效率。

这里为啥说一般情况下不能降级呢,其实我看了很多文章说的都是锁可以升级但不能降级,但我实在很好奇是否存在有锁降级的情况,所以查看了大量的文章,发现其实锁也是可以降级的,只是锁降级发生的条件会比较苛刻。所以我们其实可以不用太关心锁降级,一般的认为只有锁升级就好了。

可以参考:

第九章 synchronized与锁

Java锁优化–JVM锁降级

关于锁升级,我们这一篇主要就只是关注升级到偏向锁的过程啦,虽然这个过程比较简单,但是还是有很多值得深入学习的地方。

偏向锁

大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,所以为了让线程获得锁的代价更低而引入了偏向锁。

举个更简单的例子,比如我们的StringBuffer中的方法,就是加了synchronized关键修饰的public synchronized StringBuffer append(Object obj), 但实际上我们使用的时候往往可能都是在单一线程使用,或者是不会发生资源争抢的情况。如果以前只有重量级锁,那代表中每次加锁我们都需要从用户态 切换到 内核态,需要操作系统配合加锁,这种切换相对来说比较占用系统资源。所以我们引入了这个偏向锁,在不存在多线程竞争的情况下,降低我们加锁的代价。

当一个线程访问加了同步锁的代码块时,会在对象头Mark Word中存储当前线程的 ID,后续这个线程进入和退出这段加了同步锁的代码块时,不需要再次加锁和释放锁。而是直接比较对象头里面是否存储了指向当前线程的线程ID。如果相等表示偏向锁是偏向于当前线程的,就不需要再尝试获得锁了。

大概的流程就是上面所说的,下面我们通过代码和对象头中Mark Word的内容来看看从无锁到偏向锁的过程Mark Word的变化。

在看这些对象头之前,我们来讲一下怎么判断这个对象头中存的是无锁标志还是偏向锁还是轻量级锁还是重量锁呢?

lock:锁状态标记位,该标记的值不同,整个mark word表示的含义不同。

biased_lock:偏向锁标记,为1时候表示对象启用偏向锁,为0的时候表示对象没有偏向锁

biased_locklock锁状态
001无锁
101偏向锁
000轻量级锁
010重量级锁
011GC标记

前面说到在没有多线程竞争的时候,只有单一线程进行锁争取的时候就会引入偏向锁,现在我们通过代码来演示一下。

public class Main4 {
    public static void main(String[] args) {
        Object o = new Object();
        System.out.println(ClassLayout.parseInstance(o).toPrintable()); //没有加锁
        System.out.println("============");
        synchronized (o) { //加锁之后
            System.out.println(ClassLayout.parseInstance(o).toPrintable());
            System.out.println("============");
        }
        System.out.println(ClassLayout.parseInstance(o).toPrintable()); //释放锁
        System.out.println("============");
    }
}

上面这个代码呢就只有main这个主线程在争取对象o上的锁,所以这种情况是会开启偏向锁的。如果开启了偏向锁模式的话,一开始就直接变成匿名偏向了,就是可以偏向,但是对象Mark Word中没有存入ThreadID,在我文章《Java锁Synchronized关键字学习系列》的上一篇中也有提到。

匿名偏向

这里我就在详细讲讲这个匿名偏向的情况吧。

如果开启了偏向锁模式(在JDK1.6以后,默认是开启的,但可能是有延迟的,JDK8的默认好像是4秒,不同JDK版本可能延迟不同。如果是JDK11的话默认是关闭延迟的)。

可以在控制台中通过java -XX:+PrintFlagsInitial | grep -i biased 来查看是否开启了偏向锁,还有偏向锁的延迟时间

$ java -XX:+PrintFlagsInitial | grep -i biased
     intx BiasedLockingBulkRebiasThreshold         = 20                                        {product} {default}
     intx BiasedLockingBulkRevokeThreshold         = 40                                        {product} {default}
     intx BiasedLockingDecayTime                   = 25000                                     {product} {default}
     intx BiasedLockingStartupDelay                = 0                                         {product} {default}
     bool UseBiasedLocking                         = true                                      {product} {default}

上面的打印是JDK11 打印出来的结果,默认是开启偏向锁的,然后延迟时间为0,所以一开始就是匿名偏向了。

如果我们关闭掉偏向锁模式,使用命令-XX:-UseBiasedLocking,那就不会开启偏向锁了,自然就不会出现匿名偏向的情况了,但结果就是一旦出现锁竞争就直接从无锁变成轻量级锁了。

但是我们如果开启了偏向锁模式的话,在延迟时间之内和之外就会出现两种不一样的对象头了,就是无锁和匿名偏向了。

如果在延迟时间之内创建对象并打印对象头的话,你可以发现出来的结果是无锁的。你可以通过命令设置延迟时间-XX:BiasedLockingStartupDelay=4000设置大一点的话就可以看到效果了。因为这个时候创建的对象是在延迟时间之内的,那么这个时候一开始打印的就是无锁的状态了。

在这里插入图片描述

然后加锁之后可以看到锁的状态是轻量级锁

在这里插入图片描述

因为这个时候相当于创建的对象还并没有开启这个偏向锁模式,所以结果是从无锁到轻量级锁(即使休眠5秒之后再加锁结果也是一样)。

走的就是下面这个流程,我们主要关心的就是偏向锁未启动的时候,是直接从无锁到轻量级锁的过程

在这里插入图片描述

如果我们设置成-XX:BiasedLockingStartupDelay=0,那就是直接关闭延迟了,那在延迟时间之后创建对象并打印对象头的话,就是一种匿名偏向的情况了,状态是偏向锁状态,但是后面没有存线程ID。
在这里插入图片描述

加锁之后的结果是偏向锁了,对象头中也存了ThreadID

在这里插入图片描述

走的流程就是下面这一种了,我们主要关心的是,如果开启了偏向锁之后,会从匿名偏向到偏向锁的状态。

在这里插入图片描述

可以参考一下:

偏向锁、轻量锁与重量锁,你真的了解吗?

(二)偏向锁详解

接下来我们来看打印出来的对象头的内容来进一步分析。我们这里设置了-XX:BiasedLockingStartupDelay=0,为了演示一下升级到偏向锁的情况啦。

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           00 10 00 00 (00000000 00010000 00000000 00000000) (4096)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

============
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 f8 d1 18 (00000101 11111000 11010001 00011000) (416413701)
      4     4        (object header)                           fc 01 00 00 (11111100 00000001 00000000 00000000) (508)
      8     4        (object header)                           00 10 00 00 (00000000 00010000 00000000 00000000) (4096)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

============
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 f8 d1 18 (00000101 11111000 11010001 00011000) (416413701)
      4     4        (object header)                           fc 01 00 00 (11111100 00000001 00000000 00000000) (508)
      8     4        (object header)                           00 10 00 00 (00000000 00010000 00000000 00000000) (4096)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

============

Process finished with exit code 0

那我们现在开始分析上面打印出来的内容吧。

最开始打印的是101,所以这个时候表示的是匿名偏向状态。

在这里插入图片描述

然后经过synchronized (o)之后我们给对象加锁了

在这里插入图片描述

这个时候我们看到还是101,所以这个时候表示的是偏向锁锁,然后后面原本全部为0的,现在也变成有内容的,所以这部分中就存储了ThreadID。

最后走出synchronized包裹的代码块中,对象头如下

在这里插入图片描述

所以根据上面的演示,我们可以了解到升级到偏向锁和轻量级锁的过程。

当JVM启用了偏向锁模式(JDK6以上默认开启),新创建对象的Mark Word中的Thread Id为0,说明此时处于可偏向但未偏向任何线程,也叫做匿名偏向状态(anonymously biased)

偏向锁的加锁和释放锁

偏向锁逻辑,深入挖掘的话还是比较复杂

  1. 线程A第一次访问同步块时,先检测对象头Mark Word中的标志位是否为01,依此判断此时对象锁是否处于无锁状态或者偏向锁状态(匿名偏向锁);

  2. 然后判断偏向锁标志位是否为1(是否为偏向锁),如果不是(如果是无锁),则进入轻量级锁逻辑(使用CAS竞争锁),如果是(如果是偏向锁状态[匿名偏向锁]),则进入下一步流程;

  3. 判断是偏向锁时,检查对象头Mark Word中记录的Thread Id是否是当前线程ID,如果是,则表明当前线程已经获得对象锁,以后该线程进入同步块时,不需要CAS进行加锁,只会往当前线程的栈中添加一条Displaced Mark Word(用于存储锁对象目前的Mark Word的拷贝)为空的Lock Record中,用来统计重入的次数(如图为当对象所处于偏向锁时,当前线程重入3次,线程栈帧中Lock Record记录)。

在这里插入图片描述

退出同步块释放偏向锁时,则依次删除对应Lock Record,但是不会修改对象头中的Thread Id

(注:偏向锁撤销是指在获取偏向锁的过程中因不满足条件导致要将锁对象改为非偏向锁状态,而释放是指退出同步块时的过程。)

  1. 如果对象头Mark Word中Thread Id不是当前线程ID,则进行CAS操作,企图将当前线程ID替换进Mark Word。如果当前对象锁状态处于匿名偏向锁状态可偏向未锁定,因为这个时候偏向锁标志位是1,是偏向锁状态,但是没有记录Thread Id),则会替换成功(将Mark Word中的Thread id由匿名0改成当前线程ID,在当前线程栈中找到内存地址最高(从栈底往栈顶找)的可用Lock Record,把当前的Mark Word拷贝进去,相当于将线程ID也存入进去了),获取到锁,执行同步代码块;

  2. 如果对象锁已经被其他线程占用,则会替换失败,开始进行偏向锁撤销,这也是偏向锁的特点,一旦出现线程竞争,就会撤销偏向锁;

  3. 偏向锁的撤销需要等待全局安全点(safe point,代表了一个状态,在该状态下所有线程都是暂停的),暂停持有偏向锁的线程,检查持有偏向锁的线程状态(遍历当前JVM的所有线程,如果能找到,则说明偏向的线程还存活),如果线程还存活,则检查线程是否在执行同步代码块中的代码,如果是,则升级为轻量级锁,进行CAS竞争锁;

注:每次进入同步块(即执行monitorenter)的时候都会以从高往低(从栈底往栈顶找)的顺序在栈中找到第一个可用的Lock Record,并设置偏向线程ID;每次解锁(即执行monitorexit)的时候都会从最低(栈顶)的一个Lock Record移除。所以如果能找到对应的Lock Record说明偏向的线程还在执行同步代码块中的代码。

  1. 如果持有偏向锁的线程未存活,或者持有偏向锁的线程未在执行同步代码块中的代码,则进行校验是否允许重偏向,如果不允许重偏向,则撤销偏向锁,将Mark Word设置为无锁状态(未锁定不可偏向状态),然后升级为轻量级锁,进行CAS竞争锁;

  2. 如果允许重偏向,设置为匿名偏向锁状态,CAS将偏向锁重新指向线程A(在对象头和线程栈帧的锁记录中存储当前线程ID)

  3. 唤醒暂停的线程,从安全点继续执行代码

关于这个Lock Record的不懂的可以参考一下:synchronized和锁对象

偏向锁流程图

在这里插入图片描述

在这里插入图片描述

总结

虽然偏向锁逻辑简单的说的话就是只是通过对象头Mark Word的偏向锁标记,如果是偏向锁并且是匿名偏向状态,那么就可以把当前线程的ThreadId设置到Mark Word中,从而升级到偏向锁。如果是无锁状态,则会升级到轻量级锁。在偏向锁的状态下,如果发生竞争,就可能升级到轻量级锁了。这个系列真的挺难的,学习了好久,好多看不懂,也有很多资料的讲解都不太一样。最近学习的状态不太好了,希望可以继续坚持下去了。之后可能会更具体地讲一下这个偏向锁升级到轻量级锁的过程吧(希望不会鸽)。

学习参考

【多线程】面试官问我CAS、乐观锁、悲观锁,我反手就是骑脸输出

【多线程】月薪20K必须知道的Java锁机制

不可不说的Java“锁”事

偏向锁到底是怎么回事啊啊啊啊

从锁升级的角度理解synchronized

马士兵亲授:多线程与高并发——锁机制、锁升级

做实验验证JDK8偏向锁:未启动、匿名偏向、偏向锁失效

Synchronized笔记

JVM知识全景图

Java锁-Synchronized深层剖析

JVM 的Lock Record简介

偏向锁的【批量重偏向与批量撤销】机制

偏向锁

源代码

https://gitee.com/cckevincyh/java_synchronized_learning

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值