注意:看该篇文章之前您需要了解java对象头
首先介绍一个工具,能查看java对象头信息
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.8</version>
</dependency>
identity hashcode:如果我们没有重写默认的hashcode方法,默认的hashcode计算出的就是identity hashcode,这个值是被存储在对象头中的,如果重写之后,就不能叫identity hashcode了,计算出的值也不会存储在对象头中。
下文所说的hashcode都是identity hashcode
我们知道java对象头保存了一个对象的非常多的信息。
1.非数组对象的对象头中保存的是Mark Word和执行类源信息的指针
2.数组对象除了上述两个信息之外还有额外的一个数组长度信息。
Mark Word中保存的都是一个java对象所必须的信息,比如分代年龄等,identity hashcode等。
无锁状态下这些数据都是存储在对象头中的,但是当出现锁之后这行数据信息可能会发生变化,就比如轻量级锁下会将对象的Mark Word利用CAS替换到栈帧中用于记录锁记录的空间中,然后对象头中原有的位置变成了指向锁记录的指针。重量级锁也类似,不过他指向的是一个monitor对象,也能记录下原来的hashcode和分代年龄等。关键就是在于,原本存储Mark Word的位置被指针占据了,轻量级和重量级锁都会找个位置存放原有信息。但是偏向锁没有地方来存原来的位置了,所以如果你一个对象计算了hashcode之后,没地方放了怎么办,这就是hashcode与轻量级锁的冲突问题,一个对象如果计算了hashcode之后,锁就会膨胀,不能再做为偏向锁。
测试环境jdk1.8
一个简单的类
public class A {
boolean flag = false;
int a = 3;
}
public static void main(String[] args) throws InterruptedException {
// 需要sleep一段时间,因为java对于偏向锁的启动是在启动几秒之后才激活。
// 因为jvm启动的过程中会有大量的同步块,且这些同步块都有竞争,如果一启动就启动
// 偏向锁,会出现很多没有必要的锁撤销
Thread.sleep(5000);
A a = new A();
// 未出现任何获取锁的时候
System.out.println(ClassLayout.parseInstance(a).toPrintable());
synchronized (a){
// 获取一次锁之后
System.out.println(ClassLayout.parseInstance(a).toPrintable());
}
// 输出hashcode
System.out.println(a.hashCode());
// 计算了hashcode之后
System.out.println(ClassLayout.parseInstance(a).toPrintable());
synchronized (a){
// 再次获取锁
System.out.println(ClassLayout.parseInstance(a).toPrintable());
}
}
输出1
测试环境是windows,所以数据的存储是以小端模式,101可以看出这时候的锁状态是偏向锁,但是并没有偏向的threadId,因为还没有线程获取过锁。此时是匿名偏向锁。
输出2
这时候出线程获取了一次锁,threadId出现了值。
输出3
计算了hascode之后,大家可以计算一下输出的hashcode与存储的值是否一致,注意,小端模式。这时候发现偏向锁的标志位已经为0了,虽然锁标志位还是01,但是他进行不能作为偏向锁了,就导致至少升级为轻量级锁了。
输出4
这时候看,代码中虽然没有任何的线程竞争,但是我们的锁还是变成了轻量级锁,就是因为hashcode带来的锁膨胀的问题