Java对象头
- 当对象头最后三位为001时,处于无锁状态
- 当对象头最后三位为101时,处于偏向锁状态
- 当对象头最后三位为000时,处于轻量锁状态
- 当对象头最后三位为010时,处于重量锁状态
锁的升级流程
轻量级锁
轻量级锁在线程交替进行的情况下相对于重量级锁效率有所提高。这是因为在申请轻量级锁时不会进行系统调用,减少了用户态到内核态的切换。
轻量级锁加锁过程:
- 创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录对象,内部可以存储锁定对象的Mark Word信息
- 让锁记录中Object reference指向锁对象,并尝试用cas替换Object中的Mark Word,将Mark Word的值存入锁记录中
- 如果cas替换成功,对象头中存储了锁记录和状态00,表示该线程给对象加锁,此时如图
- 如果cas失败,此时有两种情况
- 一种是已经有线程对obj对象进行了加锁,证明有竞争发生,此时会发生锁膨胀
- 如果是自己执行了synchronized锁重入,那么线程中将会再添加一条Lock record作为重入的计数
- 锁膨胀
代码验证轻量级锁
在这里准备打印出对象头信息来观察对象头的bit变化,使用jol查看对象头
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.14</version>
</dependency>
由于jol打印的对象头不太好观察,这里写了一个方法进行解析
public static String parseHeader(String header) {
int index = header.indexOf('\n');
int i2 = header.indexOf('\n', index + 1);
header = header.substring(i2+1);
int toFRBracket = header.indexOf(')');
int toValBracket = header.indexOf('(',toFRBracket+1);
int toValEndBracket = header.indexOf(')',toValBracket);
String first4bit = header.substring(toValBracket+1,toValEndBracket);
int endline = header.indexOf('\n');
header = header.substring(endline+1);
String second4bit = header.substring(toValBracket+1,toValEndBracket);
String[] firsts = first4bit.split(" ");
String[] seconds = second4bit.split(" ");
StringBuilder res = new StringBuilder();
StringBuilder out = new StringBuilder();
for (int i = seconds.length-1; i >= 0; i--) {
res.append(seconds[i]).append(" ");
out.append(seconds[i]);
}
for (int i = firsts.length-1; i >= 0; i--) {
res.append(firsts[i]).append(" ");
out.append(firsts[i]);
}
String unused = out.substring(0, 25);
String hashcode = out.substring(25, 56);
String unused2 = out.substring(56, 57);
String age = out.substring(57, 61);
String bias = out.substring(61, 62);
String sort = out.substring(62, 64);
System.out.println("unused_25bit " +
" hashcode_31bit " +
" " + "age " + "b " + "st ");
System.out.println(unused + " " + hashcode + " " + unused2 + " " + age + " " + bias + " " +sort);
return res.toString();
}
我们先不谈论偏向锁,可以使用虚拟机参数-XX:-UseBiasedLocking关闭偏向锁。
public class Synchronized {
public static void main(String[] args) throws InterruptedException {
ThreadTest test = new ThreadTest();
Thread t1 = new Thread(test,"first thread");
Thread t2 = new Thread(test,"second thread");
t1.start();
t2.start();
}
static class ThreadTest implements Runnable{
int count = 0;
@Override
public void run() {
synchronized (this) {
System.out.println("thread: "+Thread.currentThread().getName());
System.out.println(parseHeader(ClassLayout.parseInstance(this).toPrintable()));
System.out.println();
count++;
}
}
}
}
在synchronized处打下断点,设置为thread断点类型
使用debug启动程序
目前是first thread线程执行到了断点处,直接令first thread将run方法执行完毕,观察结果
可以看到对象头的最后三位是000,处于轻量锁状态,接下来执行second thread线程,观察输出
可见second thread线程获取到的锁也是轻量锁,此时就是线程的交替进行。下面我们来看看有竞争的情况 ,还是debug启动,让first thread执行进入run方法
此时选择second thread,点击箭头尝试进入run方法
此时就发生了锁的争抢,first thread会进行所升级,进入到重量级锁状态,让first thread线程执行完,观察输出
此时second thread 线程也可以执行了,观察输出
可以看到无论是线程1还是线程2获取到的锁的对象头的最后三位都是010,表示重量级锁。
偏向锁
偏向锁默认是开启的,不过延迟了4s,可以在加虚拟机参数设置无延迟开启偏向锁XX:BiasedLockingStartupDelay=0
虚拟机为对象加偏向锁的步骤:
- 检查锁对象的mark word的线程id
- 如果为空则使用cas设置为当前线程id,设置成功则获取锁成功,失败则撤销偏向锁
- 如果不为空就检查线程id是否和当前线程的id匹配,如果匹配则获取锁成功,否则撤销偏向锁
这在轻量锁的基础上简化了cas的步骤,一定程度上提高了效率,但是偏向锁只适用与一个线程反复的获得锁
代码验证偏向锁
还是上方的代码,只不过虚拟机参数变为XX:BiasedLockingStartupDelay=0,先让线程1执行完毕,观察输出:
可以看到线程1获取到的锁的对象头的最后三位是101,代表是偏向锁,此时执行线程2的话,就会发现对象头的mark word线程id和当前线程id不匹配,然后撤销偏量锁,升级到轻量级锁
可以观察到对象头的最后三位是000,代表轻量级锁
重量级的锁自旋
重量级锁竞争的时候,还可以使用自选来进行优化,如果当前线程自旋成功,也就是说在同步代码快中的线程执行完毕,此时当前线程可以避免阻塞,一把来说自旋时间会设置为少于线程上下文切换的时间。当然,锁的自旋我们并不能控制