synchronized

synchronized

当我们使用synchronized关键字来修饰代码块时,字节码层面上是通过monitorenter与monitorexit指令来实现锁的获取与释放动作。
当线程进入到monitorenter指令后,线程将会持有Monitor对象,执行monitorexit指令后,线程将会释放Monitor对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 当我们使用synchronized关键字来修饰代码块时,字节码层面上是通过monitorenter与monitorexit指令来实现锁的获取与释放动作。
* 当线程进入到monitorenter指令后,线程将会持有Monitor对象,执行monitorexit指令后,线程将会释放Monitor对象。
*/
public class MyTest1 {
private Object object = new Object();
public void method() {
synchronized (object) {
System.out.println("hello world");
throw new RuntimeException();
}
}
public void method2() {
synchronized (object) {
System.out.println("welcome");
}
}
}
1
javap -c com.bob.concurrency3.MyTest1
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
Compiled from "MyTest1.java"
public class com.bob.concurrency3.MyTest1 {
public com.bob.concurrency3.MyTest1();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: new #2 // class java/lang/Object
8: dup
9: invokespecial #1 // Method java/lang/Object."<init>":()V
12: putfield #3 // Field object:Ljava/lang/Object;
15: return
public void method();
Code:
0: aload_0
1: getfield #3 // Field object:Ljava/lang/Object;
4: dup
5: astore_1
6: monitorenter
7: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
10: ldc #5 // String hello world
12: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
15: new #7 // class java/lang/RuntimeException
18: dup
19: invokespecial #8 // Method java/lang/RuntimeException."<init>":()V
22: athrow
23: astore_2
24: aload_1
25: monitorexit
26: aload_2
27: athrow
Exception table:
from to target type
7 26 23 any
public void method2();
Code:
0: aload_0
1: getfield #3 // Field object:Ljava/lang/Object;
4: dup
5: astore_1
6: monitorenter
7: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
10: ldc #9 // String welcome
12: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
15: aload_1
16: monitorexit
17: goto 25
20: astore_2
21: aload_1
22: monitorexit
23: aload_2
24: athrow
25: return
Exception table:
from to target type
7 17 20 any
20 23 20 any
}

对于synchronized关键字修饰方法来说并没有出现monitorenter与monitorexit指令,而是出现了一个ACC_SYNCHRONIZED标志。
JVM使用了ACC_SYNCHRONIZED标志来区分一个方法是否为同步方法;当方法被调用时,调用指令会检查该方法是否有ACC_SYNCHRONIZED标志,
如果有,那么执行线程将会先持有方法所在的Monitor对象,然后再去执行方法体;在该方法执行期间,其他任何线程均无法再获取到这个Monitor对象,
当线程执行完该方法后,它会释放掉这个Monitor对象。

1
2
3
4
5
6
7
public class MyTest2 {
public synchronized void method() {
System.out.println("hello world");
}
}
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 com.bob.concurrency3.MyTest2();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 10: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/bob/concurrency3/MyTest2;
public synchronized void method();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String hello world
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 13: 0
line 14: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lcom/bob/concurrency3/MyTest2;
}

当一个执行线程调用这个方法时,调用指令发现该方法同时有ACC_STATIC,ACC_SYNCHRONIZED标志,那么它就知道这个方法是一个静态的同步方法
那么执行线程将会先持有方法所在类的class对象的Monitor对象,然后再去执行方法体;在该方法执行期间,其他任何线程均无法再获取到这个方法所在类的class对象的Monitor对象,
当线程执行完该方法后,它会释放掉这个方法所在类的class对象的Monitor对象。

1
2
3
4
5
6
7
public class MyTest3 {
public static synchronized void method() {
System.out.println("hello world");
}
}
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
{
public com.bob.concurrency3.MyTest3();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 10: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/bob/concurrency3/MyTest3;
public static synchronized void method();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=0, args_size=0
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String hello world
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 13: 0
line 14: 8
}

JVM中的同步是基于进入与退出监视器对象(管程对象)(Monitor)来实现的,每个对象实例都会有一个Monitor对象,Monitor对象会和java对象一同创建并销毁,Monitor对象是由C++来实现的。
当多个线程同时访问一段同步代码时,这些线程会被放到一个EntryList集合中,处于阻塞状态的线程都会被放到该列表当中。接下来,当线程获取到对象的Monitor时,
Monitor是依赖于底层操作系统的mutex lock来实现互斥的,线程获取mutex成功,则会持有该mutex,这时其他线程就无法再获取到该mutex。

如果线程调用了wait方法,那么该线程就会释放掉所持有的mutex,并且该线程会进入到WaitSet集合(等待集合)中,等待下一次被其他线程调用notify/notifyAll唤醒。如果当前线程顺利执行完毕方法,
那么它也会释放掉所有mutex。

总结一下:
同步锁在这种实现方式中,因为Monitor是依赖于底层的操作系统实现,这样用户态与内核态之间的切换,所以会增加性能开销。

通过对象互斥锁的概念来保证共享数据操作的完整性。每个对象都对应于一个可称为[互斥锁]的标记,这个标记用于保证在任何时刻,只能有一个线程访问该对象。

那些处于EntryList与WaitSet中的线程均处于阻塞状态,阻塞操作是由操作系统来完成的,在linux下是通过pthread_mutex_lock函数实现的。
线程被阻塞后便会进入到内核调度状态,这会导致系统在用户态与内核态之间来回切换,严重影响锁的性能。

解决上述问题的办法便是自旋(Spin)。其原理是:当发生Monitor对象的争用时,若Owner能够在很短的时间内释放掉锁,则那些正在争用的线程就可以稍微等待一下(即所谓的自旋),
在Owner线程释放掉锁之后,争用线程可能会立即获取到锁,从而避免了系统阻塞。不过当Owner运行的时间超过了临界值后,争用线程自旋一段时间后依然无法获取到锁,
这时争用线程则会停止自旋而进入到阻塞状态。所以总体思想是:先自旋,不成功再进行阻塞,尽量降低阻塞可能性,这对那些执行时间很短的代码来说有极大的性能提升。显然,
自旋在多处理器(多核心)上才有意义。

互斥锁的属性:

  1. PTHREAD_MUTEX_TIMED_NP:这是缺省值,也就是普通锁。当一个线程加锁以后,其余请求锁的线程将会形成一个等待队列,并且在解锁后按照优先级获取到锁。这种策略可以确保资源分配的公平性。
  2. PTHREAD_MUTEX_RECURSIVE_NP:嵌套锁。允许一个线程对同一个锁成功获取多次,并通过unlock解锁。如果是不同线程请求,则在加锁线程解锁时重新竞争。
  3. PTHREAD_MUTEX_ERRORCHECK_NP:检错锁。如果一个线程请求同一个锁,则返回EDEADLK,否则与PTHREAD_MUTEX_TIMED_NP类型动作相同,这样就保证了当不允许多次加锁时不会出现最简单情况下的死锁。
  4. PTHREAD_MUTEX_ADAPTIVE_NP:适应锁。动作最简单的锁类型,仅仅等待解锁后重新竞争。

在jdk1.5之前,我们若想实现线程同步,只能通过synchronized关键字这一种方式来达成;底层,Java也是通过synchronized关键字来做数据的
原子性维护的;synchronized关键字是JVM实现的一种内置锁,从底层角度来说,这种锁的获取与释放都是由JVM帮助我们隐式实现的。

从JDK1.5开始,并发包引入了Lock锁,Lock同步锁是基于Java来实现的,因此锁的获取与释放是通过Java代码来实现与控制的;然而,synchronized
是基于底层操作系统的Mutex Lock来实现的,每次对锁的获取与释放动作都会带来用户态与内核态之间的切换,这种切换会极大地增加系统的负担。在并发量
较高时,也就是锁的竞争比较激烈时,synchronized锁在性能上的表现就非常差。

从JDK1.6开始,synchronized锁的实现发生了很大的变化;JVM引入了相应的优化手段来提升synchronized锁的性能,这种提升涉及到偏向锁、轻量级锁
重量级锁等,从而减少锁的竞争所带来的用户态与内核态之间的切换;这种锁的优化实际上是通过Java对象头中的一些标志位来实现的;对于锁的访问与改变,
实际上都与Java对象头息息相关。

从JDK1.6开始,对象实例在堆当中会被划分为三个组成部分:对象头、实例数据与对齐填充。
对象头主要也是有由3块内容来构成:

  1. Mark Word
  2. 指向类的指针
  3. 数组长度

其中Mark Word(它记录了对象、锁及垃圾回收相关的信息,在64位的JVM中,其长度也是64bit)的位信息包括如下组成部分:

  1. 无锁标记
  2. 偏向锁标记
  3. 轻量级锁标记
  4. 重量级锁标记
  5. GC标记

对于synchronized锁来说,锁的升级主要是通过Mark Word中的标志位与是否是偏向标志位来达成的;synchronized关键字所对应的锁都是先从偏向锁
开始,随着锁竞争不断升级,逐步演化至轻量级锁,最后则变成了重量级锁。

对于锁的演化来说,它会经历如下阶段:
无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁

偏向锁:
针对于一个线程来说的,它的主要作用就是优化同一个线程多次获取一个锁的情况;如果一个synchronized方法被一个线程访问,那么这个方法所在的对象
就会在其Mark Word中将偏向锁进行标记,同时还会有一个字段来存储该线程的ID;当这个线程再次访问同一个synchronized方法时,它会检查这个对象的
Mark Word的偏向锁标记以及是否指向了其线程ID,如果是的话,那么该线程就无需再去进入管程(Monitor)了,而是直接进入该方法体中。

如果是另外一个线程访问了这个synchronized方法,那么实际情况会如何呢?
偏向锁会被取消掉。

轻量级锁:
若第一个线程已经获取到了当前对象的锁,这时第二个线程又开始尝试争抢该对象的锁,由于该对象的锁已经被第一个线程获取到,因此它是偏向锁,而第二个线程
在争抢时,会发现该对象头中的Mark Word已经是偏向锁,但里面存储的线程ID并不是自己(是第一个线程),那么它会进行CAS(Compare and Swap),从而
获取到锁,这里面存在另种情况:

  1. 获取锁成功:那么它会直接将Mark Word中的线程ID由第一个变成自己(偏向锁标记位不变),这样该对象依然会保持偏向锁的状态。
  2. 获取锁失败:则表示这时可能会有多个线程同时在尝试争抢该对象的锁,那么这是偏向锁就会进行升级,升级为轻量级锁。

自旋锁:自旋是轻量级锁的一种手段
若自旋失败(依然无法获取到锁),那么锁就会转化为重量级锁,在这种情况下,无法获取到锁的线程都会进入到Monitor(即内核)
自旋最大的一个特点就是避免了线程从用户态进入到内核态。

重量级锁:
线程最终从用户态进入到了内核态。

编译器对于锁的优化措施:
JIT编译器(Just In Time编译器)可以在动态编译同步代码时,使用一种叫做逃逸分析的技术,来通过该项技术判别程序中所使用的锁对象是否只被
一个线程所使用,而没有散布到其他线程当中;如果情况就是这样的话,那么JIT编译器在编译这个同步代码时就不会生成synchronized关键字所标识
的锁申请与释放机器码,从而消除了锁的使用流程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MyTest4 {
// private Object object = new Object();
public void method() {
Object object = new Object();
synchronized (object) {
System.out.println("hello world");
}
}
}

锁粗化

JIT编译器在动态编译时,若发现前后相邻的synchronized块使用的是同一个锁对象,那么它就会把这几个synchronized块给合并为一个较大
的同步块,这样做的好处在于线程在执行这些代码是,就无需频繁申请与释放锁了,从而达到申请与释放锁一次,就可以执行完全部的同步代码块,从而
提升性能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MyTest5 {
private Object object = new Object();
public void method() {
synchronized (object) {
System.out.println("hello world");
}
synchronized (object) {
System.out.println("welcome");
}
synchronized (object) {
System.out.println("person");
}
}
}