前言
这个面试题其实涉及到的底层知识比较多,在Java中都知道synchronized,这是一个关键字,为什么使用了之后,可以结果多线程安全问题。里面内部流程是怎样的呢?加锁是加在哪里?金三银四越来越卷,面试官不再是,单纯的问如何解决线程安全,有没有使用过synchronized,而是想知道synchronized底层的知识点。本文就深入讲解synchronized底层原理,对象加锁是如果一步一步实现的。
synchronized加锁加在哪里?
synchronized可以使用两种方式进行加锁,一个同步代码块,另一种同步方法,其实都是针对对象进行加锁的。synchronized,是一个指令,解析成monitener,然后jvm去执行,实际改变对象头信息。所以要想知道synchronized原理需要先知道java对象头,改变的是对象头的什么信息。
对象头信息
JVM已经规定对象头的内容openjdk官网也说了,包括:
可以存放堆对象的布局、类型、gc状态、同步状态、标识的哈希值。有两个词组成
mark word:64bit(读取方式从反方向读)
klass pointer/class metadata address:类模块数据地址,指向元空间,32bit(有个是64bit,没有指针压缩)
c 源码中指出64位的jvm的mark word大小占64位,包括以下信息:
markword的结构,可以看下图:
可以看出:
hash:用31位来存储hash值,hash本身是不存在对象头的,只有调用hashcode时,调用native方法去计算生成,然后才放到对象头hash
age:GC分代年龄存了4位(新生区超过15次还没没回收就到老年区,设置只能1-15,原因是对象头gc状态,最大值记录在4四位的二进制上,也就是2的4次方,16。),biased_lock:偏向锁,1位
lock:锁状态,2位
biased_lock lock: 最后3位控制对象的5种状态
对象状态:无锁、偏向锁、轻量锁、重量锁、gc标记,只有锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。
synchronized锁的升级过程(锁/对象状态)
通过上述对象头介绍,应该清楚了,synchronized加锁主要改变的是对象头的信息,改变的是64位对象头,最后的三位。
无锁 001:无锁就是没有对任何资源进行锁定,所有线程都能访问并修改资源,
偏向锁 101:在锁对象的对象头中记录一下当前获取到该锁的线程ID,该线程下次如果又来获取该锁就可以直接获取到了,也就是支持锁重入。偏向锁作用,主要是解决可重入问题,当线程重复获取锁的时候,就判断该锁是否有线程ID
轻量级锁 000:当两个或者以上线程交替获取锁,当没有在对象上并发的获取锁时,偏向锁升级为轻量级锁。在此阶段,线程采取CAS的自旋锁方式尝试获取锁,避免阻塞线程造成的CPU在用户态和内核态间转换的消耗。
重量级锁 010:两个或者以上线程并发的在一个对象上进行同步时,为了避免无用自旋锁cpu,轻量级锁就会升级成重量级锁。
代码演示synchronized锁的升级过程
synchronized加锁,一把锁,在没有竞争的情况下,被同一个对象多次获取,所以没必要一直加锁操作,以此来减少CPU资源,所以就会导致加了锁,最后三位数还是000,接下来通过代码展示synchronized加锁这四种状态的对象头的改变情况。
首先,64位对象头查看需要依赖对应的插件,官网 https://mvnrepository.com/artifact/org.openjdk.jol/jol-core 对象头查看,只要用10.0的版本进行查看,就可以查看到二进制,在pom文件中加入对应依赖。
代码语言:xml复制
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>
无锁状态 001
即没有使用关键字synchronized,对象创建的时候。
代码语言:java复制 class ZPF {
boolean flag = false;
}
public class Main{
public static void main(String[] args) throws Exception {
ZPF a = new ZPF();
System.out.println("befor hash");
//没有计算HASHCODE之前的对象头
System.out.println(ClassLayout.parseInstance(a).toPrintable());
//JVM 计算的hashcode
System.out.println("jvm------------0x" Integer.toHexString(a.hashCode()));
//当计算完hashcode之后,我们可以查看对象头的信息变化
System.out.println("after hash");
System.out.println(ClassLayout.parseInstance(a).toPrintable());
}
}
可以看到对象头object header 有12byte也就是96bit,64bit是mark word,32bit是类模块数据地址
由于对象头读取方式逆序的,所以最终结果是跟官网指出的一样,最后三位是 001
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
偏向锁101
无并发线程竞争,加锁中的对象状态,就是偏向锁,如下代码:
代码语言:java复制// 偏向锁
public static void main(String[] args) throws InterruptedException {
Thread.sleep(5000);//虚拟机启动时对偏向锁有延时
ZPF a = new ZPF();
synchronized (a){
System.out.println("lock ing");
System.out.println(ClassLayout.parseInstance(a).toPrintable());
}
}
在这个例子中,加了Thread.sleep(5000)
时间之后,对象锁变成了偏向锁,不加的话就是轻量级锁。这是因为在没有加Thread.sleep(5000)
时,程序执行非常快,锁的状态会在偏向锁、轻量级锁和重量级锁之间快速切换。而加了Thread.sleep(5000)
后,程序执行时间足够长,使得锁的状态可以稳定在偏向锁上,最终结果如下
根据读取规则,最终64位对象头是,最后三位是 101
00000000 00000000 00000000 00000000 00000011 01101011 01000000 00000101
轻量级锁 000
主要演示无并发线程竞争是,对象锁的变化,这里主要区别是跟上述对象锁的对比,少了延时,synchronized加锁的时候,对象头锁状态就会变成轻量级锁。
代码语言:java复制static ZPF a;
public static void main(String[] args) throws Exception {
a = new ZPF();
System.out.println("befre lock");
System.out.println(ClassLayout.parseInstance(a).toPrintable());
sync();
System.out.println("after lock");
System.out.println(ClassLayout.parseInstance(a).toPrintable());
}
public static void sync() throws InterruptedException {
synchronized (a){
System.out.println("lock ing");
System.out.println(ClassLayout.parseInstance(a).toPrintable());
}
}
查看结果,加锁中,锁的对象状态是轻量级的
根据读取规则,最终64位对象头是,最后三位是 000
00000000 00000000 00000000 00000000 00000010 11000100 11110011 01000000
重量级锁 010
由于jdk优化之后synchronized加锁,不会立即升级成重量级锁,只有在多个线程并发,加锁,对象状态才会升级重量级锁。接下来模拟,多个线程抢占对象锁。
代码语言:java复制static ZPF a;
public static void main(String[] args) throws Exception {
a = new ZPF();
System.out.println("befre lock");
System.out.println(ClassLayout.parseInstance(a).toPrintable());//无锁
// 线程1,先占有锁,如果有其他线程过来抢占,就会升级重量级锁,没有其他线程抢占,就是轻量级锁
Thread t1 = new Thread(() -> {
synchronized (a) {
try {
Thread.sleep(1000);
System.out.println(ClassLayout.parseInstance(a).toPrintable());//重量锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
// 开启线程2,此时锁给线程1获取中
Thread t2 = new Thread(() -> {
synchronized (a) {
System.out.println(ClassLayout.parseInstance(a).toPrintable());//重量锁
}
});
t2.start();
}
查看结果,可以看到,对象头最后三位是010,已经变成了重量级锁了。
00000000 00000000 00000000 00000000 00011000 01011001 00011111 01111010
总结
对于java八股文,已经不局限于表层使用方法了,面试官更想知道的是,底层的知识,本文主要通过介绍同步锁synchronized 的原理,进而通过实际案例代码,将抽象的java对象信息展示出来,让面试者更加深入了解synchronized 底层原理,希望大家在金三银四找到好坑位。