synchronized解析

synchronized

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
public static final Object LOCK = new Object();

public void m1() {
synchronized (LOCK) {

}

}

public synchronized void m2() {

}

编译 -> 字节码

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
// access flags 0x
public final static Ljava/lang/Object; LOCK

// access flags 0x
public m1()V
TRYCATCHBLOCK L0 L1 L2 null
TRYCATCHBLOCK L2 L3 L2 null
L
LINENUMBER 13 L
GETSTATIC com/hh/skilljava/javabase/juc/SynchronizeTest.LOCK :
Ljava/lang/Object;
DUP
ASTORE 1
MONITORENTER // MONITORENTER
L
LINENUMBER 15 L
ALOAD 1
MONITOREXIT
L
GOTO L
L
FRAME FULL [com/hh/skilljava/javabase/juc/SynchronizeTest
java/lang/Object] [java/lang/Throwable]
ASTORE 2
ALOAD 1
MONITOREXIT // MONITOREXIT
L
ALOAD 2
ATHROW
L
LINENUMBER 16 L
FRAME CHOP 1
RETURN
L
LOCALVARIABLE this Lcom/hh/skilljava/javabase/juc/SynchronizeTest; L
L6 0
MAXSTACK = 2
MAXLOCALS = 3

// access flags 0x
public synchronized m2()V
L
LINENUMBER 20 L
RETURN
L
LOCALVARIABLE this Lcom/hh/skilljava/javabase/juc/SynchronizeTest; L
L1 0
MAXSTACK = 0
MAXLOCALS = 1

Synchronized在修饰同步代码块时,是由monitorenter和monitorexit指令来实现同步的。

进入monitorenter指令后,线程将持有Monitor对象,退出 monitorenter指令后,线程将释放该Monitor对象。(修饰方法,使用标识符,原理相同)

Monitor

JVM中的同步是基于进入和退出管程(Monitor)对象实现的。每个对象实例都会有一个Monitor,Monitor可以和对象一起创建、销毁。Monitor是由ObjectMonitor实现,而ObjectMonitor 是由 C++ 的 ObjectMonitor.hpp文件实现,如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ObjectMonitor() {
_header = NULL;
_count = 0; // 记录个数
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL; // 处于 wait 状态的线程,会被加入到 _WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; // 处于等待锁 block 状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}

当多个线程同时访问一段同步代码时,多个线程会先被存放在EntryList集合中,处于block状态的线程,都会被加入到该列表。接下来当线程获取到对象的Monitor时,Monitor是依靠底层操作系统的Mutex Lock来实现互斥的,线程申请Mutex成功,则持有该Mutex,其它线程将无法获取到该Mutex。

因Monitor是依赖于底层的操作系统实现,存在用户态与内核态之间的切换,所以增加了性能开销。

Synchronized 优化

为了提升性能,JDK1.6 引入了偏向锁、轻量级锁、重量级锁概念,来减少锁竞争带来的上下文切换,而正是新增的 Java 对象头实现了锁升级功能

对象头

对象实例在堆内存中被分为了三个部分:对象头、实例数据和对⻬填充。其中 Java 对象头由 Mark Word、指向类的指针以及数组⻓度三部分组成。
Mark Word记录了对象和锁有关的信息。Mark Word在64位JVM中的⻓度是64bit,我们可以一起看下Mark Word在64位JVM的存储结构是怎么样的。如下图所示

img.png

锁升级功能主要依赖于 Mark Word 中的锁标志位和释放偏向锁标志位,Synchronized同步锁就是从偏向锁开始的,随着竞争越来越激烈,偏向锁升级到轻量级锁,最终升级到重量级锁。

偏向锁

当没有竞争出现时,默认会使用偏向锁。JVM会利用CAS操作,在对象头上的Mark Word部分设置线程 ID,以表示这个对象偏向于当前线程。偏向锁的作用就是,当一个线程再次
访问这个同步代码或方法时,该线程只需去对象头的Mark Word中去判断一下是否有偏向锁指向它的ID,无需再进入Monitor去竞争对象了。

偏向锁主要用来优化同一线程多次申请同一个锁的竞争(例如单线程操作线程安全的集合)。

在高并发场景下,当大量线程同时竞争同一个锁资源时,偏向锁就会被撤销,偏向锁的撤销需要等待全局安全点,暂停持有该锁的线程, 开启偏向锁无疑会带来更大的性能开销,这时我们可以通过添加JVM参数关闭偏向锁来调优系统性能。

轻量级锁

当有另外一个线程竞争获取这个锁时,由于该锁已经是偏向锁,当发现对象Mark Word中的线程ID不是自己的线程ID,就会进行CAS操作获取锁,如果获取成功,直接替换Mark Word中的线程ID为自己的ID,该锁会保持偏向锁状态;如果获取锁失败,代表当前
锁有一定的竞争,偏向锁将升级为轻量级锁。

轻量级锁适用于线程交替执行同步块的场景,绝大部分的锁在整个同步周期内都不存在⻓时间的竞争。

自旋锁和重量级锁

从JDK1.7开始,自旋锁默认启用,自旋次数由JVM设置决定。

在尝试升级轻量级锁CAS抢锁失败时,会进行自旋,自旋重试上限后依然没有抢到锁,线程将会被挂起进入阻塞状态。升级为重量级锁,这个状态下,未抢到锁的线程都会进入Monitor,被阻塞在_waitSet 队列中。

1
-XX:-UseBiasedLocking // 关闭偏向锁(默认打开)

动态编译实现锁消除 / 锁粗化

Java还使用了编译器对锁进行优化。JIT编译器在动态编译同步块的时候,借助了一种被称为逃逸分析的技术,来判断同步代码块使用的锁对象是否只能够被一个线程访问,编译时直接不生成Synchronized对应的机器码。

锁粗化,就是在 JIT 编译器动态编译时,如果发现几个相邻的同步块使用的是同一个锁实例,那么 JIT 编译器将会把这几个同步块合并为一个大的同步块,从而避免一个线程“反复申请、释放同一个锁“所带来的性能开销。

代码层优化

减小锁粒度,分段锁..