Java 中的锁 synchronized

Java Lock juc 大约 21379 字

锁的状态

无锁状态、偏向锁、轻量级锁、重量级锁。

偏向锁加锁过程

  1. 访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01,确认为可偏向状态。
  2. 如果为可偏向状态,则判断Mark Word中记录的线程ID是否指向当前线程,如果是,进入步骤(5),否则进入步骤(3)。
  3. 如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行(5);如果竞争失败,执行(4)。
  4. 如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。
  5. 执行同步代码。

偏向锁的释放

偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为001)或轻量级锁(标志位为000)的状态。

轻量级锁加锁过程

  1. 代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为001状态),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(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的锁标志位设置为000,即表示此对象处于轻量级锁定状态。
  5. 如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要升级为重量级锁,锁标志的状态值变为010Mark Word中存储的就是指向重量级锁的指针,后面等待锁的线程也要进入阻塞状态。而当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程。

轻量级锁的释放

  1. 通过CAS操作尝试把线程中复制的Displaced Mark Word对象替换当前的Mark Word
  2. 如果替换成功,整个同步过程就完成了。
  3. 如果替换失败,说明有其他线程尝试过获取该锁(此时锁已升级),那就要在释放锁的同时,唤醒被挂起的线程。

Mark Word 示意图

mark word.jpg

锁标志位

根据Mark Word示意图可知,最后三位确定锁的状态:

  • 无锁状态:001
  • 偏向锁:101
  • 轻量级锁:000
  • 重量级锁:010

jol

使用OpenJDK提供的jol分析对象的布局,包括:Mark Word,类型压缩、实例数据、内存填充。

Object lock1 = new Object();
System.out.println("lock1 hashcodeHex#" + Integer.toHexString(lock1.hashCode()));
System.out.println(ClassLayout.parseInstance(lock1).toPrintable());

输出:

lock1 hashcodeHex#1540e19d
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 9d e1 40 (00000001 10011101 11100001 01000000) (1088527617)
      4     4        (object header)                           15 00 00 00 (00010101 00000000 00000000 00000000) (21)
      8     4        (object header)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

注意

由于大多数CPU都是小端存储,所以存储后打印顺序是反向的。

大端模式(Big-endian):是指数据的高字节,保存在内存的低地址中,而数据的低字节,保存在内存的高地址中,这样的存储模式有点儿类似于把数据当作字符串顺序处理,地址由小向大增加,而数据从高位往低位放。

小端模式(Little-endian):是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内在的低地址中,这样的存储模式将地址的高低和数据位权有效结合起来,高地址部分权值高,低地址部分权值低。

十六进制应该是这样的:1540e19d,二进制应该是这样的:(后8位为分代年龄、锁标志位等其他信息)

00000000 00000000 00000000 00010101 01000000 11100001 10011101 00000001

但是,实际十六进制哈希值是这样的:9de14015,二进制实际是这样的:

00000001 10011101 11100001 01000000 00010101 00000000 00000000 00000000

对象布局

示意图

object layout.jpg

说明

普通对象在开启类指针压缩后对象头:前8Mark Word信息,后4位为类指针。

数组对象在开启类指针压缩后对象头:前8Mark Word信息,中间4为类指针,后4位为数组长度。

JDK1.8后默认开启了类指针压缩,可使用-XX:-UseCompressedClassPointers关闭。

示例代码

public static void classLayout() {
    Object obj = new Object();
    System.out.println("obj hashcodeHex#" + Integer.toHexString(obj.hashCode()));
    System.out.println(ClassLayout.parseInstance(obj).toPrintable());

    System.out.println("----------------------------------------------------------");

    int[] arr = new int[10];
    System.out.println("arr hashcodeHex#" + Integer.toHexString(Arrays.hashCode(arr)));
    System.out.println(ClassLayout.parseInstance(arr).toPrintable());
}

开启类指针压缩

JDK1.8默认开启了类指针压缩

obj hashcodeHex#1540e19d
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 9d e1 40 (00000001 10011101 11100001 01000000) (1088527617)
      4     4        (object header)                           15 00 00 00 (00010101 00000000 00000000 00000000) (21)
      8     4        (object header)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

----------------------------------------------------------
arr hashcodeHex#94e4b2c1
[I 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)                           6d 01 00 20 (01101101 00000001 00000000 00100000) (536871277)
     12     4        (object header)                           0a 00 00 00 (00001010 00000000 00000000 00000000) (10)
     16    40    int [I.<elements>                             N/A
Instance size: 56 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

关闭类指针压缩

启动时添加:-XX:-UseCompressedClassPointers

obj hashcodeHex#1540e19d
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 9d e1 40 (00000001 10011101 11100001 01000000) (1088527617)
      4     4        (object header)                           15 00 00 00 (00010101 00000000 00000000 00000000) (21)
      8     4        (object header)                           00 1c a8 17 (00000000 00011100 10101000 00010111) (396893184)
     12     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

----------------------------------------------------------
arr hashcodeHex#94e4b2c1
[I 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)                           68 0b a8 17 (01101000 00001011 10101000 00010111) (396888936)
     12     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
     16     4        (object header)                           0a 00 00 00 (00001010 00000000 00000000 00000000) (10)
     20     4        (alignment/padding gap)                  
     24    40    int [I.<elements>                             N/A
Instance size: 64 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total

偏向锁

适应只有一个线程执行的场景,如出现了两个线程不管是交替执行还是同时竞争,偏向锁都将升级为重量级锁。

在同步代码块(synchronized代码块)调用hashCode方法后(重写后的hashCode方法不影响),hashCode会写进头信息中,偏向锁也升级为轻量级锁。

在同步代码块(synchronized代码块)调用hashCode方法后(重写后的hashCode方法不影响),hashCode会写进头信息中,偏向锁也升级为重量级锁。

偏向锁的演示需等程序启动5秒后,或者添加jvm参数:-XX:BiasedLockingStartupDelay=0,表示偏向锁启动延迟时间为0秒。

JDK1.6JVM默认开启偏向锁,如需禁用偏向锁可添加参数:-XX:-UseBiasedLocking

示例代码

被注释掉的线程A'打开注释后,两个线程争夺lock1锁,偏向锁也就会升级为重量锁。

public static void biasedLock() throws InterruptedException {
    // 等待偏向锁启动
    TimeUnit.SECONDS.sleep(5);
    System.out.println("开始演示...");

    Object lock1 = new Object() {
        @Override
        public int hashCode() {
            return 123456;
        }
    };
    Object lock2 = new Object();
    Object lock3 = new Object();

    new Thread(() -> {
        synchronized (lock1) {
            System.out.println("lock1 Thread id#" + Thread.currentThread().getId() + ", hashCode#" + Thread.currentThread().hashCode() + ", hashCodeHex#" + Integer.toHexString(Thread.currentThread().hashCode()));
            System.out.println(ClassLayout.parseInstance(lock1).toPrintable() + "\n##########################################");
        }
    }, "A").start();

    /*new Thread(() -> {
        try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
        synchronized (lock1) {
            System.out.println("lock1' Thread id#" + Thread.currentThread().getId() + ", hashCode#" + Thread.currentThread().hashCode() + ", hashCodeHex#" + Integer.toHexString(Thread.currentThread().hashCode()));
            System.out.println(ClassLayout.parseInstance(lock1).toPrintable() + "\n##########################################");
        }
    }, "A'").start();*/

    new Thread(() -> {
        synchronized (lock2) {
            System.out.println("lock2 hashcodeHex#" + Integer.toHexString(lock2.hashCode()));
            System.out.println(ClassLayout.parseInstance(lock2).toPrintable() + "\n------------------------------------------");
        }
    }, "B").start();

    System.out.println("lock3 hashcodeHex#" + Integer.toHexString(lock3.hashCode()));
    new Thread(() -> {
        synchronized (lock3) {
            System.out.println(ClassLayout.parseInstance(lock3).toPrintable() + "\n******************************************");
        }
    }, "C").start();
}

头信息

可以看到:

  • 线程A没有其他线程竞争锁lock1,锁的标志位为101(偏向锁)
  • 线程B在同步代码块内调用了hashCode,偏向锁升级为重量级锁,锁的标志位为010(重量级锁)
  • 线程C在同步代码块外调用了hashCode,偏向锁升级为轻量级锁,锁的标志位为000(轻量级锁)
  • 若打开注释掉的线程A',则偏向锁lock1将升级为重量级锁
开始演示...
lock3 hashcodeHex#404b9385
lock2 hashcodeHex#795b7822
lock1 Thread id#12, hashCode#396953361, hashCodeHex#17a90711
lock.SynchronizedDemo$1 object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 00 d9 19 (00000101 00000000 11011001 00011001) (433651717)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           43 c1 00 20 (01000011 11000001 00000000 00100000) (536920387)
     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)                           4a 34 ad 17 (01001010 00110100 10101101 00010111) (397227082)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
     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)                           10 f0 2a 1b (00010000 11110000 00101010 00011011) (455798800)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

******************************************

疑惑

很多博客上写到偏向锁的前54位表示线程id,从jol打印的线程id来看,两者并不相等,希望有大佬能留言赐教。

轻量级锁

不同线程交替执行。如果上锁线程未执行完成,又有线程来抢锁,则轻量级锁将升级为重量级锁。

使用CAS自旋锁方式上锁。

示例代码

public static void lightweightLock() {
    Object lock = new Object();
    System.out.println("Thread id#" + Thread.currentThread().getId());
    System.out.println(ClassLayout.parseInstance(lock).toPrintable());
    System.out.println("----------------------------------------------");

    new Thread(() -> {
        synchronized (lock) {
            System.out.println(ClassLayout.parseInstance(lock).toPrintable() + "\n##########################################");
        }
    }, "A").start();

    new Thread(() -> {
        try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }

        synchronized (lock) {
            System.out.println(ClassLayout.parseInstance(lock).toPrintable() + "\n##########################################");
        }
    }, "B").start();

    new Thread(() -> {
        try { TimeUnit.SECONDS.sleep(4); } catch (InterruptedException e) { e.printStackTrace(); }

        synchronized (lock) {
            System.out.println(ClassLayout.parseInstance(lock).toPrintable() + "\n##########################################");
        }
    }, "C").start();

    new Thread(() -> {
        try { TimeUnit.SECONDS.sleep(4); } catch (InterruptedException e) { e.printStackTrace(); }

        synchronized (lock) {
            System.out.println(ClassLayout.parseInstance(lock).toPrintable() + "\n++++++++++++++++++++++++++++++++++++++++++");
        }
    }, "D").start();
}

头信息

可以看到:

  • 未上锁前的锁标志位为001(无锁状态)
  • 线程A执行时的锁标志位为000(轻量级锁)
  • 两秒后线程B在线程A释放了锁的前提下执行(即:交替执行)时的锁标志位为000(轻量级锁)
  • 四秒后线程C和线程D同时启动竞争锁,锁由轻量级升级为重量级,此时线程C和线程D的锁标志位都为010(重量级锁)
Thread id#1
java.lang.Object 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)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
     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)                           b0 ef ac 1b (10110000 11101111 10101100 00011011) (464318384)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
     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)                           b0 f2 bc 1b (10110000 11110010 10111100 00011011) (465367728)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
     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)                           0a e8 51 03 (00001010 11101000 01010001 00000011) (55699466)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
     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)                           0a e8 51 03 (00001010 11101000 01010001 00000011) (55699466)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

++++++++++++++++++++++++++++++++++++++++++

重量级锁

不同线程竞争同一资源。

效率低下原因:对象内部的一个叫做监视器锁(monitor)来实现的,但是监视器锁本质又是依赖于底层的操作系统的Mutex Lock来实现的,而操作系统实现线程之间的切换这就需要从用户态转换到内核态,这个成本非常高,状态之间的转换需要相对较长的时间。

示例代码

public static void heavyweightLock() {
    Object lock = new Object();
    System.out.println("Thread id#" + Thread.currentThread().getId());
    System.out.println("lock hashcode#" + Integer.toHexString(lock.hashCode()));
    System.out.println(ClassLayout.parseInstance(lock).toPrintable());
    System.out.println("----------------------------------------------");

    new Thread(() -> {
        synchronized (lock) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(ClassLayout.parseInstance(lock).toPrintable());
            System.out.println("##########################################");
        }
    }).start();

    new Thread(() -> {
        synchronized (lock) {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(ClassLayout.parseInstance(lock).toPrintable());
        }
    }).start();
}

头信息

Thread id#1
lock hashcode#1540e19d
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 9d e1 40 (00000001 10011101 11100001 01000000) (1088527617)
      4     4        (object header)                           15 00 00 00 (00010101 00000000 00000000 00000000) (21)
      8     4        (object header)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
     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)                           ba 34 f5 17 (10111010 00110100 11110101 00010111) (401945786)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
     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)                           ba 34 f5 17 (10111010 00110100 11110101 00010111) (401945786)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

其他优化

适应性自旋 Adaptive Spinning

从轻量级锁获取的流程中我们知道,当线程在获取轻量级锁的过程中执行CAS操作失败时,是要通过自旋来获取重量级锁的。问题在于,自旋是需要消耗CPU的,如果一直获取不到锁的话,那该线程就一直处在自旋状态,白白浪费CPU资源。解决这个问题最简单的办法就是指定自旋的次数,例如让其循环10次,如果还没获取到锁就进入阻塞状态。但是JDK采用了更聪明的方式——适应性自旋,简单来说就是线程如果自旋成功了,则下次自旋的次数会更多,如果自旋失败了,则自旋的次数就会减少。

锁粗化 Lock Coarsening

将多次连接在一起的加锁、解锁操作合并为一次,将多个连续的锁扩展成一个范围更大的锁。

示例代码:

public void test() {
    Object lock = new Object();
    synchronized (lock) {
        System.out.println("11111111");
    }
    synchronized (lock) {
        System.out.println("22222222");
    }
    synchronized (lock) {
        System.out.println("33333333");
    }
}

锁粗化后:

public void test() {
    Object lock = new Object();
    synchronized (lock) {
        System.out.println("11111111");
        System.out.println("22222222");
        System.out.println("33333333");
    }
}

锁消除 Lock Elimination

锁消除即删除不必要的加锁操作。根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程,那么可以认为这段代码是线程安全的,不必要加锁。

示例代码:

public class LockEliminationDemo {

    public void append(String str1, String str2) {
        StringBuffer stringBuffer = new StringBuffer();
        stringBuffer.append(str1).append(str2);
    }

    public static void main(String[] args) {
        LockEliminationDemo lockEliminationDemo = new LockEliminationDemo();

        long startTs = System.currentTimeMillis();
        for (int i = 0; i < 100000000; i++) {
            lockEliminationDemo.append("a", "b");
        }
        System.out.println(System.currentTimeMillis() - startTs);
    }

}

输出如下,可以看到JDK1.8是默认开启锁消除的,关闭锁消除后执行时间上升1.5倍:

PS D:\src> java LockEliminationDemo
1552
PS D:\src> java -XX:-EliminateLocks LockEliminationDemo
4448
PS D:\src> java -XX:-EliminateLocks LockEliminationDemo
4457
PS D:\src> java -XX:+EliminateLocks LockEliminationDemo
1553
PS D:\src> java -XX:+EliminateLocks LockEliminationDemo
1541

参考

https://www.oracle.com/technetwork/java/javase/tech/biasedlocking-oopsla2006-preso-150106.pdf

https://www.cnblogs.com/paddix/p/5405678.html

阅读 37 · 发布于 2021-04-04

————        END        ————

扫描下方二维码关注公众号和小程序↓↓↓

扫描二维码关注我
昵称:
随便看看 换一批