浅析synchronized锁升级的原理与实现

发布时间 2023-07-11 14:04:29作者: 小新成长之路

背景

在多线程编程中,线程同步是一个关键的概念,它确保了多个线程对共享资源的安全访问。Java中的synchronized关键字是一种常用的线程同步机制,它不仅提供了互斥访问的功能,还具备锁升级的特性。本文将深入探讨synchronized的锁升级原理和实现方式。
在jdk1.5(包含)版本之前,因为加锁和释放锁的过程JVM的底层都是由操作系统mutex lock来实现的,其中会涉及上下文的切换(即用户态和内核态的转换),性能消耗极其高,所以在当时synchronized锁是公认的重量级锁。
后来JVM开发团队为解决性能问题,在jdk1.5版本中加入了JUC并发包,包下开发了很多Lock相关的锁,来解决同步的性能问题,同时也开始在后续的迭代版本中对synchronized锁不断的进行优化来提高性能,比如在jdk1.6版本中就引入了“偏向锁”和“轻量级锁”,通过锁的升级来解决不同并发场景下的性能问题。
通常用使用synchronized方式加锁影响性能,主要原因如下:

  1. 加锁解锁依赖JVM层的的额外操作完成。
  2. 重量级锁是通过操作系统对线程的挂起和恢复来实现,涉及内核态和用户态的切换

需要储备的知识:java对象的内存布局

注意:本文代码所使用的JDK版本是1.8,JVM虚拟机是64位的HotSpot实现为准。

锁的用法

synchronized是java的同步关键字,可以使共享资源串行的执行,避免多线程竞争导致的执行结果错误,使用方法有以下三种。

  1. 作用在类的普通方法(非静态方法)上,锁的是当前对象实例。
public synchronized void lockInstance() {
    System.out.println("锁的是当前对象实例");
}
  1. 作用在类的静态方法上,锁的是当前类class。
public synchronized static void lockClass() {
    System.out.println("锁的是当前类class");
}
  1. 作用在代码块上,锁的是指定的对象实例。
public void lockObject(Object obj) {
    synchronized (obj) {
        System.out.println("锁的是指定的对象实例obj");
    }
}

原理分析

通过以上的用法,我们可以看到synchronized使用起来很简单,那它究竟是怎么做到线程间互斥访问的呢,底层原理及实现是怎样的呢,接下来我们一一解答。
前一篇文章写了java对象的内存布局,里面有一个关于对象头Markword存储的内容表格,在synchronized锁的使用过程中就用到了,如下图所示。
image.png

锁的状态

在jdk1.5版本(包含)之前,锁的状态只有两种状态:“无锁状态”和“重量级锁状态”,只要有线程访问共享资源对象,则锁直接成为重量级锁,jdk1.6版本后,对synchronized锁进行了优化,新加了“偏向锁”和“轻量级锁”,用来减少上下文的切换以提高性能,所以锁就有了4种状态。

  1. 无锁

对于共享资源,不涉及多线程的竞争访问。

  1. 偏向锁

共享资源首次被访问时,JVM会对该共享资源对象做一些设置,比如将对象头中是否偏向锁标志位置为1,对象头中的线程ID设置为当前线程ID(注意:这里是操作系统的线程ID),后续当前线程再次访问这个共享资源时,会根据偏向锁标识跟线程ID进行比对是否相同,比对成功则直接获取到锁,进入临界区域(就是被锁保护,线程间只能串行访问的代码),这也是synchronized锁的可重入功能。

  1. 轻量级锁

当多个线程同时申请共享资源锁的访问时,这就产生了竞争,JVM会先尝试使用轻量级锁,以CAS方式来获取锁(一般就是自旋加锁,不阻塞线程采用循环等待的方式),成功则获取到锁,状态为轻量级锁,失败(达到一定的自旋次数还未成功)则锁升级到重量级锁。

  1. 重量级锁

如果共享资源锁已经被某个线程持有,此时是偏向锁状态,未释放锁前,再有其他线程来竞争时,则会升级到重量级锁,另外轻量级锁状态多线程竞争锁时,也会升级到重量级锁,重量级锁由操作系统来实现,所以性能消耗相对较高。
这4种级别的锁,在获取时性能消耗:重量级锁 > 轻量级锁 > 偏向锁 > 无锁。

锁升级

锁升级是针对于synchronized锁在不同竞争条件下的一种优化,根据锁在多线程中竞争的程度和状态,synchronized锁可在无锁、偏向锁、轻量级锁和重量级锁之间进行流转,以降低获取锁的成本,提高获取锁的性能。
通过下面这个命令,可以看到所有JVM参数的默认值。

java -XX:+PrintFlagsFinal -version

锁升级过程

  1. 当JVM启动后,一个共享资源对象直到有线程第一个访问时,这段时间内是处于无锁状态,对象头的Markword里偏向锁标识位是0,锁标识位是01。

image.png

  1. 从jdk1.6之后,JVM有两个默认参数是开启的,-XX:+UseBiasedLocking(表示启用偏向锁,想要关闭偏向锁,可添加JVM参数:-XX:-UseBiasedLocking),-XX:BiasedLockingStartupDelay=4000(表示JVM启动4秒后打开偏向锁,也可以自定义这个延迟时间,如果设置成0,那么JVM启动就打开偏向锁)。

当一个共享资源首次被某个线程访问时,锁就会从无锁状态升级到偏向锁状态,偏向锁会在Markword的偏向线程ID里存储当前线程的操作系统线程ID,偏向锁标识位是1,锁标识位是01。此后如果当前线程再次进入临界区域时,只比较这个偏向线程ID即可,这种情况是在只有一个线程访问的情况下,不再需要操作系统的重量级锁来切换上下文,提供程序的访问效率。
另外需要注意的是,由于硬件资源的不断升级,获取锁的成本随之下降,jdk15版本后默认关闭了偏向锁。
如果未开启偏向锁(或者在JVM偏向锁延迟时间之前)有线程访问共享资源则直接由无锁升级为轻量级锁,请看第3步。
image.png

  1. 如果未开启偏向锁(或者在JVM偏向锁延迟时间之前),有线程访问共享资源则直接由无锁升级为轻量级锁,开启偏向线程锁后,并且当前共享资源锁已经是偏向锁时,再有第二个线程访问共享资源锁时,此时锁可能升级为轻量级锁,也可能还是偏向锁状态,因为这取决于线程间的竞争情况,如有没有竞争,那么偏向锁的效率更高(因为频繁的锁竞争会导致偏向锁的撤销和升级到轻量级锁),继续保持偏向锁。如果有竞争,则锁状态会从偏向锁升级到轻量级锁,这种情况下轻量级锁效率会更高。

当第二个线程尝试获取偏向锁失败时,偏向锁会升级为轻量级锁,此时,JVM会使用CAS自旋操作来尝试获取锁,如果成功则进入临界区域,否则升级为重量级锁。
轻量级锁是在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,尝试拷贝锁对象头的Markword到栈帧的Lock Record,若拷贝成功,JVM将使用CAS操作尝试将对象头的Markword更新为指向Lock Record的指针,并将Lock Record里的owner指针指向对象头的Markword。若拷贝失败,若当前只有一个等待线程,则可通过自旋继续尝试, 当自旋超过一定的次数,或者一个线程在持有锁,一个线程在自旋,又有第三个线程来访问时,轻量级锁就会膨胀为重量级锁。
image.png

  1. 当轻量级锁获取锁失败时,说明有竞争存在,轻量级锁会升级为重量级锁,此时,JVM会将线程阻塞,直到获取到锁后才能进入临界区域,底层是通过操作系统的mutex lock来实现的,每个对象指向一个monitor对象,这个monitor对象在堆中与锁是关联的,通过monitorenter指令插入到同步代码块在编译后的开始位置,monitorexit指令插入到同步代码块的结束处和异常处,这两个指令配对出现。JVM的线程和操作系统的线程是对应的,重量级锁的Markword里存储的指针是这个monitor对象的地址,操作系统来控制内核态中的线程的阻塞和恢复,从而达到JVM线程的阻塞和恢复,涉及内核态和用户态的切换,影响性能,所以叫重量级锁。

image.png
锁升级简要步骤如下所示
image.png

注意:图中无锁到偏向锁这不是升级,是在偏向锁打开后,对象默认是偏向状态,没有从无锁升级到偏向锁的过程。偏向锁未开启,会直接从无锁升级到轻量级锁,偏向锁开启时,会从偏向锁升级到轻量级锁。

锁升级细化流程
image.png
下面我们结合代码看下各状态锁的升级场景
需要添加JOL包,用来查看对象头信息

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

无锁 --> 轻量级锁

无锁升级到轻量级锁有两种情况

  1. 第一种,关闭偏向锁,执行时增加JVM参数:-XX:-UseBiasedLocking
public void lockUpgradeTest1() {
    Object obj = new Object();
    System.out.println("未开启偏向锁,对象信息");
    System.out.println(ClassLayout.parseInstance(obj).toPrintable());
    synchronized (obj) {
        System.out.println("已获取到锁信息");
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());
    }
    System.out.println("已释放锁信息");
    System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}

运行结果:

未开启偏向锁,对象信息
java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

已获取到锁信息
java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000000336f2b0 (thin lock: 0x000000000336f2b0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

已释放锁信息
java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

采用JOL输出的对象头markword是16进制的,需要转换成64位的2进制来看。

关闭偏向锁的情况下,对象加锁之前,对象头markword是0x0000000000000001换算成二进制末尾三位是001,即偏向锁标识为0,锁标识为01,是无锁状态。
加锁成功后,执行同步代码块,对象头markword是0x000000000336f2b0换算成二进制末尾两位是00,即锁标识为00,是轻量级锁状态。
最后在执行完同步代码块后,再次打印对象头信息,对象头markword是0x0000000000000001换算成二进制末尾三位是001,即偏向锁标识为0,锁标识为01,是无锁状态,说明轻量级锁在执行完同步代码块后进行了锁的释放。

  1. 第二种,默认情况下,在偏向锁延迟时间之前获取锁
public void lockUpgradeTest2() {
    Object obj = new Object();
    System.out.println("开启偏向锁,偏向锁延迟时间前,对象信息");
    System.out.println(ClassLayout.parseInstance(obj).toPrintable());
    synchronized (obj) {
        System.out.println("已获取到锁信息");
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());
    }
    System.out.println("开启偏向锁,已释放锁信息");
    System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}

运行结果:

开启偏向锁,偏向锁延迟时间前,对象信息
java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

已获取到锁信息
java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000000316f390 (thin lock: 0x000000000316f390)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

开启偏向锁,已释放锁信息
java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

使用默认的偏向锁配置,JVM启动4秒后才启动偏向锁,所以JVM启动时就打印并获取锁信息,效果跟第一种一样,markword解释同上。

偏向锁 --> 轻量级锁

public void lockUpgradeTest3() {
    // JVM默认4秒后才可以偏向锁,所以这里休眠5秒,锁对象就是偏向锁了
    try {
        Thread.sleep(5000);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
    Object object = new Object();
    Thread t1 = new Thread(() -> {
        System.out.println(Thread.currentThread().getName() + "开启偏向锁,偏向锁延迟时间后,对象信息" + ClassLayout.parseInstance(object).toPrintable());
        synchronized (object) {
            System.out.println(Thread.currentThread().getName() + "已获取到锁信息" + ClassLayout.parseInstance(object).toPrintable());
        }
        System.out.println(Thread.currentThread().getName() + "开启偏向锁,已释放锁信息" + ClassLayout.parseInstance(object).toPrintable());
    }, "t1");
    t1.start();
    try {
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
    Thread t2 = new Thread(() -> {
        System.out.println(Thread.currentThread().getName() + "开启偏向锁,偏向锁延迟时间后,对象信息" + ClassLayout.parseInstance(object).toPrintable());
        synchronized (object) {
            System.out.println(Thread.currentThread().getName() + "已获取到锁信息" + ClassLayout.parseInstance(object).toPrintable());
        }
        System.out.println(Thread.currentThread().getName() + "开启偏向锁,已释放锁信息" + ClassLayout.parseInstance(object).toPrintable());
    }, "t2");
    t2.start();
}

运行结果有两种可能:
第一种:

t1开启偏向锁,偏向锁延迟时间后,对象信息java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000005 (biasable; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t1已获取到锁信息java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000001fbb3005 (biased: 0x000000000007eecc; epoch: 0; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t1开启偏向锁,已释放锁信息java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000001fbb3005 (biased: 0x000000000007eecc; epoch: 0; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t2开启偏向锁,偏向锁延迟时间后,对象信息java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000001fbb3005 (biased: 0x000000000007eecc; epoch: 0; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t2已获取到锁信息java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000020f7f2d0 (thin lock: 0x0000000020f7f2d0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t2开启偏向锁,已释放锁信息java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

启动JVM,默认4秒后开启偏向锁,这里休眠了5秒,保证JVM开启偏向锁,然后创建了对象,对象头markword信息0x0000000000000005换算成二进制后三位是101,偏向锁标识为1,锁标识为01,为偏向锁状态,偏向线程ID是0,说明这是初始偏向状态,t1先获取到锁进入同步代码块后,markword变成0x000000001fbb3005转换成二进制:11111101110110011000000000101(前面补0直到长度是64位),末尾三位依然是101,还是偏向锁,只不过前54位将对应的操作系统线程ID写到偏向线程ID里了,同步代码块执行完成后,markword依然没变,说明偏向锁状态不会自动释放锁,需要等其他线程来竞争锁才走偏向锁撤销流程。t2线程开始执行时锁对象markword是0x000000001fbb3005,说明偏向锁偏向了t1对应的操作系统线程,等t1释放锁,t2获取到锁进入同步代码块时,对象锁markword是0x0000000020f7f2d0,换算成二进制:100000111101111111001011010000(前面补0直到长度是64位),末尾两位是00,锁已经变成轻量级锁了,锁的指针也变了,是指向t2线程栈中的Lock Record记录了,等t2线程释放锁后,对象锁末尾是001,说明是无锁状态了,轻量级锁会自动释放锁。
第二种:

t1开启偏向锁,偏向锁延迟时间后,对象信息java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000005 (biasable; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t1已获取到锁信息java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000002031d805 (biased: 0x0000000000080c76; epoch: 0; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t1开启偏向锁,已释放锁信息java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000002031d805 (biased: 0x0000000000080c76; epoch: 0; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t2开启偏向锁,偏向锁延迟时间后,对象信息java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000002031d805 (biased: 0x0000000000080c76; epoch: 0; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t2已获取到锁信息java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000002031d805 (biased: 0x0000000000080c76; epoch: 0; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t2开启偏向锁,已释放锁信息java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000002031d805 (biased: 0x0000000000080c76; epoch: 0; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t1线程正常获取锁,锁状态是偏向锁,执行完同步代码块后锁还是偏向锁,说明偏向锁不随执行同步代码块的结束而释放锁,t2线程拿到锁是偏向锁,获取到锁依然是偏向锁,而没有升级到轻量级锁,说明线程间锁没有竞争的情况下,依然保持偏向锁,这样效率会更高。

偏向锁 --> 重量级锁

public void lockUpgradeTest4() {
    // JVM默认4秒后才可以偏向锁,所以这里休眠5秒,锁对象就是偏向锁了
    try {
        Thread.sleep(5000);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
    Object object = new Object();
    Thread t1 = new Thread(() -> {
        System.out.println(Thread.currentThread().getName() + "加锁前对象信息" + ClassLayout.parseInstance(object).toPrintable());
        synchronized (object) {
            System.out.println(Thread.currentThread().getName() + "已获取到锁信息" + ClassLayout.parseInstance(object).toPrintable());
            try {
                // 让t2线程启动后并竞争锁
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        System.out.println(Thread.currentThread().getName() + "已释放锁信息" + ClassLayout.parseInstance(object).toPrintable());
    }, "t1");
    t1.start();
    try {
        // 让t1线程先启动并拿到锁
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
    Thread t2 = new Thread(() -> {
        System.out.println(Thread.currentThread().getName() + "加锁前对象信息" + ClassLayout.parseInstance(object).toPrintable());
        synchronized (object) {
            System.out.println(Thread.currentThread().getName() + "已获取到锁信息" + ClassLayout.parseInstance(object).toPrintable());
        }
        System.out.println(Thread.currentThread().getName() + "已释放锁信息" + ClassLayout.parseInstance(object).toPrintable());
    }, "t2");
    t2.start();
}

运行结果:

t1加锁前对象信息java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000005 (biasable; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t1已获取到锁信息java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000020993805 (biased: 0x000000000008264e; epoch: 0; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t2加锁前对象信息java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000020993805 (biased: 0x000000000008264e; epoch: 0; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t2已获取到锁信息java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000001d2c6c2a (fat lock: 0x000000001d2c6c2a)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t1已释放锁信息java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000001d2c6c2a (fat lock: 0x000000001d2c6c2a)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t2已释放锁信息java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

程序先休眠5秒保证偏向锁开启,然后t1线程先启动并成功获取到锁,t1获取到锁之前对象markword是偏向状态但偏向线程ID是0,t1获取到锁之后markword里有了偏向线程ID,也就是t1线程对应的操作系统线程ID。t2线程获取锁之前,对象锁已经是偏向锁并偏向t1对应的线程,t2线程获取锁时t1已经持有锁并没有释放,锁未释放其他线程再竞争锁,这时会发生锁升级,由偏向锁升级成重量级锁,所以t1释放锁跟t2获取到锁时,对象头的markword是0x000000001d2c6c2a,转换成二进制11101001011000110110000101010(前后补0到够64位),最后两位是10,标识重量级锁,前面的62存的是指向堆中跟monitor对应锁对象的指针。

轻量级锁 --> 重量级锁

public void lockUpgradeTest5() {
        // JVM默认4秒后才可以偏向锁,所以这里休眠5秒,锁对象就是偏向锁了
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        Object object = new Object();
        Thread t1 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "加锁前对象信息" + ClassLayout.parseInstance(object).toPrintable());
            synchronized (object) {
                System.out.println(Thread.currentThread().getName() + "已获取到锁信息" + ClassLayout.parseInstance(object).toPrintable());
                try {
                    // 让t2线程启动后并竞争锁
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println(Thread.currentThread().getName() + "已释放锁信息" + ClassLayout.parseInstance(object).toPrintable());
        }, "t1");
        t1.start();
        try {
            // 让t1线程先启动并拿到锁
            t1.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        Thread t2 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "加锁前对象信息" + ClassLayout.parseInstance(object).toPrintable());
            synchronized (object) {
                System.out.println(Thread.currentThread().getName() + "已获取到锁信息" + ClassLayout.parseInstance(object).toPrintable());
            }
            System.out.println(Thread.currentThread().getName() + "已释放锁信息" + ClassLayout.parseInstance(object).toPrintable());
        }, "t2");
        t2.start();
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "加锁前对象信息" + ClassLayout.parseInstance(object).toPrintable());
                synchronized (object) {
                    System.out.println(Thread.currentThread().getName() + "已获取到锁信息" + ClassLayout.parseInstance(object).toPrintable());
                }
                System.out.println(Thread.currentThread().getName() + "已释放锁信息" + ClassLayout.parseInstance(object).toPrintable());
            }, "t3_" + i).start();
        }

    }

运行结果:

注意:这里t2线程也有可能获取到的锁是偏向锁,无竞争的情况下,这取决于线程的执行情况。这里我们以t2获取到轻量级锁,讲解轻量级锁升级到重量级锁的过程。

t1加锁前对象信息java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000005 (biasable; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t1已获取到锁信息java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000020e1b805 (biased: 0x000000000008386e; epoch: 0; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t1已释放锁信息java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000020e1b805 (biased: 0x000000000008386e; epoch: 0; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t2加锁前对象信息java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000020e1b805 (biased: 0x000000000008386e; epoch: 0; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t3_0加锁前对象信息java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000020e1b805 (biased: 0x000000000008386e; epoch: 0; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t3_1加锁前对象信息java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000020e1b805 (biased: 0x000000000008386e; epoch: 0; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t2已获取到锁信息java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000002270f1d0 (thin lock: 0x000000002270f1d0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t3_2加锁前对象信息java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000002270f1d0 (thin lock: 0x000000002270f1d0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t3_1已获取到锁信息java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000001d0356da (fat lock: 0x000000001d0356da)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t2已释放锁信息java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000001d0356da (fat lock: 0x000000001d0356da)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t3_1已释放锁信息java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000001d0356da (fat lock: 0x000000001d0356da)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t3_0已获取到锁信息java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000001d0356da (fat lock: 0x000000001d0356da)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t3_0已释放锁信息java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000001d0356da (fat lock: 0x000000001d0356da)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t3_2已获取到锁信息java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000001d0356da (fat lock: 0x000000001d0356da)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t3_2已释放锁信息java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000001d0356da (fat lock: 0x000000001d0356da)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t1线程加锁执行代码块后,锁状态是偏向锁,t1在同步代码块里让休眠了3秒目的是让t2线程起来并竞争锁,然后t1线程执行完同步代码块,锁状态还是偏向锁,这时候for循环的3个线程也启动起来争抢所,t2线程先启动获取到锁为轻量级锁,for循环里启动的3个线程在获取同步锁前,我们看到打印的锁状态有的是偏向锁、有的是轻量级锁,说明在t2线程加锁成功前还是偏向锁,t2加锁后就成轻量级锁了,然后for循环的3个线程相继获取到锁,发现锁已经升级到重量级锁了,对象头markword是0x000000001d0356da,换成二进制:11101000000110101011011011010(前面补齐0到够64位),末尾两位锁状态是10,表示重量级锁。

底层实现

本文开头讲的synchronized在代码层的用法有三种,锁对象实例、锁类class、锁指定实例对象,我们可以将以下代码编译成class后,在反编译出来看看JVM指令码是怎样的。

public class Synchronized1 {
    public static void main(String[] args) {
        System.out.println("test Synchronized1");
    }
    public synchronized void lockInstance() {
        System.out.println("锁的是当前对象实例");
    }

    public synchronized static void lockClass() {
        System.out.println("锁的是当前类class");
    }

    public void lockObject(Object obj) {
        synchronized (obj) {
            System.out.println("锁的是指定的对象实例obj");
        }
    }
}

通过javap命令反编译class文件。
image.png
我本文的例子使用的命令是这样的:

javap -c -v -l Synchronized1.class

我们主要关注那3个方法的JVM指令码。
image.png

image.png

image.png
在方法(非静态方法锁的是对象,静态方法锁的是类class)上加synchronized关键字,是通过在access_flags中设置ACC_SYNCHRONIZED标志来实现,synchronized使用在代码块上,是通过monitorenter和monitorexit指令来实现。
重量锁底层最终是依靠操作系统的阻塞和唤醒来实现,每个对象有一个监视器锁(monitor),在 Java 虚拟机(HotSpot)中,monitor 是基于 C++的ObjectMonitor实现,对象锁里有计数器、重入次数、等待锁的线程列表、存储该monitor的对象、拥有monitor的线程等参数,虚拟机是通过进入和退出monitor来实现同步,monitorenter指令是在编译后插入到同步代码块的开始位置,monitorexit是插入到方法结束处和异常处。根据虚拟机规范的要求,在执行monitorenter指令时,首先要去尝试获取对象的锁,如果这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加1;相应地,在执行monitorexit指令时会将锁计数器减1,当计数器被减到0时,锁就释放了(注意执行monitorexit的线程必须是已经获得monitor对象锁的线程)。如果获取对象锁失败了,那当前线程就要阻塞等待,直到对象锁被另一个线程释放然后由操作系统唤醒等到锁的线程继续竞争锁直到获取到锁为止。

总结

synchronized关键字是Java中常用的线程同步机制,其具备锁升级的特性,可以根据竞争的程度和锁的状态进行自动切换。锁升级通过无锁、偏向锁、轻量级锁和重量级锁四种状态的转换,以提高并发性能。在实际开发中,我们应该了解锁升级的原理,并根据具体场景进行合理的锁设计和优化,以实现高效且安全的多线程编程。
随着jdk版本的升级,JVM底层的实现持续优化,版本的不同伴随着参数使用及默认配置的不同,但总之JVM层对synchronized的优化效率越来越高,所以不应该再把synchronized同步当重量级锁来看。
其实本文介绍了锁升级的主要过程,关于synchronized还有锁消除、锁粗化的优化手段,使得synchronized性能在某些场景应用下,可能会比JUC包底下的Lock相关锁效率更高。
另外synchronized锁原理、优化、使用远不止本文说的这么多,感兴趣的可进一步探索。