synchronized锁升级与降级


针对Synchronized。在Java 5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。

Synchronized锁升级与降级

对象的内存布局

在HotSpot虚拟机中,对象的内存布局可以划分为三部分:对象头(header)、实例对象(Instance Data)和对齐填充(Padding)。

HotSpot虚拟机对象的对象头包含两类信息。第一类是用于存储对象本身的运行时数据,如哈希码、GC分代年龄、锁状态标志等,官方称之为(Mark Word)。对象头的另一部分为类型指针(klass word),即指向它的类型元数据的指针,JAVA虚拟机通过这个指针来确定该对象是哪个类的实例。此外,如果对象是一个数组,那么对象头里边还必须有一块用于记录数组长度的数据。

实例数据部分是对象真正存储的有效信息,即我们在程序中定义的各种类型的字段内容。HotSpot虚拟机默认的分配顺序为longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers,OOPs),从以上默认的分配策略中可以看到,相同宽度的字段总是被分配到一起存放,在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前。如果HotSpot虚拟机的+XX:CompactFields参数值为true(默认就为true),那子类之中较窄的变量也允许插入父类变量的空隙之中,以节省出一点点空间。

对象的第三部分为对齐填充,这并不是必然存在的,仅起占位符的作用,因为虚拟机要求对象的起始地址必须是8字节的整数倍,换句话说就是任何对象的大小都必须是8字节的整数倍。对象头部分已经被精心设计成正好是8字节的倍数(1倍或者2倍),因此,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。

对象信息查看分析

POM文件中引入JOL包

<dependency>
  <groupId>org.openjdk.jol</groupId>
  <artifactId>jol-core</artifactId>
  <version>0.16</version>
</dependency>

测试类

package com.rrc.util;

import org.openjdk.jol.info.ClassLayout;

public class ObjectSee {

    public static void main(String[] args) {
        ObjectSee a = new ObjectSee();
        System.out.println(ClassLayout.parseInstance(a).toPrintable());
    }
}

打印结果:

com.rrc.util.ObjectSee 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)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

从打印日志我们发现整个对象一共16B,其中对象头(Object header)12B,还有4B是对齐的字节(因为在64位虚拟机上对象的大小必 须是8的倍数),

由于这个对象里面没有任何字段,故而对象的实例数据为0B。

为对象增加boolean类型属性

package com.rrc.util;

import org.openjdk.jol.info.ClassLayout;

public class ObjectSee {
    private boolean flag;

    public static void main(String[] args) {
        ObjectSee a = new ObjectSee();
        //System.out.println(Integer.toHexString(a.hashCode()));
        System.out.println(ClassLayout.parseInstance(a).toPrintable());
    }
}

打印结果:

com.rrc.util.ObjectSee 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)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
     12     1   boolean ObjectSee.flag                            false
     13     3           (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 3 bytes external = 3 bytes total

整个对象的大小还是没有改变一共16B,其中对象头(Object header)12B,boolean字段flag(对象的实例数据)占 1B、剩下的3B就是对齐字节。

由此我们可以认为一个对象的布局大体分为三个部分分别是:对象头(Object header)、 对象的实例数据和字节对齐

对象头信息查看分析

对象头组成

根据上述利用JOL打印的对象头信息可以知道一个对象头是12B,其中8B是mark word 那么剩下的4B就是klass word了,和锁相关的就是mark word了。

手动计算HashCode

package com.rrc.util;

import sun.misc.Unsafe;

import java.lang.reflect.Field;

public class HashUtil {
    public static void countHash(Object object) throws NoSuchFieldException, IllegalAccessException {
        // 手动计算HashCode
        Field field = Unsafe.class.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        Unsafe unsafe = (Unsafe) field.get(null);
        long hashCode = 0;
        for (long index = 7; index > 0; index--) {
            // 取Mark Word中的每一个Byte进行计算
            hashCode |= (unsafe.getByte(object, index) & 0xFF) << ((index - 1) * 8);
        }
        String code = Long.toHexString(hashCode);
        System.out.println("util-----------0x"+code);
    }
}
package com.rrc.util;

import org.openjdk.jol.info.ClassLayout;

public class ObjectSee {
    private boolean flag;

    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        ObjectSee b = new ObjectSee();

        System.out.println("befor hash");
        //没有计算HASHCODE之前的对象头
        System.out.println(ClassLayout.parseInstance(b).toPrintable());
        //JVM 计算的hashcode
        System.out.println("jvm------------0x"+Integer.toHexString(b.hashCode()));
        HashUtil.countHash(b);
        //当计算完hashcode之后,我们可以查看对象头的信息变化
        System.out.println("after hash");
        System.out.println(ClassLayout.parseInstance(b).toPrintable());
    }
}

运行结果为:

befor hash
com.rrc.util.ObjectSee 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)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
     12     1   boolean ObjectSee.flag                            false
     13     3           (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 3 bytes external = 3 bytes total

jvm------------0x7a92922
util-----------0x7a92922
after hash
com.rrc.util.ObjectSee object internals:
 OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
      0     4           (object header)                           01 22 29 a9 (00000001 00100010 00101001 10101001) (-1456922111)
      4     4           (object header)                           07 00 00 00 (00000111 00000000 00000000 00000000) (7)
      8     4           (object header)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
     12     1   boolean ObjectSee.flag                            false
     13     3           (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 3 bytes external = 3 bytes total

分析结果:

没有进行hashcode之前的对象头信息,可以看到的56bit没有值,打印完hashcode之后就有值了,为什 么是1-7B,不是0-6B呢?因为是小端存储

两行打印的hashcode结果相同,所以可以确定java对象头当中的mark work里面的后七个字节存储的是hashcode信息。

那么第一个字节当中的八位分别存的就是分带年龄、偏向锁信息,和对象状态,这个8bit分别表示的信息如下图(其实上图也有信息),这个图会随着对象状态改变而改变, 下图是无锁状态下

img

关于对象状态一共分为五种状态,分别是无锁、偏向锁、轻量锁、重量锁、GC标记,

那么2bit,如何能表示五种状 态(2bit最多只能表示4中状态分别是:00,01,10,11),

jvm做的比较好的是把偏向锁和无锁状态表示为同一个状态,然 后根据图中偏向锁的标识再去标识是无锁还是偏向锁状态。

从上面打印的信息可以看到无锁状态下的信息00000001(无锁不可偏向)。

Synchronized锁

从JDK6开始,就对synchronized的实现机制进行了较大调整,包括使用JDK5引进的CAS自旋之外,还增加了自适应的CAS自旋、锁消除、锁粗化、偏向锁、轻量级锁这些优化策略。

在 JDK 1.6 中默认是开启偏向锁的。

可以通过-XX:-UseBiasedLocking来禁用偏向锁。使用-XX:-UseSpinning参数关闭自旋锁优化;-XX:PreBlockSpin参数修改默认的自旋次数。
偏向锁:无实际竞争,且将来只有第一个申请锁的线程会使用锁。
轻量级锁:无实际竞争,多个线程交替使用锁;允许短时间的锁竞争。
重量级锁:有实际竞争,且锁竞争时间长。

偏向锁默认配置

work@call-center-portal-v1-77466db6dc-cbw8d:~$ java -XX:+PrintFlagsFinal -version | grep  "BiasedLocking"
     intx BiasedLockingBulkRebiasThreshold          = 20                                  {product}
     intx BiasedLockingBulkRevokeThreshold          = 40                                  {product}
     intx BiasedLockingDecayTime                    = 25000                               {product}
     intx BiasedLockingStartupDelay                 = 4000                                {product}
     bool TraceBiasedLocking                        = false                               {product}
     bool UseBiasedLocking                          = true                                {product}
openjdk version "1.8.0_222"
OpenJDK Runtime Environment (build 1.8.0_222-b10)
OpenJDK 64-Bit Server VM (build 25.222-b10, mixed mode)
work@call-center-portal-v1-77466db6dc-cbw8d:~$

如上:

BiasedLockingStartupDelay值为4000毫秒,因为JDK1.8 的偏向锁默认延迟4s生效的

UseBiasedLocking=true,表示程序默认开启偏向锁。如果设置为false则程序默认会进入轻量级锁。

-XX:+PrintSafepointStatistics可打印安全点事件,与偏向锁有关的可重点可关注EnableBiasedLocking,RevokeBias和BulkRevokeBias。

选项-XX:+TraceBiasedLocking可以帮助生成一个详细描述jvm做出的偏向锁决策的日志。

JDK1.8锁升级与降级

接下来我们写一个偏向锁的例子看结果。

package com.rrc.util;

import org.openjdk.jol.info.ClassLayout;

public class ObjectSee {
    private boolean flag;

    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        ObjectSee b = new ObjectSee();

        System.out.println("befor lock");
        System.out.println(ClassLayout.parseInstance(b).toPrintable());
        synchronized (b){
            System.out.println("lock ing");
            System.out.println(ClassLayout.parseInstance(b).toPrintable());
        }
        System.out.println("after lock");
        System.out.println(ClassLayout.parseInstance(b).toPrintable());
    }
}

运行结果为:

com.rrc.util.ObjectSee 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)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
     12     1   boolean ObjectSee.flag                            false
     13     3           (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 3 bytes external = 3 bytes total

lock ing
com.rrc.util.ObjectSee object internals:
 OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
      0     4           (object header)                           90 b9 7a 0c (10010000 10111001 01111010 00001100) (209369488)
      4     4           (object header)                           03 00 00 00 (00000011 00000000 00000000 00000000) (3)
      8     4           (object header)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
     12     1   boolean ObjectSee.flag                            false
     13     3           (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 3 bytes external = 3 bytes total

after lock
com.rrc.util.ObjectSee 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)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
     12     1   boolean ObjectSee.flag                            false
     13     3           (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 3 bytes external = 3 bytes total

上面这个程序只有一个线程去调用sync方法,故而讲道理应该是偏向锁,但是此时却是轻量级锁。

而且你会发现最后输出的结果依然是00000001和无锁的时候一模一样,其实这是因为虚拟机在启动的时候对于偏向锁有延迟。

此时我们选择关闭JDK1.8的默认偏向锁延迟(-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0)

此时运行结果如下:

befor lock
com.rrc.util.ObjectSee 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)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
     12     1   boolean ObjectSee.flag                            false
     13     3           (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 3 bytes external = 3 bytes total

lock ing
com.rrc.util.ObjectSee object internals:
 OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
      0     4           (object header)                           05 18 01 e4 (00000101 00011000 00000001 11100100) (-469690363)
      4     4           (object header)                           c9 7f 00 00 (11001001 01111111 00000000 00000000) (32713)
      8     4           (object header)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
     12     1   boolean ObjectSee.flag                            false
     13     3           (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 3 bytes external = 3 bytes total

after lock
com.rrc.util.ObjectSee object internals:
 OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
      0     4           (object header)                           05 18 01 e4 (00000101 00011000 00000001 11100100) (-469690363)
      4     4           (object header)                           c9 7f 00 00 (11001001 01111111 00000000 00000000) (32713)
      8     4           (object header)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
     12     1   boolean ObjectSee.flag                            false
     13     3           (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 3 bytes external = 3 bytes total

此时的00000101标识可偏向已锁定 。

延迟偏向是因为JVM启动的时候会有很多操作,运行时存在大量的同步方法,很多都不是偏向锁,而偏向锁升级为轻/重量级锁的很费时间和资源,因此jvm会延迟4秒左右再开启偏向锁。

需要注意的after lock,退出同步后依然保持了偏向信息(Mark Word中保存有线程ID)。

偏向锁使用了一种等到竞争出现才释放锁的机制,即一个线程在执行完同步代码块以后,并不会尝试将MarkWord中的thread ID赋回原值。这样做的好处是:如果该线程需要再次对这个对象加锁,而这个对象之前一直没有被其他线程尝试获取过锁,依旧停留在可偏向的状态下,即可在不修改对象头的情况下,直接认为偏向成功。

轻量级锁(JVM options:-XX:-UseBiasedLocking)

package com.rrc.util;

import org.openjdk.jol.info.ClassLayout;

public class ObjectSee {
    private static ObjectSee a;

    public static void main(String[] args) throws Exception {
        a = new ObjectSee();
        System.out.println("befre lock");
        System.out.println(ClassLayout.parseInstance(a).toPrintable());
        synchronized (a){
            System.out.println("lock ing");
            System.out.println(ClassLayout.parseInstance(a).toPrintable());
        }
        System.out.println("after lock");
        System.out.println(ClassLayout.parseInstance(a).toPrintable());
    }
}

运行结果为:

befre lock
com.rrc.util.ObjectSee 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)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

lock ing
com.rrc.util.ObjectSee object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           98 59 b6 09 (10011000 01011001 10110110 00001001) (162945432)
      4     4        (object header)                           03 00 00 00 (00000011 00000000 00000000 00000000) (3)
      8     4        (object header)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

after lock
com.rrc.util.ObjectSee 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)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

在关闭偏向锁后,使用synchronized加锁后直接升级为轻量级锁。

偏向锁的撤销(Revoke) 操作并不是将对象恢复到无锁可偏向的状态,而是指在获取偏向锁的过程因为不满足条件导致要将锁对象改为非偏向锁状态;释放是指退出同步块时的过程,即将内存最低的对应的lock Record的obj置为null,需要注意撤销与释放的区别。 偏向锁的撤销,是轻量级锁的前提。

需要注意的after lock,退出同步后回到了无锁的状态(Mark Word中线程ID消失)。

重量级锁(JVM options:-XX:-UseBiasedLocking):

package com.rrc.util;

import org.openjdk.jol.info.ClassLayout;

public class ObjectSee {
    private static ObjectSee a;

    public static void main(String[] args) throws Exception {
        //Thread.sleep(5000);
        a = new ObjectSee();
        System.out.println("befre lock");
        System.out.println(ClassLayout.parseInstance(a).toPrintable());//无锁

        Thread t1= new Thread(){
            public void run() {
                synchronized (a){
                    try {
                        Thread.sleep(5000);
                        System.out.println("t1 release");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        t1.start();
        Thread.sleep(1000);
        System.out.println("t1 lock ing");
        System.out.println(ClassLayout.parseInstance(a).toPrintable());//轻量锁
        sync();
        System.out.println("after lock");
        System.out.println(ClassLayout.parseInstance(a).toPrintable());//重量锁
        System.gc();
        System.out.println("after gc()");
        System.out.println(ClassLayout.parseInstance(a).toPrintable());//无锁---gc
    }

    public  static  void sync() throws InterruptedException {
        synchronized (a){
            System.out.println("t1 main lock");
            System.out.println(ClassLayout.parseInstance(a).toPrintable());//重量锁
        }
    }
}

运行结果如下:

befre lock
com.rrc.util.ObjectSee 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)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t1 lock ing
com.rrc.util.ObjectSee object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           b0 d9 d7 09 (10110000 11011001 11010111 00001001) (165140912)
      4     4        (object header)                           03 00 00 00 (00000011 00000000 00000000 00000000) (3)
      8     4        (object header)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t1 release
t1 main lock
com.rrc.util.ObjectSee object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           4a 7b 81 21 (01001010 01111011 10000001 00100001) (562133834)
      4     4        (object header)                           8f 7f 00 00 (10001111 01111111 00000000 00000000) (32655)
      8     4        (object header)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

after lock
com.rrc.util.ObjectSee object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           4a 7b 81 21 (01001010 01111011 10000001 00100001) (562133834)
      4     4        (object header)                           8f 7f 00 00 (10001111 01111111 00000000 00000000) (32655)
      8     4        (object header)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

after gc()
com.rrc.util.ObjectSee object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           09 00 00 00 (00001001 00000000 00000000 00000000) (9)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

after lock之后依然是重量级锁,但是经过gc之后变成了无锁状态。Hotspot 在 1.8 开始有了锁降级。在 STW 期间 JVM 进入安全点时如果发现有闲置的 monitor(重量级锁对象),会进行锁降级。

由上述实验可总结下图:

img

批量重偏向和批量撤销

当只有一个线程反复进入同步块时,偏向锁带来的性能开销基本可以忽略,但是当有其他线程尝试获得锁时,就需要等到safe point时将偏向锁撤销为无锁状态或升级为轻量级/重量级锁。safe point这个词我们在GC中经常会提到,其代表了一个状态,在该状态下所有线程都是暂停的。总之,偏向锁的撤销是有一定成本的,如果说运行时的场景本身存在多线程竞争的,那偏向锁的存在不仅不能提高性能,而且会导致性能下降。因此,JVM中增加了一种批量重偏向/撤销的机制。

1 批量重偏向锁:当对某个类的对象偏向锁批量撤销20次,则偏向锁认为,后面的锁需要重新偏向新的线程(批量重偏向)

2 批量撤销:当某个类的对象的偏向锁累计被撤销到阈值40次(从40次开始),则偏向锁认为偏向锁撤销过于频繁,则后面的对象包括新生成的对象(标识为101和001)如果需要使用锁,则直接轻量级锁,不在使用偏向锁(即禁用了偏向锁)

存在如下两种情况:

  1. 一个线程创建了大量对象并执行了初始的同步操作,之后在另一个线程中将这些对象作为锁进行之后的操作。这种case下,会导致大量的偏向锁撤销操作。
  2. 存在明显多线程竞争的场景下使用偏向锁是不合适的,例如生产者/消费者队列。

批量重偏向(bulk rebias)机制是为了解决第一种场景。批量撤销(bulk revoke)则是为了解决第二种场景。

其做法是:以class为单位,为每个class维护一个偏向锁撤销计数器,每一次该class的对象发生偏向撤销操作时,该计数器+1,当这个值达到重偏向阈值(默认20,jvm参数BiasedLockingBulkRebiasThreshold控制)时,JVM就认为该class的偏向锁有问题,因此会进行批量重偏向。

当达到重偏向阈值后,假设该class计数器继续增长,当其达到批量撤销的阈值后(默认40,jvm参数BiasedLockingBulkRevokeThreshold控制),JVM就认为该class的使用场景存在多线程竞争,执行批量撤销,会标记该class为不可偏向,之后,对于该class的锁,直接走轻量级锁的逻辑。

BiasedLockingDecayTime是开启一次新的批量重偏向距离上次批量重偏向的后的延迟时间,默认25000。也就是开启批量重偏向后,如果经过了一段较长的时间(>=BiasedLockingDecayTime),撤销计数器才超过阈值,那我们会重置计数器。

批量撤销相关DEMO如下:

package com.rrc.util;


import org.openjdk.jol.info.ClassLayout;


import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.LockSupport;


/**
 * Hello world!
 * -XX:+PrintFlagsFinal
 * -Xms1g -Xmx1g  -XX:+PrintGCDetails -XX:BiasedLockingStartupDelay=0 偏向延迟关闭参数
 */


public class Test4 {
    static Thread t1 = null;
    static Thread t2 = null;
    static Thread t3 = null;


    static int count = 39;//39 则正好是经历了,40次偏向锁撤销,以后新创建的对象为无锁不可偏向标识,那加锁则直接为轻量级锁(撤销了偏向锁这个步骤)


    public static void main(String[] args) throws InterruptedException {


//        System.out.println(String.format(" 新对象锁标识:%s ", ClassLayout.parseInstance(new B()).toPrintable()));
        B b2 = new B();
        //保存锁对象列表
        List<B> list = new ArrayList<>();
        //第一个线程 加锁了38次
        t1 = new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < count; i++) {
                    B b = new B();
                    list.add(b);
                    System.out.println(String.format("线程名称 %s 执行的次数 %d ", Thread.currentThread().getName(), i));
                    System.out.println(ClassLayout.parseInstance(b).toPrintable());
                    synchronized (b) {
                        //打印第一个线程加锁后 ,对象头变化
                        System.out.println(ClassLayout.parseInstance(b).toPrintable());
                    }
                    System.out.println(ClassLayout.parseInstance(b).toPrintable());
                }
                LockSupport.unpark(t2);
            }
        };


        t2 = new Thread() {
            @Override
            public void run() {
                LockSupport.park();
                System.out.println("线程2开始执行======");
                for (int i = 0; i < count; i++) {
                    System.out.println(String.format("线程名称 %s 执行的次数 %d ", Thread.currentThread().getName(), i));
                    B b = list.get(i);
                    System.out.println(ClassLayout.parseInstance(b).toPrintable());
                    synchronized (list.get(i)) {
                        //打印第二个线程对对象加锁,对象头变化(线程前20次为偏向锁升级轻量级锁,从20次开始偏向锁偏向线程t2,发生线程重偏向)
                        System.out.println(ClassLayout.parseInstance(b).toPrintable());
                    }
                    System.out.println(ClassLayout.parseInstance(b).toPrintable());
                }
                LockSupport.unpark(t3);
            }
        };


        t3 = new Thread(() -> {
            LockSupport.park();
            System.out.println("线程3开始执行========================");
            for (int i = 0; i < count; i++) {
                System.out.println(String.format("线程名称 %s 执行的次数 %d ", Thread.currentThread().getName(), i));
                B b = list.get(i);
                System.out.println(ClassLayout.parseInstance(b).toPrintable());
                synchronized (b) {
                    //线程从20个开始进行偏向锁撤销直到发生撤销40次到达阈值,则后面的对象发生 偏向锁 批量撤销
                    System.out.println(ClassLayout.parseInstance(b).toPrintable());
                }
                System.out.println(ClassLayout.parseInstance(b).toPrintable());
            }
        });


        t1.setName("线程t1 ");
        t2.setName("线程t2 ");
        t3.setName("线程t3 ");


        t1.start();
        t2.start();
        t3.start();
        t1.join();
        t2.join();
        t3.join();


        System.out.println("= 主线程新====================");
        //发生批量撤销后线程加锁,转换为轻量级锁
        System.out.println(String.format(" 新对象锁标识:%s ", ClassLayout.parseInstance(b2).toPrintable()));
        synchronized (b2) {
            System.out.println(String.format(" 新对象锁标识:%s ", ClassLayout.parseInstance(b2).toPrintable()));
        }
    }
}

class B {
    
}

部分运行结果如下:

线程t1

count为39次,for循环进行39次(0-38,线程t1最后一次运行结果如下,最开始对象为可偏向无锁状态,进入同步代码块后Mark Word中塞入了线程ID(a8 03 7a bb 7f 00 00),退出同步代码块后依然保持了偏向信息(Mark Word中保存有线程ID)。

count为39,
  线程
线程名称 线程t1  执行的次数 38 
com.rrc.util.B 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)                           05 c2 00 f8 (00000101 11000010 00000000 11111000) (-134168059)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

com.rrc.util.B object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 a8 03 7a (00000101 10101000 00000011 01111010) (2047059973)
      4     4        (object header)                           bb 7f 00 00 (10111011 01111111 00000000 00000000) (32699)
      8     4        (object header)                           05 c2 00 f8 (00000101 11000010 00000000 11111000) (-134168059)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

com.rrc.util.B object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 a8 03 7a (00000101 10101000 00000011 01111010) (2047059973)
      4     4        (object header)                           bb 7f 00 00 (10111011 01111111 00000000 00000000) (32699)
      8     4        (object header)                           05 c2 00 f8 (00000101 11000010 00000000 11111000) (-134168059)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

线程t2

可以看到进入同步代码块后Mark Word中塞入了t2的线程ID(d9 00 0e 03 00 00 00),锁升级为轻量级锁,在退出同步代码块后锁状态变成不可偏向无锁,Mark Word中线程ID清除。

线程名称 线程t2  执行的次数 0 
com.rrc.util.B object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 a8 03 7a (00000101 10101000 00000011 01111010) (2047059973)
      4     4        (object header)                           bb 7f 00 00 (10111011 01111111 00000000 00000000) (32699)
      8     4        (object header)                           05 c2 00 f8 (00000101 11000010 00000000 11111000) (-134168059)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

com.rrc.util.B object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           a8 d9 00 0e (10101000 11011001 00000000 00001110) (234936744)
      4     4        (object header)                           03 00 00 00 (00000011 00000000 00000000 00000000) (3)
      8     4        (object header)                           05 c2 00 f8 (00000101 11000010 00000000 11111000) (-134168059)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

com.rrc.util.B 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)                           05 c2 00 f8 (00000101 11000010 00000000 11111000) (-134168059)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

线程t2执行到第19次时,符合默认的批量重偏向的条件,从对象头中可以看出,加锁后锁依然为偏向锁,但是已不再偏向t1线程。但是为什么对象头中的线程ID与前18次的线程ID不一致呢?

线程名称 线程t2  执行的次数 19 
com.rrc.util.B object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 a8 03 7a (00000101 10101000 00000011 01111010) (2047059973)
      4     4        (object header)                           bb 7f 00 00 (10111011 01111111 00000000 00000000) (32699)
      8     4        (object header)                           05 c2 00 f8 (00000101 11000010 00000000 11111000) (-134168059)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

com.rrc.util.B object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 a1 92 7a (00000101 10100001 10010010 01111010) (2056429829)
      4     4        (object header)                           bb 7f 00 00 (10111011 01111111 00000000 00000000) (32699)
      8     4        (object header)                           05 c2 00 f8 (00000101 11000010 00000000 11111000) (-134168059)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

com.rrc.util.B object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 a1 92 7a (00000101 10100001 10010010 01111010) (2056429829)
      4     4        (object header)                           bb 7f 00 00 (10111011 01111111 00000000 00000000) (32699)
      8     4        (object header)                           05 c2 00 f8 (00000101 11000010 00000000 11111000) (-134168059)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

线程t3执行完毕之后相当于对象b已经经历超过40次的偏向撤销,对应类B的新对象虽然为可偏向无锁定状态(Mark Word中没有线程ID),但是进入同步代码块后将直接变为轻量级锁。

= 主线程新====================
 新对象锁标识:com.rrc.util.B 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)                           05 c2 00 f8 (00000101 11000010 00000000 11111000) (-134168059)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
 
 新对象锁标识:com.rrc.util.B object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           88 e9 9b 0c (10001000 11101001 10011011 00001100) (211544456)
      4     4        (object header)                           03 00 00 00 (00000011 00000000 00000000 00000000) (3)
      8     4        (object header)                           05 c2 00 f8 (00000101 11000010 00000000 11111000) (-134168059)
     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对象头信息和三种锁的性能对比

synchronized原理和锁优化策略(偏向/轻量级/重量级)

Synchronized 锁 批量重偏向 和批量撤销

分析对象内部结构,并详解synchronized锁膨胀升级和降级的过程

java中关于锁知识的整理

synchronized原理


文章作者: WangQingLei
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 WangQingLei !
  目录