年底了,最近好几天没吃饭了,在微博吃瓜吃的饱饱的。
续上次被问到synchronized锁后,面试官继续刁难阿巴阿巴,进而深入到对象头中相关的概念。
当场拿offer
面试官: 上次提到了synchronized锁,那你知道synchronized锁具体是怎么实现的吗?
阿巴阿巴: 在JDK版本1.5及之前的版本synchronized主要靠的是Monitor对象来完成,同步代码块使用的是monitorenter和monitorexit指令,而synchronized修饰方法靠的是ACC_SYNCHRONIZED标识,这些都是进入到内核态进行加锁的,然后将竞争锁失败的线程直接挂起,等待后面恢复。
阿巴阿巴: 在JDK1.6及之后的版本中,synchronized锁得到了优化,引入了自适应自旋锁、偏向锁、轻量锁,他们主要优化了锁在一定条件下的性能。避免了一上来就加重量级锁,等待锁的其他线程只能乖乖挂起,对cpu性能影响特别大。
阿巴阿巴: 在hotspot虚拟机中,对象头主要包括两部分 MarkWord和Klass Pointer。
MarkWord 对象标记字段,默认存储的是对象的HashCode,GC的分代年龄(2bit最大表示15)和锁的标志信息等。对于32位的虚拟机MarkWord占32bit,对于64位的虚拟机MarkWord占用64字节。
Klass Pointer Class 对象的类型指针,它指向对象对应的Class对象的内存地址。大小占4字节(指针压缩的情况下为4字节,未进行指针压缩则占8字节)。32位虚拟机MarkWord分布
64位虚拟机MarkWord分布
图片来源https://blog.csdn.net/weixin_40816843/article/details/120811181
查看虚拟机是多少位的可以使用:java -version
面试官: 我们怎么看对象头里的MarkWord数据呢?
阿巴阿巴: 可以看到在openJDK中关于MarkWord的描述,首先可以在Github上找到Open Jdk的源码
gitHub地址:https://github.com/openjdk/jdk
在IDE中打开并找到如下的位置
src/hotspot/share/oops/markWord.hpp
复制
// 查看虚拟机是多少位的可以使用:java -version // 32 bits: // -------- // hash:25 ------------>| age:4 unused_gap:1 lock:2 (normal object) // // 64 bits: // -------- // unused:25 hash:31 -->| unused_gap:1 age:4 unused_gap:1 lock:2 (normal object)
1.
2.
3.
4.
5.
6.
7.
8.
阿巴阿巴: 当然可以引入openjdk提供的jol-core,然后进行打印即可。
复制
// 在pom中引入 <dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> <version>0.10</version> </dependency>
1.
2.
3.
4.
5.
6.
然后编写如下代码
复制
public static void main(String[] args) { Test t = new Test(); System.out.println(ClassLayout.parseInstance(t).toPrintable()); }
1.
2.
3.
4.
打印如下
markword在哪?Klass pointer在哪儿?
1处是MarkWord占用8Byte也就是64bit
2处是Klass Pointer占用了4Byte也就是32bit
klass pointer看起来是被压缩了,怎么确定是被压缩了呢?可以通过如下命令
面试官: 对于JDK1.6及以上版本,synchronized和MarkWord有啥关系嘛?
阿巴阿巴: 那关系可大了,可以看到在MarkWord中有2bit用来表示锁的标志位,代表着经过优化的synchronized锁不会直接上重量级锁,而是由偏向锁转为轻量锁,再由轻量锁转为重量级锁,一步一步膨胀的过程。
下面是2bit的锁标志位代表的含义
复制
// [ptr | 00] locked ptr points to real header on stack // [header | 01] unlocked regular object header // [ptr | 10] monitor inflated lock (header is wapped out) // [ptr | 11] marked used to mark an object // [0 ............ 0| 00] inflating inflation in progress 001 无锁状态 (第一位代表偏向标志,为0的时候表示不偏向,为1的时候表示偏向) 101 偏向锁 且记录线程ID 00 轻量锁 指向栈中锁记录的指针 10 重量级锁 重量级锁的指针 11 GC标志
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
然后再找到上图Value部分的数据,这两位是锁的标志位
面试官: 你刚不是说有一位是锁的偏向标志吗?在哪儿呢?
阿巴阿巴: 锁的偏向标志就在锁标志的前一位
阿巴阿巴: 程序启动后4s就会加偏向锁,只不过这个偏向锁没有偏向任何线程ID,也属于无锁状态
阿巴阿巴: 当应用处于单线程环境中时,这时候上的是偏向锁,在对象头中偏向标示显示为1,案例如下
复制
public static void main(String[] args) { Test t = new Test(); new Thread(()->{ synchronized (t) { System.out.println(ClassLayout.parseInstance(t).toPrintable()); } }).start(); }
1.
2.
3.
4.
5.
6.
7.
8.
打印出来的数据如下
阿巴阿巴: 让程序处于2个线程交替进行竞争锁
复制
public static void main(String[] args) throws InterruptedException { Test t = new Test(); Thread thread = new Thread(()->{ synchronized (t) { System.out.println(ClassLayout.parseInstance(t).toPrintable()); } }); thread.start(); // 等待thread运行完 thread.join(); synchronized (t) { System.out.println(ClassLayout.parseInstance(t).toPrintable()); } }
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
可以看到当main线程拿锁时已经膨胀为轻量锁了,锁的2bit标志为变成00了
阿巴阿巴: 轻量锁的时候,虚拟机会在当前线程的栈帧中建立一个锁记录的空间“Lock Record”,用于存储锁对象目前的MarkWord的拷贝,这一步采用CAS,如果成功了,那么与此同时,2bit的锁标记位会从“01”转变为“00”。这就是加轻量锁的过程。
阿巴阿巴: 之所以引入偏向锁,是为了解决在无多线程竞争环境下的轻量锁,轻量锁CAS多次的尝试也是对性能的损耗。相对于轻量锁而言,偏向锁值只需要进行一次CAS,这次CAS是用来设置线程ID的,设置成功后就代表获取锁了。轻量锁更适合于线程交替执行的场景,它们通过CAS自旋,避免了线程直接挂起以及挂起后的恢复过程,以此来降低CPU的损耗。
阿巴阿巴: 最后让我们看看加上重量锁后的MarkWord表现吧,先上代码
复制
public static void main(String[] args) throws InterruptedException { Test t = new Test(); Thread thread = new Thread(()->{ synchronized (t) { System.out.println(ClassLayout.parseInstance(t).toPrintable()); } }); thread.start(); // 等待thread运行完 // thread.join(); 去掉该代码 synchronized (t) { System.out.println(ClassLayout.parseInstance(t).toPrintable()); } }
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
控制台打印如下,发现已经加上重量锁了,锁的2bit标志为变成10了。
阿巴阿巴: 当轻量级锁升级成重量级锁时,Mark Word的锁标记位更新为10,Mark Word 将指向互斥量(重量级锁)。
阿巴阿巴: 以上就是关于synchronized和MarkWord的关系啦。
面试官: 理解的不错,明天来上班吧~
阿巴阿巴: 好的~