偏向锁和轻量级锁

关于java的偏向锁和轻量级锁

基础知识

java中锁有四种级别,分别是

  • 无锁
  • 偏向锁
  • 轻量级锁
  • 重量级锁

其中 偏向锁 是锁仅获取一次,后续加锁只需要判断对象头里记录的线程是不是当前线程,如果是就直接进入同步。这样可以免去频繁的加锁

而如果有别的线程来竞争这个锁,那么这个锁就会升级成轻量级锁,线程通过自旋去竞争锁。

锁升级过程是不可逆

偏向锁

在开启偏向锁(默认开启)的情况下,一个锁将由第一个获取锁的线程偏向,对象头里将会记录对应的线程信息。以后这个线程只需要对比记录的线程是否一致,如果一致则不需要加锁解锁操作

要注意的是,即使线程已经结束了,偏向锁状态也不会消失。而是等到另一个线程来获取这个锁,因为和记录不一致,将升级成轻量级锁

轻量级锁

轻量级锁是通过cas+自旋来尝试获取锁,首先利用CAS尝试把对象原本的Mark Word 更新为Lock Record的指针,成功就说明加锁成功,然后执行相关同步操作。如果不成功,则开始自旋,不断去CAS。如果指定自旋次数内无法获取锁,则升级为重量级锁,这一过程代表着有过多的线程在竞争锁,才会导致某个线程无法获取锁

测试用例

根据上述知识,设计一个演示程序。基本逻辑如下

  • 首先有一个线程每隔1秒去获取锁,此时锁应该为偏向锁
  • 程序运行5.5秒后,另外起线程去获取锁,0.5秒是为了错开竞争,保证之前那个线程不会在此时来获取锁。由于线程不一致,锁升级为轻量级锁
  • 再次间隔5.5秒后,再起线程每隔1秒去获取锁,此时会和第一个线程竞争,锁应该升级为重量级锁

以下为一个两个线程竞争锁的程序

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101

private static Object sync = new Object();

public static void main(String[] args) {

VirtualMachine vm = VM.current();
long mark = vm.getLong(sync, 0);
System.out.println(lockDes(mark));

// 测试偏向锁升级成轻量级锁
new Thread(new Runnable() {
@Override
public void run() {
while (true) {//怀疑是这里使用了
long l = System.nanoTime();
long start = System.currentTimeMillis();
//如果没有线程竞争锁,这里就是偏向锁,有竞争升级成轻量级锁,自旋以获取锁,理论上获取锁的时间会更长
synchronized (sync) {
long mark = vm.getLong(sync, 0);
System.out.printf("%s获取了锁 %d %d %s %n", Thread.currentThread().getName(), System.nanoTime() - l, (System.currentTimeMillis() - start) / 1000, lockDes(mark));
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}

}
}
}).start();


try {
TimeUnit.SECONDS.sleep(5);
TimeUnit.MILLISECONDS.sleep(500);//把获取锁间隔开
} catch (InterruptedException e) {
e.printStackTrace();
}
long l = System.nanoTime();
long start = System.currentTimeMillis();
System.out.println("开始竞争");
synchronized (sync) {
mark = vm.getLong(sync, 0);
System.out.printf("只竞争一次的%s获取了锁 %d %d %s %n", Thread.currentThread().getName(), System.nanoTime() - l, (System.currentTimeMillis() - start) / 1000, lockDes(mark));
}
try {
TimeUnit.SECONDS.sleep(5);
TimeUnit.MILLISECONDS.sleep(500);//把获取锁间隔开
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(new Runnable() {
@Override
public void run() {


while (true) {
long l = System.nanoTime();
long start = System.currentTimeMillis();
//如果没有线程竞争锁,这里就是偏向锁,有竞争升级成轻量级锁,自旋以获取锁,理论上获取锁的时间会更长
synchronized (sync) {
long m = vm.getLong(sync, 0);
System.out.printf("第三个线程%s获取了锁 %d %d %s %n", Thread.currentThread().getName(), System.nanoTime() - l, (System.currentTimeMillis() - start) / 1000, lockDes(m));

try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

}
}
}).start();

}


private static String lockDes(long mark) {

long bits = mark & 0b11;
switch ((int) bits) {
case 0b11:
return "(marked: GC)";
case 0b00:
return "(thin lock: 轻量级锁)";
case 0b10:
return "(fat lock: 重量级锁)";
case 0b01:
int tribits = (int) (mark & 0b111);
switch (tribits) {
case 0b001:
return "(non-biasable)";
case 0b101:
return "(biased: 偏向锁)";
}
}

return "错误数据";
}

输出如下:

Thread-0获取了锁 82570 0 (biased: 偏向锁)
Thread-0获取了锁 28500 0 (biased: 偏向锁)
Thread-0获取了锁 26974 0 (biased: 偏向锁)
Thread-0获取了锁 29410 0 (biased: 偏向锁)
Thread-0获取了锁 25658 0 (biased: 偏向锁)
Thread-0获取了锁 19214 0 (biased: 偏向锁)
开始竞争
只竞争一次的main获取了锁 2132418 0 (thin lock: 轻量级锁)
Thread-0获取了锁 19542 0 (thin lock: 轻量级锁)
Thread-0获取了锁 24973 0 (thin lock: 轻量级锁)
Thread-0获取了锁 28656 0 (thin lock: 轻量级锁)
Thread-0获取了锁 29874 0 (thin lock: 轻量级锁)
Thread-0获取了锁 30851 0 (thin lock: 轻量级锁)
第三个线程Thread-1获取了锁 46264 0 (thin lock: 轻量级锁)
Thread-0获取了锁 943671726 0 (fat lock: 重量级锁)
第三个线程Thread-1获取了锁 1384640 0 (fat lock: 重量级锁)
Thread-0获取了锁 77250 0 (fat lock: 重量级锁)
第三个线程Thread-1获取了锁 574245 0 (fat lock: 重量级锁)
第三个线程Thread-1获取了锁 21957 0 (fat lock: 重量级锁)
第三个线程Thread-1获取了锁 26252 0 (fat lock: 重量级锁)
Thread-0获取了锁 2004668640 2 (fat lock: 重量级锁)
第三个线程Thread-1获取了锁 1563713 0 (fat lock: 重量级锁)
第三个线程Thread-1获取了锁 34069 0 (fat lock: 重量级锁)


流程

借助网上的一篇博客,来梳理一下过程

1、主线程来竞争锁
2、判断锁为偏向锁,且指向的线程0依旧存活
3、暂停线程0
4、将锁升级为轻量级锁
5、继续执行线程0
6、主线程开始自旋
7、主线程执行
8、主线程释放锁
9、线程0获取锁,此时应该为轻量级锁

解释

首先上面代码中用了jol来获取锁状态

1
2
3
4
5
6
7
 <!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-core -->
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.16</version>
<!--<scope>provided</scope>-->
</dependency>

主要是获取对象头低3位

java对象头结构
无锁和偏向锁


锁的升级过程基本可以理解了,但是有一个问题就是:一个对象初始状态怎么就是偏向锁了?

查阅jdk源码(markOop.hpp,从jdk6到jdk11,这部分都没有改动),关于markWord部分注释如下:

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102

// unused:25 hash:31 -->| unused:1 age:4 biased_lock:1 lock:2 (normal object)

// JavaThread*:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 (biased object)

// PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)

// size:64 ----------------------------------------------------->| (CMS free block)

//

// unused:25 hash:31 -->| cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && normal object)

// JavaThread*:54 epoch:2 cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && biased object)

// narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)

// unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)

//

// - hash contains the identity hash value: largest value is

// 31 bits, see os::random(). Also, 64-bit vm's require

// a hash value no bigger than 32 bits because they will not

// properly generate a mask larger than that: see library_call.cpp

// and c1_CodePatterns_sparc.cpp.

//

// - the biased lock pattern is used to bias a lock toward a given

// thread. When this pattern is set in the low three bits, the lock

// is either biased toward a given thread or "anonymously" biased,

// indicating that it is possible for it to be biased. When the

// lock is biased toward a given thread, locking and unlocking can

// be performed by that thread without using atomic operations.

// When a lock's bias is revoked, it reverts back to the normal

// locking scheme described below.

//

// Note that we are overloading the meaning of the "unlocked" state

// of the header. Because we steal a bit from the age we can

// guarantee that the bias pattern will never be seen for a truly

// unlocked object.

//

// Note also that the biased state contains the age bits normally

// contained in the object header. Large increases in scavenge

// times were seen when these bits were absent and an arbitrary age

// assigned to all biased objects, because they tended to consume a

// significant fraction of the eden semispaces and were not

// promoted promptly, causing an increase in the amount of copying

// performed. The runtime system aligns all JavaThread* pointers to

// a very large value (currently 128 bytes (32bVM) or 256 bytes (64bVM))

// to make room for the age bits & the epoch bits (used in support of

// biased locking), and for the CMS "freeness" bit in the 64bVM (+COOPs).

//

// [JavaThread* | epoch | age | 1 | 01] lock is biased toward given thread

// [0 | epoch | age | 1 | 01] lock is anonymously biased

//

// - the two lock bits are used to describe three states: locked/unlocked and monitor.

//

// [ptr | 00] locked ptr points to real header on stack

// [header | 0 | 01] unlocked regular object header

// [ptr | 10] monitor inflated lock (header is wapped out)

// [ptr | 11] marked used by markSweep to mark an object

// not valid at any other time

另外还有jol中的代码:

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
32
33
34
35
private static String parseMarkWord(long mark) {
// 64 bits:
// unused:25 hash:31 -->| unused_gap:1 age:4 biased_lock:1 lock:2 (normal object)
// JavaThread*:54 epoch:2 unused_gap:1 age:4 biased_lock:1 lock:2 (biased object)
long bits = mark & 0b11;
switch ((int) bits) {
case 0b11:
return "(marked: " + toHex(mark) + ")";
case 0b00:
return "(thin lock: " + toHex(mark) + ")";
case 0b10:
return "(fat lock: " + toHex(mark) + ")";
case 0b01:
String s = "; age: " + ((mark >> 3) & 0xF);
int tribits = (int) (mark & 0b111);
switch (tribits) {
case 0b001:
int hash = (int)(mark >>> 8);
if (hash != 0) {
return "(hash: " + toHex(hash) + s + ")";
} else {
return "(non-biasable" + s + ")";
}
case 0b101:
long thread = mark >>> 10;
if (thread == 0) {
return "(biasable" + s + ")";
} else {
return "(biased: " + toHex(thread) + "; epoch: " + ((mark >> 8) & 0x2) + s + ")";
}
}
default:
return "(parse error)";
}
}

关于偏向锁部分,重点在于倒数第三位的baised_lock
虽然名字叫偏向锁标记,但是我看下来结果更像是baiseable_lock——是否可以偏向的标志;如果是1,代表这个对象可以拥有偏向锁;区分是否已经获取了偏向锁,则靠高54位是否为0,获取了偏向锁的情况下,这54bit应该是对应的threadId。

在使用了 -XX:-UseBiasedLocking 关闭偏向锁后,这一位就都变成0了。


偏向锁和轻量级锁
http://blog.inkroom.cn/2021/06/03/3YX47QX.html
作者
inkbox
发布于
2021年6月3日
许可协议