0%

深入理解Synchronized

Synchronized是Java中解决并发问题的一种最常用的方法,也是最简单的一种方法。

Synchronized的作用主要有三个:

  1. 确保线程互斥的访问同步代码
  2. 保证共享变量的修改能够及时可见
  3. 有效解决重排序问题(synchronized 不能防止指令重排序,但满足 as-if-serial 语义)

从语法上讲,Synchronized总共有三种用法:

  1. 修饰普通方法
  2. 修饰静态方法
  3. 修饰代码块

Synchronized在JDK1.6后的优化,主要包括了自适应自旋锁、锁消除、锁粗化、轻量级锁和偏向锁

前置概念

重量级锁

当要进入一个同步、线程安全的方法时,是需要先获得这个方法的锁的,退出这个方法时,则会释放锁。如果获取不到这个锁的话,意味着有别的线程在执行这个方法,这时我们就会马上进入阻塞的状态,等待那个持有锁的线程释放锁,然后再把我们从阻塞的状态唤醒,我们再去获取这个方法的锁。

这种获取不到锁就马上进入阻塞状态的锁,我们称之为重量级锁。

自旋锁和自适应自旋锁

我们知道,线程从运行态进入阻塞态这个过程,是非常耗时的,因为不仅需要保存线程此时的执行状态,上下文等数据,还涉及到用户态到内核态的转换。当然,把线程从阻塞态唤醒也是一样,也是非常消耗时间的。

刚才我说线程拿不到锁,就会马上进入阻塞状态,然而现实是,它虽然这一刻拿不到锁,可能在下 0.0001 秒,就有其他线程把这个锁释放了。如果它慢0.0001秒来拿这个锁的话,可能就可以顺利拿到了,不需要经历阻塞/唤醒这个花时间的过程了。

然而重量级锁就是这么坑,它就是不肯等待一下,一拿不到就是要马上进入阻塞状态。为了解决这个问题,我们引入了另外一种愿意等待一段时间的锁 — 自旋锁。

自旋锁就是,如果此时拿不到锁,它不马上进入阻塞状态,而是等待一段时间,看看这段时间有没其他人把这锁给释放了。怎么等呢?这个就类似于线程在那里做空循环,如果循环一定的次数还拿不到锁,那么它才会进入阻塞的状态。

上面我们说的自旋锁,每个线程循环等待的次数都是一样的,例如我设置为 100次的话,那么线程在空循环 100 次之后还没拿到锁,就会进入阻塞状态了。

而自适应自旋锁就牛逼了,它不需要我们人为指定循环几次,它自己本身会进行判断要循环几次,而且每个线程可能循环的次数也是不一样的。而之所以这样做,主要是我们觉得,如果一个线程在不久前拿到过这个锁,或者它之前经常拿到过这个锁,那么我们认为它再次拿到锁的几率非常大,所以循环的次数会多一些。

而如果有些线程从来就没有拿到过这个锁,或者说,平时很少拿到,那么我们认为,它再次拿到的概率是比较小的,所以我们就让它循环的次数少一些。因为你在那里做空循环是很消耗 CPU 的。

所以这种能够根据线程最近获得锁的状态来调整循环次数的自旋锁,我们称之为自适应自旋锁。

轻量级锁

上面我们介绍的三种锁:重量级、自旋锁和自适应自旋锁,他们都有一个特点,就是进入一个方法的时候,就会加上锁,退出一个方法的时候,也就释放对应的锁。

之所以要加锁,是因为他们害怕自己在这个方法执行的时候,被别人偷偷进来了,所以只能加锁,防止其他线程进来。这就相当于,每次离开自己的房间,都要锁上门,人回来了再把锁解开。

这实在是太麻烦了,如果根本就没有线程来和他们竞争锁,那他们不是白白上锁了?要知道,加锁这个过程是需要操作系统这个大佬来帮忙的,是很消耗时间的。为了解决这种动不动就加锁带来的开销,轻量级锁出现了。

轻量级锁认为,当你在方法里面执行的时候,其实是很少刚好有人也来执行这个方法的,所以,当我们进入一个方法的时候根本就不用加锁,我们只需要做一个标记就可以了,也就是说,我们可以用一个变量来记录此时该方法是否有人在执行。也就是说,如果这个方法没人在执行,当我们进入这个方法的时候,采用CAS机制,把这个方法的状态标记为已经有人在执行,退出这个方法时,在把这个状态改为了没有人在执行了。

之所以要用CAS机制来改变状态,是因为我们对这个状态的改变,不是一个原子性操作,所以需要CAS机制来保证操作的原子性。
显然,比起加锁操作,这个采用CAS来改变状态的操作,花销就小多了。

然而可能会说,没人来竞争的这种想法,那是你说的而已,那如果万一有人来竞争说呢?也就是说,当一个线程来执行一个方法的时候,方法里面已经有人在执行了。

如果真的遇到了竞争,我们就会认为轻量级锁已经不适合了,我们就会把轻量级锁升级为重量级锁了。

所以轻量级锁适合用在那种,很少出现多个线程竞争一个锁的情况,也就是说,适合那种多个线程总是错开时间来获取锁的情况。

偏向锁

偏向锁就更加牛逼了,我们已经觉得轻量级锁已经够轻,然而偏向锁更加省事,偏向锁认为,你轻量级锁每次进入一个方法都需要用CAS来改变状态,退出也需要改变,多麻烦。

偏向锁认为,其实对于一个方法,是很少有两个线程来执行的,搞来搞去,其实也就一个线程在执行这个方法而已,相当于单线程的情况,居然是单线程,那就没必要加锁了。

不过毕竟实际情况的多线程,单线程只是自己认为的而已了,所以呢,偏向锁进入一个方法的时候是这样处理的:如果这个方法没有人进来过,那么一个线程首次进入这个方法的时候,会采用CAS机制,把这个方法标记为有人在执行了,和轻量级锁加锁有点类似,并且也会把该线程的 ID 也记录进去,相当于记录了哪个线程在执行。

然后,但这个线程退出这个方法的时候,它不会改变这个方法的状态,而是直接退出来,懒的去改,因为它认为除了自己这个线程之外,其他线程并不会来执行这个方法。

然后当这个线程想要再次进入这个方法的时候,会判断一下这个方法的状态,如果这个方法已经被标记为有人在执行了,并且线程的ID是自己,那么它就直接进入这个方法执行,啥也不用做

你看,多方便,第一次进入需要CAS机制来设置,以后进出就啥也不用干了,直接进入退出。

然而,现实总是残酷的,毕竟实际情况还是多线程,所以万一有其他线程来进入这个方法呢?如果真的出现这种情况,其他线程一看这个方法的ID不是自己,这个时候说明,至少有两个线程要来执行这个方法论,这意味着偏向锁已经不适用了,这个时候就会从偏向锁升级为轻量级锁。

所以呢,偏向锁适用于那种,始终只有一个线程在执行一个方法的情况。

可重入锁

可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁。

Synchronized和ReentrantLock都是可重入锁。

悲观锁和乐观锁

最开始我们说的三种锁,重量级锁、自旋锁和自适应自旋锁,进入方法之前,就一定要先加一个锁,这种我们为称之为悲观锁。悲观锁总认为,如果不事先加锁的话,就会出事,这种想法确实悲观了点。

而乐观锁却相反,认为不加锁也没事,我们可以先不加锁,如果出现了冲突,我们在想办法解决,例如 CAS 机制,上面说的轻量级锁,就是乐观锁的。不会马上加锁,而是等待真的出现了冲突,在想办法解决。

markword

工具:JOL = Java Object Layout

1
2
3
4
5
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>

使用效果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class TestHashCode {
static class Student{
private String name;
public Student(String name) {
this.name = name;
}
}

public static void main(String[] args) {
Student student = new Student("张三");

System.out.println(Thread.currentThread().getName() + " # " + ClassLayout.parseInstance(student).toPrintable());
System.out.println(student.hashCode());
System.out.println(Thread.currentThread().getName() + " # " + ClassLayout.parseInstance(student).toPrintable());
}
}

“0110101 10000001 11000101 11110011”转为十进制就是“897697267”,所以如果不调用对象的hashcode方法,markword中就不会生成hashcode。

Synchronized锁升级

偏向锁未启动-锁升级

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
public class TestSyn {
static class Student{
private String name;
public Student(String name) {
this.name = name;
}
}

public static void main(String[] args) throws InterruptedException {
Student student = new Student("张三");
System.out.println(Thread.currentThread().getName() + " # " + ClassLayout.parseInstance(student).toPrintable());

Thread thread1 = new Thread(()->{
synchronized (student){
System.out.println("threadID # " + Thread.currentThread().getId());
System.out.println("thread hashcode # " + Thread.currentThread().hashCode());
System.out.println("thread hashcode # " + System.identityHashCode(Thread.currentThread()));
System.out.println(Thread.currentThread().getName() + " # " + ClassLayout.parseInstance(student).toPrintable());
try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {}
}
});
thread1.start();

TimeUnit.SECONDS.sleep(1);
synchronized (student) {
System.out.println("threadID # " + Thread.currentThread().getId());
System.out.println("thread hashcode # " + Thread.currentThread().hashCode());
System.out.println("thread hashcode # " + System.identityHashCode(Thread.currentThread()));
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName() + " # " + ClassLayout.parseInstance(student).toPrintable());
}

TimeUnit.SECONDS.sleep(4);
System.out.println(Thread.currentThread().getName() + " ## " + ClassLayout.parseInstance(student).toPrintable());
}
}

偏向锁未启动创建对象,对象处于无锁状态,线程1对对象加锁,对象处于轻量级锁状态,然后主线程尝试加锁,发生资源竞争,升级为重量级锁,最后线程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
public class TestSyn {

public static void main(String[] args) throws InterruptedException {
TimeUnit.SECONDS.sleep(5);
Object student = new Object();
System.out.println(Thread.currentThread().getName() + " # " + ClassLayout.parseInstance(student).toPrintable());

synchronized (student) {
System.out.println(Thread.currentThread().getName() + " # " + ClassLayout.parseInstance(student).toPrintable());
}
TimeUnit.SECONDS.sleep(1);

Thread thread1 = new Thread(()->{
synchronized (student){
System.out.println(Thread.currentThread().getName() + " # " + ClassLayout.parseInstance(student).toPrintable());
try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {}
}
});
thread1.start();

TimeUnit.SECONDS.sleep(1);
synchronized (student) {
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName() + " # " + ClassLayout.parseInstance(student).toPrintable());
}

thread1.join();
TimeUnit.SECONDS.sleep(4);
System.out.println(Thread.currentThread().getName() + " # " + ClassLayout.parseInstance(student).toPrintable());
}
}

主线程休眠5s后偏向锁已启动。创建对象后,对象处于偏向锁状态(匿名),主线程获取到了锁,对象处于偏向锁状态(非匿名)。主线程释放锁后,线程“thread1”获取到锁,对象处于轻量级锁状态,然后主线程也要获取锁,此时发生了竞争,则升级为重量级锁,最后线程“thread1”和主线程释放锁后,对象处于无锁状态。

上偏向锁,指的就是,把markword的线程ID改为自己线程ID的过程

如果有线程竞争,撤销偏向锁,升级轻量级锁,线程在自己的线程栈生成LockRecord ,用CAS操作将markword设置为指向自己这个线程的LR的指针,设置成功者得到锁

如果竞争加剧,有线程超过10次自旋, -XX:PreBlockSpin, 或者自旋线程数超过CPU核数的一半, 1.6之后,加入自适应自旋 (Adapative Self Spinning) ,由 JVM自己控制。

锁消除 lock eliminate

1
2
3
4
public void add(String str1,String str2){
StringBuffer sb = new StringBuffer();
sb.append(str1).append(str2);
}

我们都知道 StringBuffer 是线程安全的,因为它的关键方法都是被 synchronized 修饰过的,但我们看上面这段代码,我们会发现,sb 这个引用只会在 add 方法中使用,不可能被其它线程引用(因为是局部变量,栈私有),因此 sb 是不可能共享的资源,JVM 会自动消除 StringBuffer 对象内部的锁。

锁粗化 lock coarsening

1
2
3
4
5
6
7
8
9
public String test(String str){
int i = 0;
StringBuffer sb = new StringBuffer():
while(i < 100){
sb.append(str);
i++;
}
return sb.toString():
}

JVM 会检测到这样一连串的操作都对同一个对象加锁(while 循环内 100 次执行 append,没有锁粗化的就要进行 100 次加锁/解锁),此时 JVM 就会将加锁的范围粗化到这一连串的操作的外部(比如 while 虚幻体外),使得这一连串操作只需要加一次锁即可。

偏向锁默认延迟启动

默认情况 偏向锁有个时延,默认是4秒

因为JVM虚拟机自己有一些默认启动的线程,里面有好多sync代码,这些sync代码启动时就知道肯定会有竞争,如果使用偏向锁,就会造成偏向锁不断的进行锁撤销和锁升级的操作,效率较低。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class TestDelay {

static class Student{
private String name;

public Student(String name) {
this.name = name;
}
}

public static void main(String[] args) throws InterruptedException {
Student lisi = new Student("李四");
System.out.println(Thread.currentThread().getName() + " # " + ClassLayout.parseInstance(lisi).toPrintable());
TimeUnit.SECONDS.sleep(5);
Student zhangsan = new Student("张三");
System.out.println(Thread.currentThread().getName() + " # " + ClassLayout.parseInstance(zhangsan).toPrintable());
}
}

如果计算过对象的hashCode,则对象无法进入偏向状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class TestHashCode {
static class Student{
private String name;
public Student(String name) {
this.name = name;
}
}

public static void main(String[] args) throws InterruptedException {
TimeUnit.SECONDS.sleep(5);
Student student = new Student("张三");
System.out.println(student.hashCode());
System.out.println(Thread.currentThread().getName() + " # " + ClassLayout.parseInstance(student).toPrintable());
}
}

由上图可看出:如果计算过对象的hashCode,则对象无法进入偏向状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class TestHashCode {
static class Student{
private String name;
public Student(String name) {
this.name = name;
}
}

public static void main(String[] args) throws InterruptedException {
TimeUnit.SECONDS.sleep(5);
Student student = new Student("张三");
System.out.println(Thread.currentThread().getName() + " # " + ClassLayout.parseInstance(student).toPrintable());
System.out.println(student.hashCode());
System.out.println(Thread.currentThread().getName() + " # " + ClassLayout.parseInstance(student).toPrintable());
}
}

由上图可看出:如果对象处于匿名偏向锁状态,计算hashCode后,对象就会处于无锁状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class TestHashCode {
static class Student{
private String name;
public Student(String name) {
this.name = name;
}
}

public static void main(String[] args) throws InterruptedException {
TimeUnit.SECONDS.sleep(5);
Student student = new Student("张三");
synchronized (student){
System.out.println(Thread.currentThread().getName() + " # " + ClassLayout.parseInstance(student).toPrintable());
System.out.println(student.hashCode());
System.out.println(Thread.currentThread().getName() + " # " + ClassLayout.parseInstance(student).toPrintable());
}
}
}

由上图可看出:如果对象处于偏向锁(非匿名)状态,计算hashCode后,对象就会处于重量级锁状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class TestHashCode {
static class Student{
private String name;
public Student(String name) {
this.name = name;
}
}

public static void main(String[] args) throws InterruptedException {
Student student = new Student("张三");
synchronized (student){
System.out.println(Thread.currentThread().getName() + " # " + ClassLayout.parseInstance(student).toPrintable());
System.out.println(student.hashCode());
System.out.println(Thread.currentThread().getName() + " # " + ClassLayout.parseInstance(student).toPrintable());
}
}
}

由上图可看出:如果对象处于轻量级锁状态,计算hashCode后,对象就会处于重量级锁状态。

轻量级锁重量级锁的hashCode存在什么地方?

线程栈中,轻量级锁的LR中,或是代表重量级锁的ObjectMonitor的成员中

批量重偏向与批量撤销

从偏向锁的加锁解锁过程中可看出,当只有一个线程反复进入同步块时,偏向锁带来的性能开销基本可以忽略,但是当有其他线程尝试获得锁时,就需要等到safe point时,再将偏向锁撤销为无锁状态或升级为轻量级,会消耗一定的性能,所以在多线程竞争频繁的情况下,偏向锁不仅不能提高性能,还会导致性能下降。于是,就有了批量重偏向与批量撤销的机制。

原理以class为单位,为每个class维护解决场景批量重偏向(bulk rebias)机制是为了解决:一个线程创建了大量对象并执行了初始的同步操作,后来另一个线程也来将这些对象作为锁对象进行操作,这样会导致大量的偏向锁撤销操作。批量撤销(bulk revoke)机制是为了解决:在明显多线程竞争剧烈的场景下使用偏向锁是不合适的。

一个偏向锁撤销计数器,每一次该class的对象发生偏向撤销操作时,该计数器+1,当这个值达到重偏向阈值(默认20)时,JVM就认为该class的偏向锁有问题,因此会进行批量重偏向。每个class对象会有一个对应的epoch字段,每个处于偏向锁状态对象的Mark Word中也有该字段,其初始值为创建该对象时class中的epoch的值。每次发生批量重偏向时,就将该值+1,同时遍历JVM中所有线程的栈,找到该class所有正处于加锁状态的偏向锁,将其epoch字段改为新值。下次获得锁时,发现当前对象的epoch值和class的epoch不相等,那就算当前已经偏向了其他线程,也不会执行撤销操作,而是直接通过CAS操作将其Mark Word的Thread Id 改成当前线程Id。当达到重偏向阈值后,假设该class计数器继续增长,当其达到批量撤销的阈值后(默认40),JVM就认为该class的使用场景存在多线程竞争,会标记该class为不可偏向,之后,对于该class的锁,直接走轻量级锁的逻辑。

更多资料

  1. 偏向锁
  2. 死磕Synchronized底层实现
  3. Hotspot JVM锁是否可以降级?
  4. http://openjdk.java.net/groups/hotspot/docs/HotSpotGlossary.html

实现原理

语法层面

下面这个例子介绍了Synchronized的各种不同用法(实际上test1和test2都属于修饰代码块)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Test {
int count = 0;
static int static_count = 0;

public void test1(){
synchronized (this){
count++;
}
}

public void test2(){
synchronized (Test.class){
count++;
}
}

public synchronized void test3(){
count++;
}

public static synchronized void test4(){
static_count++;
}
}

字节码层面

先看下上面的例子去掉synchronized后生成的字节码(删减了一些没必要的内容):

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
{
int count;
descriptor: I
flags:

static int static_count;
descriptor: I
flags: ACC_STATIC

public void test1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: dup
2: getfield #2 // Field count:I
5: iconst_1
6: iadd
7: putfield #2 // Field count:I
10: return

public void test2();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: dup
2: getfield #2 // Field count:I
5: iconst_1
6: iadd
7: putfield #2 // Field count:I
10: return

public void test3();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: dup
2: getfield #2 // Field count:I
5: iconst_1
6: iadd
7: putfield #2 // Field count:I
10: return

public static void test4();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: getstatic #3 // Field static_count:I
3: iconst_1
4: iadd
5: putstatic #3 // Field static_count:I
8: return
}

然后是使用synchronized后生成的字节码(删减了一些没必要的内容):

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
{
int count;
descriptor: I
flags:

static int static_count;
descriptor: I
flags: ACC_STATIC

public void test1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: aload_0
5: dup
6: getfield #2 // Field count:I
9: iconst_1
10: iadd
11: putfield #2 // Field count:I
14: aload_1
15: monitorexit
16: goto 24
19: astore_2
20: aload_1
21: monitorexit
22: aload_2
23: athrow
24: return

public void test2();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=3, args_size=1
0: ldc #3 // class per/jaceding/myjava/syn/Test
2: dup
3: astore_1
4: monitorenter
5: aload_0
6: dup
7: getfield #2 // Field count:I
10: iconst_1
11: iadd
12: putfield #2 // Field count:I
15: aload_1
16: monitorexit
17: goto 25
20: astore_2
21: aload_1
22: monitorexit
23: aload_2
24: athrow
25: return

public synchronized void test3();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: dup
2: getfield #2 // Field count:I
5: iconst_1
6: iadd
7: putfield #2 // Field count:I
10: return

public static synchronized void test4();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=0, args_size=0
0: getstatic #4 // Field static_count:I
3: iconst_1
4: iadd
5: putstatic #4 // Field static_count:I
8: return
}

对比一下可以发现,使用synchronized修饰的代码块“test1”、“test2”的字节码中包含有monitorentermonitorexit指令,而使用synchronized修饰方法“test3”、“test4”的字节码中多了ACC_SYNCHRONIZED标签。

参考JVM规范中关于monitorenter和monitorexit的描述如下图:

简单翻译下:

synchronized修饰代码块时,()中必须是引用类型。

每个对象有一个监视器锁(monitor),当监视器锁被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:

  1. 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
  2. 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
  3. 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权

简单翻译下:

执行monitorexit的线程必须是与objectref引用的实例关联的monitor的所有者。

指令执行后,monitor的进入数减1,如果进入数为0,那线程就退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。

通过这两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。

ACC_SYNCHRONIZED标签不同于字节码指令,它是JVM运行时处理的。

JVM层面

在虚拟机的ObjectMonitor.hpp文件可以monitor监视器源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ObjectMonitor() {
_header = NULL;
_count = 0;
_waiters = 0,
_recursions = 0;// 线程重入次数
_object = NULL;// 存储Monitor对象
_owner = NULL;// 当前持有锁的线程
_WaitSet = NULL;// wait状态的线程列表
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ;// 处于等待锁状态block状态的线程列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}

monitorenter字节码指令对应 InterpreterRuntime:: monitorenter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
#ifdef ASSERT
thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
if (PrintBiasedLockingStatistics) {
Atomic::inc(BiasedLocking::slow_path_entry_count_addr());
}
Handle h_obj(thread, elem->obj());
assert(Universe::heap()->is_in_reserved_or_null(h_obj()),
"must be NULL or an object");
if (UseBiasedLocking) {
// Retry fast entry if bias is revoked to avoid unnecessary inflation
ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
} else {
ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
}
assert(Universe::heap()->is_in_reserved_or_null(elem->obj()),
"must be NULL or an object");
#ifdef ASSERT
thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
IRT_END
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
void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock, bool attempt_rebias, TRAPS) {
if (UseBiasedLocking) {
if (!SafepointSynchronize::is_at_safepoint()) {
BiasedLocking::Condition cond = BiasedLocking::revoke_and_rebias(obj, attempt_rebias, THREAD);
if (cond == BiasedLocking::BIAS_REVOKED_AND_REBIASED) {
return;
}
} else {
assert(!attempt_rebias, "can not rebias toward VM thread");
BiasedLocking::revoke_at_safepoint(obj);
}
assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now");
}

slow_enter (obj, lock, THREAD) ;
}

void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {
markOop mark = obj->mark();
assert(!mark->has_bias_pattern(), "should not see bias pattern here");

if (mark->is_neutral()) {
// Anticipate successful CAS -- the ST of the displaced mark must
// be visible <= the ST performed by the CAS.
lock->set_displaced_header(mark);
if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) {
TEVENT (slow_enter: release stacklock) ;
return ;
}
// Fall through to inflate() ...
} else
if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) {
assert(lock != mark->locker(), "must not re-lock the same lock");
assert(lock != (BasicLock*)obj->mark(), "don't relock with same BasicLock");
lock->set_displaced_header(NULL);
return;
}

#if 0
// The following optimization isn't particularly useful.
if (mark->has_monitor() && mark->monitor()->is_entered(THREAD)) {
lock->set_displaced_header (NULL) ;
return ;
}
#endif

// The object header will never be displaced to this lock,
// so it does not matter what the value is, except that it
// must be non-zero to avoid looking like a re-entrant lock,
// and must not look locked either.
lock->set_displaced_header(markOopDesc::unused_mark());
ObjectSynchronizer::inflate(THREAD, obj())->enter(THREAD);
}

inflate方法:是指膨胀为重量级锁

monitorenter字节码指令对应 InterpreterRuntime:: monitorexit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//%note monitor_1
IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorexit(JavaThread* thread, BasicObjectLock* elem))
#ifdef ASSERT
thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
Handle h_obj(thread, elem->obj());
assert(Universe::heap()->is_in_reserved_or_null(h_obj()),
"must be NULL or an object");
if (elem == NULL || h_obj()->is_unlocked()) {
THROW(vmSymbols::java_lang_IllegalMonitorStateException());
}
ObjectSynchronizer::slow_exit(h_obj(), elem->lock(), thread);
// Free entry. This must be done here, since a pending exception might be installed on
// exit. If it is not cleared, the exception handling code will try to unlock the monitor again.
elem->set_obj(NULL);
#ifdef ASSERT
thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
IRT_END

ACC_SYNCHRONIZED标签的处理在bytecodeinterpreter.cpp中的可以找到

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
103
104
105
106
107
108
109
110
111
112
// lock method if synchronized
if (METHOD->is_synchronized()) {
// oop rcvr = locals[0].j.r;
oop rcvr;
if (METHOD->is_static()) {
rcvr = METHOD->constants()->pool_holder()->java_mirror();
} else {
rcvr = LOCALS_OBJECT(0);
VERIFY_OOP(rcvr);
}
// The initial monitor is ours for the taking
BasicObjectLock* mon = &istate->monitor_base()[-1];
oop monobj = mon->obj();
assert(mon->obj() == rcvr, "method monitor mis-initialized");

bool success = UseBiasedLocking;
if (UseBiasedLocking) {
markOop mark = rcvr->mark();
if (mark->has_bias_pattern()) {
// The bias pattern is present in the object's header. Need to check
// whether the bias owner and the epoch are both still current.
intptr_t xx = ((intptr_t) THREAD) ^ (intptr_t) mark;
xx = (intptr_t) rcvr->klass()->prototype_header() ^ xx;
intptr_t yy = (xx & ~((int) markOopDesc::age_mask_in_place));
if (yy != 0 ) {
// At this point we know that the header has the bias pattern and
// that we are not the bias owner in the current epoch. We need to
// figure out more details about the state of the header in order to
// know what operations can be legally performed on the object's
// header.

// If the low three bits in the xor result aren't clear, that means
// the prototype header is no longer biased and we have to revoke
// the bias on this object.

if (yy & markOopDesc::biased_lock_mask_in_place == 0 ) {
// Biasing is still enabled for this data type. See whether the
// epoch of the current bias is still valid, meaning that the epoch
// bits of the mark word are equal to the epoch bits of the
// prototype header. (Note that the prototype header's epoch bits
// only change at a safepoint.) If not, attempt to rebias the object
// toward the current thread. Note that we must be absolutely sure
// that the current epoch is invalid in order to do this because
// otherwise the manipulations it performs on the mark word are
// illegal.
if (yy & markOopDesc::epoch_mask_in_place == 0) {
// The epoch of the current bias is still valid but we know nothing
// about the owner; it might be set or it might be clear. Try to
// acquire the bias of the object using an atomic operation. If this
// fails we will go in to the runtime to revoke the object's bias.
// Note that we first construct the presumed unbiased header so we
// don't accidentally blow away another thread's valid bias.
intptr_t unbiased = (intptr_t) mark & (markOopDesc::biased_lock_mask_in_place |
markOopDesc::age_mask_in_place |
markOopDesc::epoch_mask_in_place);
if (Atomic::cmpxchg_ptr((intptr_t)THREAD | unbiased, (intptr_t*) rcvr->mark_addr(), unbiased) != unbiased) {
CALL_VM(InterpreterRuntime::monitorenter(THREAD, mon), handle_exception);
}
} else {
try_rebias:
// At this point we know the epoch has expired, meaning that the
// current "bias owner", if any, is actually invalid. Under these
// circumstances _only_, we are allowed to use the current header's
// value as the comparison value when doing the cas to acquire the
// bias in the current epoch. In other words, we allow transfer of
// the bias from one thread to another directly in this situation.
xx = (intptr_t) rcvr->klass()->prototype_header() | (intptr_t) THREAD;
if (Atomic::cmpxchg_ptr((intptr_t)THREAD | (intptr_t) rcvr->klass()->prototype_header(),
(intptr_t*) rcvr->mark_addr(),
(intptr_t) mark) != (intptr_t) mark) {
CALL_VM(InterpreterRuntime::monitorenter(THREAD, mon), handle_exception);
}
}
} else {
try_revoke_bias:
// The prototype mark in the klass doesn't have the bias bit set any
// more, indicating that objects of this data type are not supposed
// to be biased any more. We are going to try to reset the mark of
// this object to the prototype value and fall through to the
// CAS-based locking scheme. Note that if our CAS fails, it means
// that another thread raced us for the privilege of revoking the
// bias of this particular object, so it's okay to continue in the
// normal locking code.
//
xx = (intptr_t) rcvr->klass()->prototype_header() | (intptr_t) THREAD;
if (Atomic::cmpxchg_ptr(rcvr->klass()->prototype_header(),
(intptr_t*) rcvr->mark_addr(),
mark) == mark) {
// (*counters->revoked_lock_entry_count_addr())++;
success = false;
}
}
}
} else {
cas_label:
success = false;
}
}
if (!success) {
markOop displaced = rcvr->mark()->set_unlocked();
mon->lock()->set_displaced_header(displaced);
if (Atomic::cmpxchg_ptr(mon, rcvr->mark_addr(), displaced) != displaced) {
// Is it simple recursive case?
if (THREAD->is_lock_owned((address) displaced->clear_lock_bits())) {
mon->lock()->set_displaced_header(NULL);
} else {
CALL_VM(InterpreterRuntime::monitorenter(THREAD, mon), handle_exception);
}
}
}
}
THREAD->clr_do_not_unlock();

汇编层面

汇编指令

1
lock comxchg ...
坚持原创技术分享,您的支持将鼓励我继续创作!