mirror of
https://github.com/Snailclimb/JavaGuide
synced 2025-06-16 18:10:13 +08:00
422 lines
12 KiB
Markdown
422 lines
12 KiB
Markdown
## synchronized / Lock
|
||
|
||
1. JDK 1.5之前
|
||
|
||
,Java通过
|
||
|
||
synchronized
|
||
|
||
关键字来实现
|
||
|
||
锁
|
||
|
||
功能
|
||
|
||
- synchronized是JVM实现的**内置锁**,锁的获取和释放都是由JVM**隐式**实现的
|
||
|
||
2. JDK 1.5
|
||
|
||
,并发包中新增了
|
||
|
||
Lock接口
|
||
|
||
来实现锁功能
|
||
|
||
- 提供了与synchronized类似的同步功能,但需要**显式**获取和释放锁
|
||
|
||
3. Lock同步锁是基于
|
||
|
||
Java
|
||
|
||
实现的,而synchronized是基于底层操作系统的
|
||
|
||
Mutex Lock
|
||
|
||
实现的
|
||
|
||
- 每次获取和释放锁都会带来**用户态和内核态的切换**,从而增加系统的**性能开销**
|
||
- 在锁竞争激烈的情况下,synchronized同步锁的性能很糟糕
|
||
- 在**JDK 1.5**,在**单线程重复申请锁**的情况下,synchronized锁性能要比Lock的性能**差很多**
|
||
|
||
4. **JDK 1.6**,Java对synchronized同步锁做了**充分的优化**,甚至在某些场景下,它的性能已经超越了Lock同步锁
|
||
|
||
|
||
|
||
## 实现原理
|
||
|
||
复制
|
||
|
||
```
|
||
public class SyncTest {
|
||
public synchronized void method1() {
|
||
}
|
||
|
||
public void method2() {
|
||
Object o = new Object();
|
||
synchronized (o) {
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
复制
|
||
|
||
```
|
||
$ javac -encoding UTF-8 SyncTest.java
|
||
$ javap -v SyncTest
|
||
```
|
||
|
||
### 修饰方法
|
||
|
||
复制
|
||
|
||
```
|
||
public synchronized void method1();
|
||
descriptor: ()V
|
||
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
|
||
Code:
|
||
stack=0, locals=1, args_size=1
|
||
0: return
|
||
```
|
||
|
||
1. JVM使用**ACC_SYNCHRONIZED**访问标识来区分一个方法是否为**同步方法**
|
||
|
||
2. 在方法调用时,会检查方法是否被设置了
|
||
|
||
ACC_SYNCHRONIZED
|
||
|
||
访问标识
|
||
|
||
- 如果是,执行线程会将先尝试**持有Monitor对象**,再执行方法,方法执行完成后,最后**释放Monitor对象**
|
||
|
||
### 修饰代码块
|
||
|
||
复制
|
||
|
||
```
|
||
public void method2();
|
||
descriptor: ()V
|
||
flags: ACC_PUBLIC
|
||
Code:
|
||
stack=2, locals=4, args_size=1
|
||
0: new #2 // class java/lang/Object
|
||
3: dup
|
||
4: invokespecial #1 // Method java/lang/Object."<init>":()V
|
||
7: astore_1
|
||
8: aload_1
|
||
9: dup
|
||
10: astore_2
|
||
11: monitorenter
|
||
12: aload_2
|
||
13: monitorexit
|
||
14: goto 22
|
||
17: astore_3
|
||
18: aload_2
|
||
19: monitorexit
|
||
20: aload_3
|
||
21: athrow
|
||
22: return
|
||
```
|
||
|
||
1. synchronized修饰同步代码块时,由**monitorenter**和**monitorexit**指令来实现同步
|
||
2. 进入**monitorenter**指令后,线程将**持有**该**Monitor对象**,进入**monitorexit**指令,线程将**释放**该**Monitor对象**
|
||
|
||
### 管程模型
|
||
|
||
1. JVM中的**同步**是基于进入和退出**管程**(**Monitor**)对象实现的
|
||
|
||
2. **每个Java对象实例都会有一个Monitor**,Monitor可以和Java对象实例一起被创建和销毁
|
||
|
||
3. Monitor是由**ObjectMonitor**实现的,对应[ObjectMonitor.hpp](https://github.com/JetBrains/jdk8u_hotspot/blob/master/src/share/vm/runtime/objectMonitor.hpp)
|
||
|
||
4. 当多个线程同时访问一段同步代码时,会先被放在**EntryList**中
|
||
|
||
5. 当线程获取到Java对象的Monitor时(Monitor是依靠
|
||
|
||
底层操作系统
|
||
|
||
的
|
||
|
||
Mutex Lock
|
||
|
||
来实现
|
||
|
||
互斥
|
||
|
||
的)
|
||
|
||
- 线程申请Mutex成功,则持有该Mutex,其它线程将无法获取到该Mutex
|
||
|
||
6. 进入
|
||
|
||
WaitSet
|
||
|
||
- 竞争锁**失败**的线程会进入**WaitSet**
|
||
- 竞争锁**成功**的线程如果调用**wait**方法,就会**释放当前持有的Mutex**,并且该线程会进入**WaitSet**
|
||
- 进入**WaitSet**的进程会等待下一次唤醒,然后进入EntryList**重新排队**
|
||
|
||
7. 如果当前线程顺利执行完方法,也会释放Mutex
|
||
|
||
8. Monitor依赖于**底层操作系统**的实现,存在**用户态**和**内核态之间**的**切换**,所以增加了**性能开销**
|
||
|
||
[](https://java-performance-1253868755.cos.ap-guangzhou.myqcloud.com/java-performance-synchronized-monitor.png)
|
||
|
||
复制
|
||
|
||
```
|
||
ObjectMonitor() {
|
||
_header = NULL;
|
||
_count = 0; // 记录个数
|
||
_waiters = 0,
|
||
_recursions = 0;
|
||
_object = NULL;
|
||
_owner = NULL; // 持有该Monitor的线程
|
||
_WaitSet = NULL; // 处于wait状态的线程,会被加入 _WaitSet
|
||
_WaitSetLock = 0 ;
|
||
_Responsible = NULL ;
|
||
_succ = NULL ;
|
||
_cxq = NULL ;
|
||
FreeNext = NULL ;
|
||
_EntryList = NULL ; // 多个线程访问同步块或同步方法,会首先被加入 _EntryList
|
||
_SpinFreq = 0 ;
|
||
_SpinClock = 0 ;
|
||
OwnerIsThread = 0 ;
|
||
_previous_owner_tid = 0;
|
||
}
|
||
```
|
||
|
||
## 锁升级优化
|
||
|
||
1. 为了提升性能,在**JDK 1.6**引入**偏向锁、轻量级锁、重量级锁**,用来**减少锁竞争带来的上下文切换**
|
||
2. 借助JDK 1.6新增的**Java对象头**,实现了**锁升级**功能
|
||
|
||
### Java对象头
|
||
|
||
1. 在**JDK 1.6**的JVM中,对象实例在**堆内存**中被分为三部分:**对象头**、**实例数据**、**对齐填充**
|
||
2. 对象头的组成部分:**Mark Word**、**指向类的指针**、**数组长度**(可选,数组类型时才有)
|
||
3. Mark Word记录了**对象**和**锁**有关的信息,在64位的JVM中,Mark Word为**64 bit**
|
||
4. 锁升级功能主要依赖于Mark Word中**锁标志位**和**是否偏向锁标志位**
|
||
5. synchronized同步锁的升级优化路径:***偏向锁** -> **轻量级锁** -> **重量级锁***
|
||
|
||
[](https://java-performance-1253868755.cos.ap-guangzhou.myqcloud.com/java-performance-synchronized-mark-word.jpg)
|
||
|
||
### 偏向锁
|
||
|
||
1. 偏向锁主要用来优化**同一线程多次申请同一个锁**的竞争,在某些情况下,大部分时间都是同一个线程竞争锁资源
|
||
|
||
2. 偏向锁的作用
|
||
|
||
- 当一个线程再次访问同一个同步代码时,该线程只需对该对象头的**Mark Word**中去判断是否有偏向锁指向它
|
||
- **无需再进入Monitor去竞争对象**(避免用户态和内核态的**切换**)
|
||
|
||
3. 当对象被当做同步锁,并有一个线程抢到锁时
|
||
|
||
- 锁标志位还是**01**,是否偏向锁标志位设置为**1**,并且记录抢到锁的**线程ID**,进入***偏向锁状态***
|
||
|
||
4. 偏向锁
|
||
|
||
**不会主动释放锁**
|
||
|
||
- 当线程1再次获取锁时,会比较**当前线程的ID**与**锁对象头部的线程ID**是否一致,如果一致,无需CAS来抢占锁
|
||
|
||
- 如果不一致,需要查看
|
||
|
||
锁对象头部记录的线程
|
||
|
||
是否存活
|
||
|
||
- 如果**没有存活**,那么锁对象被重置为**无锁**状态(也是一种撤销),然后重新偏向线程2
|
||
|
||
- 如果
|
||
|
||
存活
|
||
|
||
,查找线程1的栈帧信息
|
||
|
||
- 如果线程1还是需要继续持有该锁对象,那么暂停线程1(**STW**),**撤销偏向锁**,**升级为轻量级锁**
|
||
- 如果线程1不再使用该锁对象,那么将该锁对象设为**无锁**状态(也是一种撤销),然后重新偏向线程2
|
||
|
||
5. 一旦出现其他线程竞争锁资源时,偏向锁就会被
|
||
|
||
撤销
|
||
|
||
- 偏向锁的撤销**可能需要**等待**全局安全点**,暂停持有该锁的线程,同时检查该线程**是否还在执行该方法**
|
||
- 如果还没有执行完,说明此刻有**多个线程**竞争,升级为**轻量级锁**;如果已经执行完毕,唤醒其他线程继续**CAS**抢占
|
||
|
||
6. 在
|
||
|
||
高并发
|
||
|
||
场景下,当
|
||
|
||
大量线程
|
||
|
||
同时竞争同一个锁资源时,偏向锁会被
|
||
|
||
撤销
|
||
|
||
,发生
|
||
|
||
STW
|
||
|
||
,加大了
|
||
|
||
性能开销
|
||
|
||
- 默认配置
|
||
|
||
- `-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=4000`
|
||
- 默认开启偏向锁,并且**延迟生效**,因为JVM刚启动时竞争非常激烈
|
||
|
||
- 关闭偏向锁
|
||
|
||
- `-XX:-UseBiasedLocking`
|
||
|
||
- 直接
|
||
|
||
设置为重量级锁
|
||
|
||
- `-XX:+UseHeavyMonitors`
|
||
|
||
红线流程部分:偏向锁的**获取**和**撤销**
|
||
[](https://java-performance-1253868755.cos.ap-guangzhou.myqcloud.com/java-performance-synchronized-lock-upgrade-1.png)
|
||
|
||
### 轻量级锁
|
||
|
||
1. 当有另外一个线程竞争锁时,由于该锁处于**偏向锁**状态
|
||
|
||
2. 发现对象头Mark Word中的线程ID不是自己的线程ID,该线程就会执行
|
||
|
||
CAS
|
||
|
||
操作获取锁
|
||
|
||
- 如果获取**成功**,直接替换Mark Word中的线程ID为自己的线程ID,该锁会***保持偏向锁状态***
|
||
- 如果获取**失败**,说明当前锁有一定的竞争,将偏向锁**升级**为轻量级锁
|
||
|
||
3. 线程获取轻量级锁时会有两步
|
||
|
||
- 先把**锁对象的Mark Word**复制一份到线程的**栈帧**中(**DisplacedMarkWord**),主要为了**保留现场**!!
|
||
- 然后使用**CAS**,把对象头中的内容替换为**线程栈帧中DisplacedMarkWord的地址**
|
||
|
||
4. 场景
|
||
|
||
- 在线程1复制对象头Mark Word的同时(CAS之前),线程2也准备获取锁,也复制了对象头Mark Word
|
||
- 在线程2进行CAS时,发现线程1已经把对象头换了,线程2的CAS失败,线程2会尝试使用**自旋锁**来等待线程1释放锁
|
||
|
||
5. 轻量级锁的适用场景:线程**交替执行**同步块,***绝大部分的锁在整个同步周期内都不存在长时间的竞争***
|
||
|
||
红线流程部分:升级轻量级锁
|
||
[](https://java-performance-1253868755.cos.ap-guangzhou.myqcloud.com/java-performance-synchronized-lock-upgrade-2.png)
|
||
|
||
### 自旋锁 / 重量级锁
|
||
|
||
1. 轻量级锁
|
||
|
||
CAS
|
||
|
||
抢占失败,线程将会被挂起进入
|
||
|
||
阻塞
|
||
|
||
状态
|
||
|
||
- 如果正在持有锁的线程在**很短的时间**内释放锁资源,那么进入**阻塞**状态的线程被**唤醒**后又要**重新抢占**锁资源
|
||
|
||
2. JVM提供了**自旋锁**,可以通过**自旋**的方式**不断尝试获取锁**,从而***避免线程被挂起阻塞***
|
||
|
||
3. 从
|
||
|
||
JDK 1.7
|
||
|
||
开始,
|
||
|
||
自旋锁默认启用
|
||
|
||
,自旋次数
|
||
|
||
不建议设置过大
|
||
|
||
(意味着
|
||
|
||
长时间占用CPU
|
||
|
||
)
|
||
|
||
- `-XX:+UseSpinning -XX:PreBlockSpin=10`
|
||
|
||
4. 自旋锁重试之后如果依然抢锁失败,同步锁会升级至
|
||
|
||
重量级锁
|
||
|
||
,锁标志位为
|
||
|
||
10
|
||
|
||
- 在这个状态下,未抢到锁的线程都会**进入Monitor**,之后会被阻塞在**WaitSet**中
|
||
|
||
5. 在
|
||
|
||
锁竞争不激烈
|
||
|
||
且
|
||
|
||
锁占用时间非常短
|
||
|
||
的场景下,自旋锁可以提高系统性能
|
||
|
||
- 一旦锁竞争激烈或者锁占用的时间过长,自旋锁将会导致大量的线程一直处于**CAS重试状态**,**占用CPU资源**
|
||
|
||
6. 在
|
||
|
||
高并发
|
||
|
||
的场景下,可以通过
|
||
|
||
关闭自旋锁
|
||
|
||
来优化系统性能
|
||
|
||
- ```
|
||
-XX:-UseSpinning
|
||
```
|
||
|
||
- 关闭自旋锁优化
|
||
|
||
- ```
|
||
-XX:PreBlockSpin
|
||
```
|
||
|
||
- 默认的自旋次数,在**JDK 1.7**后,**由JVM控制**
|
||
|
||
[](https://java-performance-1253868755.cos.ap-guangzhou.myqcloud.com/java-performance-synchronized-lock-upgrade-3.png)
|
||
|
||
## 小结
|
||
|
||
1. JVM在**JDK 1.6**中引入了**分级锁**机制来优化synchronized
|
||
|
||
2. 当一个线程获取锁时,首先对象锁成为一个
|
||
|
||
偏向锁
|
||
|
||
- 这是为了避免在**同一线程重复获取同一把锁**时,**用户态和内核态频繁切换**
|
||
|
||
3. 如果有多个线程竞争锁资源,锁将会升级为
|
||
|
||
轻量级锁
|
||
|
||
- 这适用于在**短时间**内持有锁,且分锁**交替切换**的场景
|
||
- 轻量级锁还结合了**自旋锁**来**避免线程用户态与内核态的频繁切换**
|
||
|
||
4. 如果锁竞争太激烈(自旋锁失败),同步锁会升级为重量级锁
|
||
|
||
5. 优化synchronized同步锁的关键:
|
||
|
||
减少锁竞争
|
||
|
||
- 应该尽量使synchronized同步锁处于**轻量级锁**或**偏向锁**,这样才能提高synchronized同步锁的性能
|
||
- 常用手段
|
||
- **减少锁粒度**:降低锁竞争
|
||
- **减少锁的持有时间**,提高synchronized同步锁在自旋时获取锁资源的成功率,**避免升级为重量级锁**
|
||
|
||
6. 在**锁竞争激烈**时,可以考虑**禁用偏向锁**和**禁用自旋锁** |