Synchronized锁膨胀验证和分析

之前分析synchronized关键字时,最后面对于锁膨胀的过程并没有提供证明,其实这里有几个问题:

  • 对象初始化的时候对象头中的锁标志位是怎样的?
  • 啥时候会加上偏向锁?
  • 啥时候膨胀为轻量级锁?
  • 啥时候加重量锁?

这些问题我上次说是“无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁”的膨胀过程,上网查好多的博客和书籍也都这么说的,但是我这里有个疑问:我没有实打实的看到这个啊,怎么来证明这个过程呢?

在这篇文章中我会尽量用代码实践来呈现出对象头中各个级别的锁的标志位,以便有个直观的印象。

复习一下,锁标志位都是什么含义:

偏向锁标志 锁标志位 表示
0 01 无锁
1 01 偏向锁
0 00 轻量级锁
0 10 重量级锁
0 11 gc标志

本文只需要关注锁标志位那3位(之后是2位)即可。

无锁和偏向锁

首先就是没有加任何同步的朴素代码,这其实就是无锁的情形:

1
2
3
4
5
static L l;
public static void main(String[] args) {
l = new L();
System.out.println(ClassLayout.parseInstance(l).toPrintable());
}

输出对象头的前8位为:00000001,0 01查表就是无锁状态。

似乎一切都理所应当,但这里其实是有坑的,我们先看一下偏向锁吧。

偏向锁

偏向锁的意思就是只有一个线程进入临界区,从java视角来看就是共享变量只被一个线程锁住了,那话不多说,我们马上来验证一下偏向锁的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static L l;
public static void main(String[] args) {
l = new L();

System.out.println("before lock");
System.out.println(ClassLayout.parseInstance(l).toPrintable());

synchronized (l) {
System.out.println("locking...");
System.out.println(ClassLayout.parseInstance(l).toPrintable());
}

System.out.println("after lock");
System.out.println(ClassLayout.parseInstance(l).toPrintable());
}

看一下结果,我只贴mark word的前8位(我机器是大端存储):

1
2
3
4
5
6
7
8
before lock
00000001

locking...
11111000

after lock
00000001

加锁时候是0 00,我们预期是偏向锁啊,应该是1 01才对啊,差的有点多吧,蒙圈了没?

通过查hotspot源码(代码路径openjdk/hotspot/src/share/vm/runtime/globals.hpp)才找到问题:

1
2
product(intx, BiasedLockingStartupDelay, 4000, 
"Number of milliseconds to wait before enabling biased locking")

啥意思?这话的意思是JVM启动的时候偏向锁有延迟,延迟是4秒。那我们改进一下把主线程睡5秒再看看结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static L l;
public static void main(String[] args) {
Thread.sleep(5000); // 睡5秒
l = new L();

System.out.println("before lock");
System.out.println(ClassLayout.parseInstance(l).toPrintable());

synchronized (l) {
System.out.println("locking...");
System.out.println(ClassLayout.parseInstance(l).toPrintable());
}

System.out.println("after lock");
System.out.println(ClassLayout.parseInstance(l).toPrintable());
}

结果:

1
2
3
4
5
6
7
8
before lock
00000101

locking...
00000101

after lock
00000101

现在能清楚的看到结果了,所有加锁的前中后锁标志位都是1 01,要注意main线程也是线程,这里我们只有一个main线程,也就是只有一个线程进入临界区的时候,JVM会加上偏向锁

那么JVM为啥延迟偏向锁呢?JVM源码中是这样注释的(代码路径openjdk/hotspot/src/share/vm/runtime/biasedLocking.cpp):

If biased locking is enabled, schedule a task to fire a few seconds into the run which turns on biased locking for all currently loaded classes as well as future ones. This is a workaround for startup time regressions due to a large number of safepoints being taken during VM startup for bias revocation. Ideally we would have a lower cost for individual bias revocation and not need a mechanism like this.

大概意思是:当jvm启动记载资源的时候,初始化的对象加偏向锁会耗费资源,减少大量偏向锁撤销的成本(jvm的偏向锁的优化)。

可以修改jvm启动参数来禁止偏向锁延迟(不用手动sleep了):

1
-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0

注意:这块严谨来说,在jdk 1.6之后,关于使用偏向锁和轻量级锁,JVM是有优化的。在开启偏向锁延迟(默认)的情况下,使用的是轻量级锁(我们上面那个0 00);禁止偏向锁延迟的话,使用的是偏向锁

无锁

上面我们看到了JVM会自动延迟4秒加一个偏向锁,所以最上面我们对于无锁的演示其实是不对的,它还是应该被加上偏向锁,只不过我们的验证在4秒以内。读者可以自己试试先睡5秒,就可以看到标志位是1 01(偏向锁)了。

那么如何看到无锁状态呢?类加载时就new对象,在静态代码块中打印一下对象头即可:

1
2
3
4
5
static L l = new L();
static {
Thread.sleep(5000);
System.out.println(ClassLayout.parseInstance(l).toPrintable());
}

能看到结果为0 01(无锁),填无锁的坑完毕,往下看。

轻量级锁

轻量级锁其实我们曾经看到过,就是在第一次没禁用偏向锁延迟的时候,我再贴一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static L l;
public static void main(String[] args) {
l = new L();

System.out.println("before lock");
System.out.println(ClassLayout.parseInstance(l).toPrintable());

synchronized (l) {
System.out.println("locking...");
System.out.println(ClassLayout.parseInstance(l).toPrintable());
}

System.out.println("after lock");
System.out.println(ClassLayout.parseInstance(l).toPrintable());
}

输出为:

1
2
3
4
5
6
7
8
before lock
00000001

locking...
11111000

after lock
00000001

加锁时可以看到锁标志位是0 00(轻量级锁)。

重量级锁

重量锁的验证我会连上gc过程和标志位一起分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
static L l;
public static void main(String[] args) {
l = new L();
System.out.println("before lock");
System.out.println(ClassLayout.parseInstance(l).toPrintable());

Thread t = new Thread(() -> {
synchronized (l) {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
}
}
});
t.start();
System.out.println("t locking");
System.out.println(ClassLayout.parseInstance(l).toPrintable());

// main thread lock
synchronized (l) {
System.out.println("main locking");
System.out.println(ClassLayout.parseInstance(l).toPrintable());
}

System.out.println("after lock");
System.out.println(ClassLayout.parseInstance(l).toPrintable());

System.gc();
System.out.println("after gc");
System.out.println(ClassLayout.parseInstance(l).toPrintable());
}

先看看输出吧,分析我在注释中写了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
before lock
00000001 // 0 01无锁

t locking
01101000 // 0 00轻量级锁,前面5位连同mark word其他位表示轻量级锁指针

main locking
10011010 // 0 10重量级锁,前面5位连同mark word其他位表示重量级锁指针

after lock
10011010 // 0 10重量级锁

after gc
00001001 // gc后0 01无锁,前面4位0000变为0001表示age+1了

这里演示有竞争条件时(线程t和main线程存在竞争l锁的关系),重量级锁的标志位是0 10。

为啥after lock之后还是重量级锁呢?锁能降级吗?

锁能否降级

其实HotSpot支持锁降级,但是锁升降级效率较低,如果频繁升降级的话对性能就会造成很大影响。重量级锁降级发生于STW阶段,降级对象为仅仅能被VMThread访问(比如gc线程)而没有其他JavaThread(没有引用链)访问的对象。

被锁的对象都被gc了有没有锁还有啥关系?因此基本认为锁不可降级。

总结

所以最开始那几个问题有答案了吗(不讨论只有类加载情形,一般都是要有main线程的)?

  • 对象初始化的时候对象头中的锁标志位是怎样的?
    • 默认是0 01(无锁),禁用偏向锁延迟时是1 01(偏向锁 – 因为有main线程)。
  • 啥时候会加上偏向锁?
    • 默认是4秒后,所有对象都加。
  • 啥时候膨胀为轻量级锁?
    • 只有一个线程进入临界区,默认由偏向锁膨胀为轻量级锁0 00。
  • 啥时候加重量锁?
    • 有竞争条件时,加重量锁。