youyichannel

志于道,据于德,依于仁,游于艺!

0%

Synchronized全解读-03

synchronized锁升级 —— 偏向锁

从 JDK 15 开始,这一特性被官方标记为废弃状态,如果还想继续使用的话需要通过 JVM 参数手动启用。

参考:https://zhuanlan.zhihu.com/p/365454004

偏向锁使用的前提:

  1. JDK1.6版本之后且开启了偏向锁配置。偏向锁在JDK6和JDK7中是默认开启的,但是它在应用程序启动几秒后才会激活,如有必要可以使用JVM参数-XX:BiasedLockingStartupDelay=0来关闭延迟。如果确定应用程序里所有的锁通常情况下处于竞争状态,可以通过JVM参数-XX:UseBiasedLocking=false关闭偏向锁,那么程序默认会进入轻量级锁状态。
  2. 被加锁的对象没有真正或者隐式调用父类Object#hashcode()方法

代码示例:

引入依赖:

><!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-core -->
><dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
></dependency>
public class SyncLockFlag {
static final MyObj obj = new MyObj();

public static void main(String[] args) {
System.out.println(">>>>>>>> 无锁");
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj) {
System.out.println(">>>>>>>> 偏向锁");
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
}

static class MyObj {

}
}

输出结果:

>>>>>>>> 无锁
com.juzi.juc.sync.SyncLockFlag$MyObj 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) 18 0a 06 00 (00011000 00001010 00000110 00000000) (395800)
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.juzi.juc.sync.SyncLockFlag$MyObj object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 30 6a 5f 6d (00110000 01101010 01011111 01101101) (1834969648)
4 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
8 4 (object header) 18 0a 06 00 (00011000 00001010 00000110 00000000) (395800)
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.juzi.juc.sync.SyncLockFlag$MyObj object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)

001:无锁状态,这只是一个单纯地无锁状态。

com.juzi.juc.sync.SyncLockFlag$MyObj object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 30 6a 5f 6d (00110000 01101010 01011111 01101101) (1834969648)

00:偏向锁状态,因为偏向锁启动有延迟。

加上JVM参数配置后,再跑一次结果:

>>>>>>>> 无锁
com.juzi.juc.sync.SyncLockFlag$MyObj 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) 18 0a 06 00 (00011000 00001010 00000110 00000000) (395800)
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.juzi.juc.sync.SyncLockFlag$MyObj object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 88 80 4e (00000101 10001000 10000000 01001110) (1317046277)
4 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
8 4 (object header) 18 0a 06 00 (00011000 00001010 00000110 00000000) (395800)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

无锁 - 101:轻量级锁,当开启了偏向锁并且没有延迟开启后,新创建对象的mark word默认就是偏向锁状态。只不过是此时因为没有线程争抢,除了锁标志位和是否为偏向锁标志位外,其余的位数都是0。=> 此时是 未偏向线程的偏向锁

偏向锁 - 101:除了101之外,还有其他的信息,即记录了线程ID、epoch、对象分代年龄信息。

修改代码:

public static void main(String[] args) {
System.out.println(">>>>>>>> 未偏向线程的偏向锁");
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
obj.hashCode(); // 调用hashcode
synchronized (obj) {
System.out.println(">>>>>>>> 偏向锁?");
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
}

输出结果:

>>>>>>>> 未偏向线程的偏向锁
com.juzi.juc.sync.SyncLockFlag$MyObj 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) 18 0a 06 00 (00011000 00001010 00000110 00000000) (395800)
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.juzi.juc.sync.SyncLockFlag$MyObj object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 30 6a 8f 6b (00110000 01101010 10001111 01101011) (1804560944)
4 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
8 4 (object header) 18 0a 06 00 (00011000 00001010 00000110 00000000) (395800)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

可以看到,在第二次输出中为00,轻量级锁状态。

之前提过偏向锁的前提之一就是:被加锁的对象没有真正或者隐式调用父类Object#hashcode()方法。

如果一旦调用Object#hashcode()方法,那么对象头中就有对象的hashcode值。如果偏向锁来进行markword替换,至少需要提供一个保存hashcode的位置,但是偏向锁并没有位置进行markword的保存,轻量级锁有display mark word

隐式调用说明:间接调用了hashcode()方法,比如HashMap#put()、重写了的equals()等等。

为了让线程获得锁的代价更低,引入了偏向锁。当前一个线程访问同步块并获取锁时,会在①对象头和②栈帧的锁记录中存储锁偏向的线程ID,之后该线程在进入和退出同步块时不需要使用CAS来加锁和解锁,只需要③判断对象头的Mark Word中是否存储着指向当前线程的偏向锁。如果匹配成功,表示该线程获取了锁;如果匹配失败,则需要再判断Mark Word中的偏向锁的表示是否设置成1(表示当前是偏向锁),如果没有设置,则④使用CAS竞争锁;如果设置了,则尝试使用CAS将⑤对象头的偏向锁指向当前线程

  • ①:存储线程ID
  • ②:线程栈帧的LOCK RECORD存储当前线程ID
  • ③:线程ID是否匹配
  • ④:认为是无锁状态,去竞争偏向锁
  • ⑤:本质上是CAS竞争替换线程ID

资料:https://www.bilibili.com/video/BV1Kb4y1u7vo/ 强推 河北王校长