Java锁原理
1、Java对象头
64位虚拟机对象头的Mark Word:
2、Monitor原理
Monitor被翻译为监视器或管程。
每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁(重量级)之后,该对象头的Mark Word中就被设置指向Monitor对象的指针。
给对象加锁的过程。
①假设刚开始Thread2要执行临界区代码,这时会先尝试让obj与操作系统提供的Monitor对象相关联(即让obj对象头的Mark Word指向Monitor的地址),如果关联成功,则obj对象头的Mark Word的状态从Normal变成Heavyweight Locked。此时Thread2成为了Monitor对象的Owner(刚开始Monitor中的Owner为null且只能有一个Owner)。
②这时如果另一个线程Thread1也要来执行临界区代码,它会先检查obj是否已经关联了一个Monitor对象,如果是则进一步检查它指向的Monitor对象是否有Owner,如果有则线程Thread1会进入Monitor对象的EntryList(等待队列)中,此时它变成blocked状态直至Owner被释放。
③同理,如果紧接着Thread-3也来执行synchronized(obj),也会进入EntryList中阻塞,EntryList是一个链表结构。
④当锁的持有者Thread2执行完同步代码块的内容后会唤醒EntryList中等待的线程来竞争锁,竞争的时是非公平的。假设这时Thread1抢到了锁并成为了Owner。
⑤Monitor对象中的WaitSet用于放置之前获得过锁,但条件不满足进入WAITING状态的线程。
注意
- synchronized必须是进入同一个对象的monitor才有上述的效果。
- 不加synchronized的对象不会关联监视器,不遵从以上规则。
2.1 synchronized原理
假设有一段代码如下:
1
2
3
4
5
6
7
8
9public class TestSynchronized {
static final Object lock = new Object();
static int counter = 0;
public static void main(String[] args) {
synchronized (lock) {
counter++;
}
}
}主函数的字节码指令如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
170 getstatic #2 <org/example/TestSynchronized.lock> //将lock对象的地址值压入操作数栈
3 dup //将lock对象的地址值复制一份并压栈
4 astore_1 //将栈顶的lock对象的地址存放到局部变量表索引为1的位置
5 monitorenter //将lock对象出栈并将它的Mark Word置为Monitor指针
6 getstatic #3 <org/example/TestSynchronized.counter> //将counter的值压栈
9 iconst_1 //将常数1压栈
10 iadd //将栈顶的两个常数出栈并做加法运算后把运算结果压栈
11 putstatic #3 <org/example/TestSynchronized.counter> //将栈顶的运算结果出栈并给TestSynchronized.counter直接赋值
14 aload_1 //将局部变量表索引为1的位置的lock对象的地址压栈
15 monitorexit //将lock对象出栈并将Mark Word重置, 唤醒EntryList
16 goto 24 (+8)
19 astore_2 //将异常对象出栈并保存到局部变量表索引为2的位置
20 aload_1 //将局部变量表索引为1的位置的lock对象的地址压栈
21 monitorexit //将lock对象出栈并将Mark Word重置, 唤醒EntryList
22 aload_2 //将异常对象重新压栈
23 athrow //弹出异常对象并throw给方法的调用者
24 return异常表如图:
2.2 轻量级锁
轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。
轻量级锁对使用者是透明的,即语法仍然是synchronized。
假设有两个方法同步块,利用同一个对象加锁:
1
2
3
4
5
6
7
8
9
10
11
12static final Object obj = new Object();
public static void method1() {
synchronized(obj) {
// 同步块A
method2();
}
}
public static void method2() {
synchronized(obj) {
// 同步块B
}
}加锁流程如下:
①假设某线程执行到method1()里的同步代码块,这时会在method1()的栈帧中创建锁记录(Lock Record)对象,内部可以存储锁定对象的Mark Word和被锁定对象的地址。
②让锁记录中Object reference指向锁对象,并尝试用cas替换Object的Mark Word,将Mark Word的值存入锁记录。
③如果cas替换成功,对象头中存储了锁记录地址和状态00,表示由该线程给对象加轻量级锁,这时图示如下。
④如果cas失败(判断锁对象的状态位已经是00),有两种情况。
如果是其它线程已经持有了该Object的轻量级锁,这时表明有竞争,进入锁膨胀过程。
如果是自己执行了synchronized锁重入,那么再添加一条Lock Record作为重入的计数。
⑤当退出synchronized代码块(解锁时)如果有取值为null的锁记录,表示有重入,这时重置锁记录,表示重入计数减一。
⑥当退出synchronized代码块(解锁时)锁记录的值不为null,这时使用cas将Mark Word的值恢复给对象头。
- 成功,则解锁成功。
- 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程。
2.3 锁膨胀
- 如果在尝试加轻量级锁的过程中,CAS操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
1 | static Object obj = new Object(); |
- 当Thread-1进行轻量级加锁时,Thread-0已经对该对象加了轻量级锁。
这时Thread-1加轻量级锁失败,进入锁膨胀流程。
- 即为Object对象申请Monitor锁,让Object指向重量级锁地址。
- 然后自己进入Monitor的EntryList中BLOCKED。
当Thread-0退出同步块解锁时,使用cas将Mark Word的值恢复给对象头,失败。这时会进入重量级解锁 流程,即按照Monitor地址找到Monitor对象,设置Owner为null,唤醒EntryList中的BLOCKED线程。
2.4 自旋优化
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。
自旋重试成功的情况:
自旋重试失败的情况:
自旋会占用CPU时间,单核CPU自旋就是浪费,多核CPU自旋才能发挥优势。
在Java 6之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会 高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
Java 7之后不能控制是否开启自旋功能。
2.5 偏向锁
轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行CAS操作。
Java 6中引入了偏向锁来做进一步优化:只有第一次使用CAS将线程ID设置到对象的Mark Word头,之后发现 这个线程ID是自己的就表示没有竞争,不用重新CAS。以后只要不发生竞争,这个对象就归该线程所有。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18static final Object obj = new Object();
public static void m1() {
synchronized( obj ) {
// 同步块 A
m2();
}
}
public static void m2() {
synchronized( obj ) {
// 同步块 B
m3();
}
}
public static void m3() {
synchronized( obj ) {
// 同步块 C
}
}一个对象创建时:
- 如果开启了偏向锁(默认开启,那么对象创建后,markword值为0x05即最后3位为101,这时它的thread、epoch、age都为0。
- 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加JVM参数**-XX:BiasedLockingStartupDelay=0**来禁用延迟。
- 如果没有开启偏向锁,那么对象创建后,markword值为0x01即最后3位为001,这时它的hashcode、age都为0,第一次用到hashcode时才会赋值。
测试使用偏向锁(JVM参数为-XX:BiasedLockingStartupDelay=0)
①引入第三方jar包。
1
2
3
4
5
6<!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-core -->
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.14</version>
</dependency>②测试代码如下。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class TestBiased {
public static void main(String[] args) throws InterruptedException {
Dog dog = new Dog();
log.debug("synchronized 前");
System.out.println(ClassLayout.parseInstance(dog).toPrintable());
synchronized (dog){
log.debug("synchronized 中");
System.out.println(ClassLayout.parseInstance(dog).toPrintable());
}
log.debug("synchronized 后");
System.out.println(ClassLayout.parseInstance(dog).toPrintable());
}
}③从运行结果可以看出主线程为dog对象加了偏向锁(标志位为101),并在加锁中和解锁后dog对象的Mark Work都有保存主线程的ThreadID信息。
测试禁用偏向锁(JVM参数为-XX:-UseBiasedLocking)
在上面测试代码运行时在添加JVM参数**-XX:-UseBiasedLocking**禁用偏向锁后运行结果如下。发现在禁用偏向锁时会先使用轻量级锁(标志位为00),且在对象的Mark Word信息里存放了对应的锁记录;并在解锁后恢复该对象原本信息。
测试调用对象的hashCode()。(JVM参数为-XX:BiasedLockingStartupDelay=0)
测试代码如下。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class TestBiased {
public static void main(String[] args) throws InterruptedException {
Dog dog = new Dog();
dog.hashCode(); // 会禁用掉对象的偏向锁
log.debug("synchronized 前");
System.out.println(ClassLayout.parseInstance(dog).toPrintable());
synchronized (dog){
log.debug("synchronized 中");
System.out.println(ClassLayout.parseInstance(dog).toPrintable());
}
log.debug("synchronized 后");
System.out.println(ClassLayout.parseInstance(dog).toPrintable());
}
}
class Dog {
}运行结果如下。会发现正常状态对象一开始是没有hashCode的,第一次调用才生成;而且调用了hashCode()后会同时禁用掉对象的偏向锁(主要原因是标志位为偏向锁时没有足够的位置存放对象的哈希码),从而还是先使用轻量级锁(标志位为00)。
- 轻量级锁和重量级锁在调用对象的hashCode()后不会有禁用的问题,原因是在给对象加了轻量级锁后,其hashCode等信息会被存放到栈帧中的锁记录里,而在加了重量级锁后对象信息会放在操作系统的Monitor对象里。
测试有其它线程使用对象。(JVM参数为-XX:BiasedLockingStartupDelay=0)
当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁(前提是两个线程要交错访问锁对象)。
测试代码如下。
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
42public class TestBiased {
public static void main(String[] args) throws InterruptedException {
Dog dog = new Dog();
Thread t1 = new Thread(() -> {
log.debug("synchronized 前");
System.out.println(ClassLayout.parseInstance(dog).toPrintable());
synchronized (dog){
log.debug("synchronized 中");
System.out.println(ClassLayout.parseInstance(dog).toPrintable());
}
log.debug("synchronized 后");
System.out.println(ClassLayout.parseInstance(dog).toPrintable());
synchronized (TestBiased.class){
TestBiased.class.notify();
}
}, "t1");
t1.start();
Thread t2 = new Thread(() -> {
synchronized (TestBiased.class){
try {
TestBiased.class.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("synchronized 前");
System.out.println(ClassLayout.parseInstance(dog).toPrintable());
synchronized (dog){
log.debug("synchronized 中");
System.out.println(ClassLayout.parseInstance(dog).toPrintable());
}
log.debug("synchronized 后");
System.out.println(ClassLayout.parseInstance(dog).toPrintable());
}, "t2");
t2.start();
}
}
class Dog {
}运行结果如下图。
2.5.1 批量重定向
如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程T1的对象仍有机会重新偏向T2,重偏向会重置对象的Thread ID。
当撤销偏向锁阈值超过20次后,jvm会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至加锁线程。
测试代码如下:
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
public class TestBiased {
public static void main(String[] args) throws InterruptedException {
Vector<Dog> list = new Vector<>();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 30; i++) {
Dog d = new Dog();
list.add(d);
synchronized (d) {
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
}
}
synchronized (list) {
list.notify();
}
}, "t1");
t1.start();
Thread t2 = new Thread(() -> {
synchronized (list) {
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("===============> ");
for (int i = 0; i < 30; i++) {
Dog d = list.get(i);
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
synchronized (d) {
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
}
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
}
}, "t2");
t2.start();
}
}
class Dog {
}运行结果如下。
t1线程的前30次打印都分别给新创建的dog对象加了偏向锁。
t2线程被唤醒后,其前19次打印都将t1线程已经加了偏向锁的对象升级为轻量级锁。
直至t2线程的第20次打印达到了阈值后,接下来都不将偏向锁升级为轻量级锁了,而是重偏向到t2线程。
2.5.2 批量撤销
当撤销偏向锁阈值超过40次后,jvm会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的。
测试代码如下。
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
public class TestBiased {
static Thread t1, t2, t3;
public static void main(String[] args) throws InterruptedException {
Vector<Dog> list = new Vector<>();
int loopNumber = 39;
t1 = new Thread(() -> {
for (int i = 0; i < loopNumber; i++) {
Dog d = new Dog();
list.add(d);
synchronized (d) {
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
}
}
LockSupport.unpark(t2);
}, "t1");
t1.start();
t2 = new Thread(() -> {
LockSupport.park();
log.debug("===============> ");
for (int i = 0; i < loopNumber; i++) {
Dog d = list.get(i);
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
synchronized (d) {
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
}
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
}
LockSupport.unpark(t3);
}, "t2");
t2.start();
t3 = new Thread(() -> {
LockSupport.park();
log.debug("===============> ");
for (int i = 0; i < loopNumber; i++) {
Dog d = list.get(i);
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
synchronized (d) {
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
}
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
}
}, "t3");
t3.start();
t3.join();
log.debug(ClassLayout.parseInstance(new Dog()).toPrintable());
}
}
class Dog {
}运行结果如下。
①t1线程的前39次打印都分别给新创建的dog对象加了偏向锁。
②紧接着t2线程的前19次打印都将t1线程已经加了偏向锁的对象升级为轻量级锁,并在解锁后将锁对象恢复正常状态。
③t2线程从打印第20次开始就进行批量重偏向,即不再将偏向锁升级为轻量级锁了,而是重偏向到t2线程。
④紧接着t3线程的前19次打印还是将正常状态的锁对象加轻量级锁并在解锁后恢复正常状态。
⑤紧接着t3线程后面的第20至39次打印是将处于偏向锁状态的锁对象分别变成轻量级锁,并在解锁后变成正常状态。
⑥由于之前一共有39次进行了撤销偏向锁并升级为轻量级锁的操作,接下来主线程的第40次打印达到了阈值,即此后Dog类的所有对象都会变为不可偏向的。
2.6 锁消除
①添加相关依赖:
1
2
3
4
5
6
7
8
9
10<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.20</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.20</version>
</dependency>②测试代码如下:
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
public class MyBenchmark {
static int x = 0;
public void a() throws Exception {
x++;
}
// JIT即时编译器发现锁对象不会逃离该方法的作用范围,意味着该锁对象不会被共享,所以会将该synchronized消除掉
public void b() throws Exception {
Object o = new Object();
synchronized (o) {
x++;
}
}
public static void main(String[] args) throws RunnerException {
// 可以通过注解
Options opt = new OptionsBuilder()
.include(MyBenchmark.class.getSimpleName())
.build();
new Runner(opt).run();
}
}③通过运行结果可以发现两个方法的平均执行时间是十分相近的,即b()方法没有进行加锁操作。
1
2
3Benchmark Mode Cnt Score Error Units
MyBenchmark.a avgt 5 0.606 ± 0.377 ns/op
MyBenchmark.b avgt 5 0.586 ± 0.252 ns/op而如果jvm参数-XX:-EliminateLocks禁用了锁消除,则执行结果如下,发现执行时间相差很大。
1
2
3Benchmark Mode Cnt Score Error Units
MyBenchmark.a avgt 5 0.920 ± 1.224 ns/op
MyBenchmark.b avgt 5 11.342 ± 11.454 ns/op
2.7 死锁
有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁,即两个线程在互相等待对方释放自己想要的资源而无限制地等待下去。
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
public class TestDeadLock {
public static void main(String[] args) {
Object A = new Object();
Object B = new Object();
Thread t1 = new Thread(() -> {
synchronized (A) {
log.debug("lock A");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (B) {
log.debug("lock B");
log.debug("操作...");
}
}
}, "t1");
Thread t2 = new Thread(() -> {
synchronized (B) {
log.debug("lock B");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (A) {
log.debug("lock A");
log.debug("操作...");
}
}
}, "t2");
t1.start();
t2.start();
}
}执行结果是发生了死锁:
1
210:55:57.053 org.example.TestDeadLock [t1] - lock A
10:55:57.053 org.example.TestDeadLock [t2] - lock B定位死锁的方法
第一种方式:运行程序,打开终端运行jps,使用jps定位进程id,再用【jstack+进程id】定位死锁。打印信息如下:
第二种方式:使用jconsole工具。
2.8 活锁
活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束,例如:
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
public class TestLiveLock {
static volatile int count = 10;
static final Object lock = new Object();
public static void main(String[] args) {
new Thread(() -> {
// 期望减到 0 退出循环
while (count > 0) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
log.debug("count: {}", count);
}
}, "t1").start();
new Thread(() -> {
// 期望超过 20 退出循环
while (count < 20) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
log.debug("count: {}", count);
}
}, "t2").start();
}
}运行结果是无限制地运行下去。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1611:19:09.978 org.example.TestLiveLock [t2] - count: 10
11:19:09.978 org.example.TestLiveLock [t1] - count: 10
11:19:10.194 org.example.TestLiveLock [t1] - count: 9
11:19:10.194 org.example.TestLiveLock [t2] - count: 10
11:19:10.394 org.example.TestLiveLock [t2] - count: 11
11:19:10.394 org.example.TestLiveLock [t1] - count: 10
11:19:10.601 org.example.TestLiveLock [t1] - count: 10
11:19:10.601 org.example.TestLiveLock [t2] - count: 11
11:19:10.809 org.example.TestLiveLock [t2] - count: 11
11:19:10.809 org.example.TestLiveLock [t1] - count: 11
11:19:11.011 org.example.TestLiveLock [t1] - count: 11
11:19:11.011 org.example.TestLiveLock [t2] - count: 12
11:19:11.214 org.example.TestLiveLock [t1] - count: 10
11:19:11.214 org.example.TestLiveLock [t2] - count: 11
11:19:11.430 org.example.TestLiveLock [t1] - count: 10
…………