在Java的并发编程领域中,我们进行会使用到锁这个东西,例如在多线程环境下为了预防某些线程安全问题,这里面可能会产生一些预想不到的问题,所以下边我整理了一系列关于JDK中锁的问题,帮助大家更加深入地了解它们。
synchronized真的是重量级锁嘛?
这个问题相信大部分人在面试的时候都有遇到过,答案是否定的。这个要看JDK的版本来进行判断。如果JDK的版本在1.5之前使用synchronized
锁的原理大概如下:
-
给需要加锁的资源前后分别加入一条“
monitorenter
”和“monitorexit
”指令。 -
当线程需要进入这个代码临界区的时候,先去参与“抢锁”(本质是获取monitor的权限)
-
抢锁如果失败,就会被阻塞,此时控制权只能交给操作系统,也就会从
user mode
切换到kernel mode
, 由操作系统来负责线程间的调度和线程的状态变更, 需要频繁的在这两个模式下切换(上下文转换)。
可以看出,老模式的条件下去获取锁的开销是比较大的,所以后来JDK的作者才会在JDK中设计了Lock接口,采用CAS的方式来实现锁,从而提升性能。
但是当竞争非常激烈的时候,采用CAS的方式有可能会一直获取不到锁的话,不管进行再多的CAS也是在浪费CPU,这种状态下的性能损耗会比synchronized
还要高。所以这类情况下,不如直接升级加锁的方式,让操作系统介入。
正因为这样,所以后边才会有了锁升级的说法。
synchronized的锁升级
偏向锁
在synchronized
进行升级的过程中,第一步会升级为偏向锁。所谓偏向锁,它的本质就是让锁来记住请求的线程。
在大多数场景下,其实都是单线程访问锁的情况偏多,JDK的作者在重构synchronized
的时候,给对象头设计了一个bit位,专门用于记录锁的信息,具体我们可以通过下边这个实际案例来认识下:
public static void main(String[] args) throws InterruptedException {
Object o = new Object();
System.out.println("还没有进入到同步块");
System.out.println("markword:" + ClassLayout.parseInstance(o).toPrintable());
//默认JVM启动会有一个预热阶段,所以默认不会开启偏向锁
Thread.sleep(5000);
Object b = new Object();
System.out.println("还没有进入到同步块");
System.out.println("markword:" + ClassLayout.parseInstance(b).toPrintable());
synchronized (o){
System.out.println("进入到了同步块");
System.out.println("markword:" + ClassLayout.parseInstance(o).toPrintable());
}
}
注意要引入一些第三方的依赖,辅助我们查看对象头的信息:
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
//这个版本号的不同,查看的内容格式也不同
<version>0.16</version>
</dependency>
控制台输出的结果如下:
还没有进入到同步块
# WARNING: Unable to attach Serviceability Agent. You can try again with escalated privileges. Two options: a) use -Djol.tryWithSudo=true to try with sudo; b) echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
markword: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
还没有进入到同步块
markword: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
进入到了同步块
markword:java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x00007000050ee988 (thin lock: 0x00007000050ee988)
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秒之后),就会变成一个biasable的状态。如果需要调整这个延迟的时间,可以通过参数 -XX:BiasedLockingStartupDelay=0
来控制。
这里我解释下biasable
的含义:
biasable是JVM帮我们设置的状态,在这种状态下,一旦有线程访问锁,就会直接CAS修改对象头中的线程id。如果成功,则直接升级为偏向锁。否则就会进入到锁的下一个状态--轻量级锁。
ps:JVM因为在启动预热的阶段中,会有很多步骤使用到synchronized
,所以在刚启动的前4秒中,不会直接将synchronized
锁的标记升级为biasable
状态。这是为了较少一些无必要的性能损耗。
轻量级锁
当锁被一个线程访问的时候,它会变成偏向锁的状态,那么当新的线程再次访问该锁的时候,锁会有什么变化吗?
这里我整理了一