//System.out.println都加了锁
public void println(String x) {
synchronized (this) {
print(x);
newLine();
}
}
简单加锁发生了什么?
要弄清楚加锁之后到底发生了什么需要看一下对象创建之后再内存中的布局是个什么样的?
一个对象在 new 出来之后在内存中主要分为 4 个部分:
知道了这 4 个部分之后,我们来验证一下底层。借助于第三方包 JOL = Java Object Layout java 内存布局去看看。很简单的几行代码就可以看到内存布局的样式:
<!-- 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 JOLDemo {
private static Object o;
public static void main(String[] args) {
o = new Object();
synchronized (o){
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
}
将结果打印出来:
从输出结果看:
探讨锁的升级之前,先做个实验。两份代码,不同之处在于一个中途让它睡了5秒,一个没睡。看看是否有区别。
public class JOLDemo {
private static Object o;
public static void main(String[] args) {
o = new Object();
synchronized (o){
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
}
----------------------------------------------------------------------------------------------
public class JOLDemo {
private static Object o;
public static void main(String[] args) {
try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); }
o = new Object();
synchronized (o){
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
}
这两份代码会不会有什么区别?运行之后看看结果:
有点意思的是,让主线程睡了 5s 之后输出的内存布局跟没睡的输出结果居然不一样。Syn 锁升级之后,jdk1.8 版本的一个底层默认设置 4s 之后偏向锁开启。也就是说在 4s 内是没有开启偏向锁的,加了锁就直接升级为轻量级锁了。
那么这里就有几个问题了?
问题 1:为什么要进行锁升级?锁了就锁了,不就要加锁么?
首先明确 syn 锁 在 jdk1.2 之前效率非常低。那时候 syn 就是重量级锁,申请锁必须要经过操作系统老大 kernel 进行系统调用,入队进行排序操作,操作完之后再返回给用户态。
内核态:用户态如果要做一些比较危险的操作直接访问硬件,很容易把硬件搞死(格式化,访问网卡,访问内存干掉等),操作系统为了系统安全分成两层:用户态和内核态。申请锁资源的时候用户态要向操作系统老大内核态申请。Jdk1.2 的时候用户需要跟内核态申请锁,然后内核态还会给用户态。这个过程是非常消耗时间的,导致早期效率特别低。有些 jvm 就可以处理的为什么还交给操作系统做去呢?能不能把 jvm 就可以完成的锁操作拉取出来提升效率,所以也就有了锁优化。
问题 2:为什么要有偏向锁?
其实这本质上归根于一个概率问题,统计表示,在我们日常用的 syn 锁过程中 70%-80% 的情况下,一般都只有一个线程去拿锁,例如我们常使用的 System.out.println、StringBuffer,虽然底层加了 syn 锁,但是基本没有多线程竞争的情况。那么这种情况下,没有必要升级到轻量级锁级别了。
偏向的意义在于:第一个线程拿到锁,将自己的线程信息标记在锁上,下次进来就不需要在拿去拿锁验证了。如果超过 1 个线程去抢锁,那么偏向锁就会撤销,升级为轻量级锁,其实我认为严格意义上来讲偏向锁并不算一把真正的锁,因为只有一个线程去访问共享资源的时候才会有偏向锁这个情况。
问题 3:为什么 jdk8 要在 4s 后开启偏向锁?
其实这是一个妥协,明确知道在刚开始执行代码时,一定有好多线程来抢锁,如果开了偏向锁效率反而降低,所以上面程序在睡了 5s 之后偏向锁才开放。为什么加偏向锁效率会降低,因为中途多了几个额外的过程,上了偏向锁之后多个线程争抢共享资源的时候要进行锁升级到轻量级锁,这个过程还的把偏向锁进行撤销在进行升级,所以导致效率会降低。为什么是 4s?这是一个统计的时间值。
当然我们是可以禁止偏向锁的,通过配置参数 -XX:-UseBiasedLocking = false 来禁用偏向锁。jdk15 之后默认已经禁用了偏向锁。本文是在 jdk8 的环境下做的锁升级验证。
上面已经验证了对象从创建出来之后进内存从无锁状态->偏向锁(如果开启了)->轻量级锁的过程。对于锁升级的流程继续往下,轻量级锁之后就会变成重量级锁。首先我们先理解什么叫做轻量级锁,从一个线程抢占资源(偏向锁)到多线程抢占资源升级为轻量级锁,线程如果没那么多的话,其实这里就可以理解为 CAS(Compare and Swap:比较并交换值)。
问题 4:什么情况下轻量级锁要升级为重量级锁呢?
首先我们可以思考的是多个线程的时候先开启轻量级锁,如果它 carry 不了的情况下才会升级为重量级。那么什么情况下轻量级锁会 carry 不住?
总的来说,两种情况都会从轻量级升级为重量级,10 次自旋或等待 cpu 调度的线程数超过 cpu 核数的一半,自动升级为重量级锁。整个锁升级过程如图所示:
问题 5:都说 syn 为重量级锁,那么到底重在哪里?
JVM 偷懒把任何跟线程有关的操作全部交给操作系统去做,例如调度锁的同步直接交给操作系统去执行,而在操作系统中要执行先要入队,另外操作系统启动一个线程时需要消耗很多资源,消耗资源比较重,重就重在这里。
原文链接: