文章目录
前序
大家应该都知道,JAVA存在自适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等一系列的锁优化措施,且网上很多博客和文章都有提到偏向锁,轻量锁及重量锁都和对象头中的Mark Word有关,那么该如何证明其真实性呢?(对理论有兴趣的读者可以看笔者这篇文章《JAVA内存模型,线程安全及锁优化》)
这里还是先简单介绍一下JAVA对象头的相关知识:HotSpot虚拟机的对象头分为两部分,第一部分称作Mark Word,用于存储HashCode,GC分代年龄等自身运行时数据,长度根据虚拟机位数可能为32bit或64bit,是研究JAVA锁优化的关键。第二部分称作Klass Word,存储对象类型数据的指针。
图一:32位系统Mark Word:
图二:64位系统Mark Word:
准备JOL包
如果使用JAVA web项目测试,笔者建议去阿里的maven仓库下载JOL包,https://maven.aliyun.com/mvn/search。
如果使用Maven项目测试,直接在pom.xml中加入如下配置
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol‐core</artifactId>
<version>0.9</version>
</dependency>
大小端模式
百度百科有,这里笔者不详细介绍,由于笔者的操作系统为小端模式,读取对象头打印结果时正确读法为从末尾到开始每8个字节一读,例:
二进制:00000001 10110100 11000101 00000111
对应十六进制:07 c5 b4 01
证明Hash值只有计算后才会存储到Mark Word中
public class JOLTest1 {
public static void main(String[] args) throws Exception {
JOLTest1 a = new JOLTest1();
System.out.println(ClassLayout.parseInstance(a).toPrintable());
System.out.println("HashCode:" + Integer.toHexString(a.hashCode()));
System.out.println(ClassLayout.parseInstance(a).toPrintable());
}
}
结果:
分析:计算HashCode前,对象头前8byte,也就是前64bit除锁标志位为01外都为0,代表着对象此刻为无锁状态。计算HashCode后,发现31bit哈希位变化为实际哈希值,偏向标识,分代年龄,及锁标志位都无变化,证明成功。
证明JAVA虚拟机在启动4秒后才会开启偏向锁
public class JOLTest2 {
public static void main(String[] args) throws Exception {
Thread.sleep(3000l);
System.out.println(ClassLayout.parseInstance(new JOLTest1()).toPrintable());
Thread.sleep(2000l);
System.out.println(ClassLayout.parseInstance(new JOLTest1()).toPrintable());
}
}
结果:
分析:对比发现在main方法运行3秒时新建的对象还不允许偏向,5秒时却允许偏向了。出现这种现象的原因笔者猜测是因为JAVA虚拟机为了防止启动时过多的偏向锁膨胀为重量级锁消耗系统资源而短时间关闭了偏向模式,具体原因欢迎读者留言。
使用-XX:+PrintFlagsInitial参数启动虚拟机也可以直接看到。
证明对象在计算HashCode后不会进入可偏向状态
public class JOLTest5 {
public static void main(String[] args) throws Exception {
Thread.sleep(5000l);
JOLTest5 jt= new JOLTest5();
out.println(ClassLayout.parseInstance(jt).toPrintable());
System.out.println("HashCode:" + Integer.toHexString(jt.hashCode()));
out.println(ClassLayout.parseInstance(jt).toPrintable());
synchronized (jt){
out.println("locking");
out.println(ClassLayout.parseInstance(jt).toPrintable());
}
}
}
结果:
分析:第一次打印:对象初始化时对象头显示可偏向。第二次打印:计算HashCode后进入无锁状态,偏向模式变为0。第三次打印:加同步锁进入轻量锁。说明对象再也回不到可偏向的状态了。
证明对象在可偏向状态时加锁后会记录线程ID,且释放锁后对象头不发生变化
public class JOLTest3 {
public static void main(String[] args) throws Exception {
Thread.sleep(5000);
JOLTest3 jt= new JOLTest3();
out.println("befor lock");
out.println(ClassLayout.parseInstance(jt).toPrintable());
synchronized (jt){
out.println("locking");
out.println(ClassLayout.parseInstance(jt).toPrintable());
}
out.println("after lock");
out.println(ClassLayout.parseInstance(jt).toPrintable());
}
}
结果:
分析:对象jt在加上synchronized关键字后可偏向性与标志位无变化,并记录了线程ID:0x3de0,要证明这就是主线程ID应该不难。释放锁后对象头字节码依然不变,保留线程ID。
证明对象在无锁状态时加锁会变成轻量锁,释放锁后回到无锁状态
public class JOLTest4 {
public static void main(String[] args) throws Exception {
JOLTest4 jt= new JOLTest4();
out.println("befor lock");
out.println(ClassLayout.parseInstance(jt).toPrintable());
synchronized (jt){
out.println("locking");
out.println(ClassLayout.parseInstance(jt).toPrintable());
}
out.println("after lock");
out.println(ClassLayout.parseInstance(jt).toPrintable());
}
}
结果:
分析:对象jt在加上synchronized关键字后可偏向性无变化,标志位变为00,证明对象进入轻量级锁状态,对象头前62bit也发生了变化记录了线程ID:0x0261f5。释放锁后对象头字节码与新建时一致,回到无锁状态。
证明对象处于轻量锁时被大于1个线程竞争会膨胀为重量锁
public class JOLTest6 {
public static void main(String[] args) throws Exception {
JOLTest6 jt= new JOLTest6();
new Thread(()->{
synchronized (jt) {
System.out.println(ClassLayout.parseInstance(jt).toPrintable());
}
}).start();
Thread.sleep(1000l);
for(int i=0;i<2;i++){
new Thread(()->{
synchronized (jt) {
System.out.println(ClassLayout.parseInstance(jt).toPrintable());
}
}).start();
}
}
}
结果:
分析:第一次打印对象头时可偏向状态为0,标志位为00,表明对象处于轻量级锁状态。第二、三次打印可偏向状态为0,标志位10,表明对象处于重量级锁状态。
证明对象处于偏向锁时被大于1个线程竞争会膨胀为重量锁
public class JOLTest7 {
public static void main(String[] args) throws Exception {
Thread.sleep(5000l);
JOLTest7 jt= new JOLTest7();
new Thread(()->{
synchronized (jt) {
System.out.println(ClassLayout.parseInstance(jt).toPrintable());
}
}).start();
Thread.sleep(1000l);
for(int i=0;i<2;i++){
new Thread(()->{
synchronized (jt) {
System.out.println(ClassLayout.parseInstance(jt).toPrintable());
}
}).start();
}
}
}
结果:
分析:和上一个问题同理。
证明偏向锁效率碾压轻量级锁
public class JOLTest8 {
int count=0;
public static void main(String[] args) throws Exception {
JOLTest8 jt1= new JOLTest8();
long t1_start=System.currentTimeMillis();
new Thread(()->{
synchronized (jt1) {
System.out.println(ClassLayout.parseInstance(jt1).toPrintable());
for(int i=0;i<100000000;i++) jt1.count++;
}
}).start();
long t1_end=System.currentTimeMillis();
Thread.sleep(5000l);
JOLTest8 jt2= new JOLTest8();
long t2_start=System.currentTimeMillis();
new Thread(()->{
synchronized (jt2) {
System.out.println(ClassLayout.parseInstance(jt2).toPrintable());
for(int i=0;i<100000000;i++) jt2.count++;
}
}).start();
long t2_end=System.currentTimeMillis();
System.out.println("轻量锁加加1亿次时间:"+(t1_end-t1_start)+"毫秒");
System.out.println("偏向锁加加1亿次时间:"+(t2_end-t2_start)+"毫秒");
}
}
结果:
分析:打印结果很明显,38倍的差距,这还是笔者多次执行轻量锁表现最佳的一次结果。
证明轻量锁效率碾压重量级锁
懒得证明,如果错了笔者直播倒立吃屎,欢迎打脸。
证明重偏向存在且阀值为20
为了取消虚拟机的偏向锁启动延迟,达到更好的效果,笔者在执行下面代码时增加如下虚拟机参数-XX:BiasedLockingStartupDelay=0。
public class JOLTest9 {
public static void main(String[] args) throws Exception{
List<JOLTest9> list=new ArrayList<JOLTest9>();
Thread t1=new Thread(()->{
for(int i=0;i<20;i++){
JOLTest9 jt=new JOLTest9();
synchronized(jt){
list.add(jt);
}
}
});
t1.start();
t1.join();
System.out.println("线程t1执行完毕");
System.out.println(ClassLayout.parseInstance(list.get(18)).toPrintable());
System.out.println(ClassLayout.parseInstance(list.get(19)).toPrintable());
Thread t2=new Thread(()->{
System.out.println("线程t2尝试加锁");
for(int i=0;i<list.size();i++){
JOLTest9 jt=list.get(i);
synchronized(jt){
if(i>17) System.out.println(ClassLayout.parseInstance(jt).toPrintable());
}
}
});
t2.start();
t2.join();
System.out.println("线程t2执行完毕");
System.out.println(ClassLayout.parseInstance(list.get(18)).toPrintable());
System.out.println(ClassLayout.parseInstance(list.get(19)).toPrintable());
}
}
分析:由运行结果可知,当同一个类的对象执行超过19次偏向撤销操作后,JAVA虚拟机做出优化,从第20次开始不再撤销偏向而进行重偏向。
其实写一个空的main方法,在虚拟机参数加上-XX:+PrintFlagsInitial能看到一些虚拟机默认参数,其中就有一个参数叫BiasedLockingBulkRebiasThreshold值为20,翻译就是偏向锁批量重偏向阈值,也能证明这个结论。