doc: 阅读锁升级相关文档
This commit is contained in:
@@ -77,6 +77,15 @@
|
|||||||
- [符号引用](#符号引用)
|
- [符号引用](#符号引用)
|
||||||
- [直接引用](#直接引用)
|
- [直接引用](#直接引用)
|
||||||
- [初始化](#初始化-1)
|
- [初始化](#初始化-1)
|
||||||
|
- [锁优化](#锁优化)
|
||||||
|
- [自旋锁和自适应自旋](#自旋锁和自适应自旋)
|
||||||
|
- [锁消除](#锁消除)
|
||||||
|
- [锁粗化](#锁粗化)
|
||||||
|
- [轻量级锁](#轻量级锁)
|
||||||
|
- [Hotspot对象头内存布局](#hotspot对象头内存布局)
|
||||||
|
- [轻量级锁加锁过程](#轻量级锁加锁过程)
|
||||||
|
- [轻量级锁的解锁过程](#轻量级锁的解锁过程)
|
||||||
|
- [偏向锁](#偏向锁)
|
||||||
|
|
||||||
|
|
||||||
# 深入理解java虚拟机
|
# 深入理解java虚拟机
|
||||||
@@ -686,4 +695,91 @@ public static final int value = 123
|
|||||||
### 初始化
|
### 初始化
|
||||||
类初始化是类加载过程中的最后一步。在准备阶段,变量已经赋值过一次初始值,而在初始化阶段,`则会执行类构造器<clinit>()方法`。
|
类初始化是类加载过程中的最后一步。在准备阶段,变量已经赋值过一次初始值,而在初始化阶段,`则会执行类构造器<clinit>()方法`。
|
||||||
|
|
||||||
|
## 锁优化
|
||||||
|
### 自旋锁和自适应自旋
|
||||||
|
在许多场景下,共享数据的锁定时间只会维持很短一段时间,为此挂起和恢复线程并不值得。对此,可以令后请求锁的线程`等待但并不放弃处理器的执行时间`,看看持有锁的线程是否会很快释放锁。为此,让线程执行`忙自旋`即可。
|
||||||
|
|
||||||
|
自旋锁并不能代替阻塞,自旋等待虽然避免了线程切换的开销,但是却要占用处理器时间:
|
||||||
|
- 如果锁被占用的时间很短,那么自旋等待的效果就较好
|
||||||
|
- 如果锁被占用的时间很长,那么自旋线程只会白白浪费处理器资源
|
||||||
|
|
||||||
|
故而,自旋等待必须要有一定的限度,如果自旋超过限定次数仍然没有获取锁,就应当使用传统方式去挂起线程。自旋的默认次数为10次。
|
||||||
|
|
||||||
|
在jdk 1.6中,引入了自适应的自旋锁,自适应代表自旋时间不再固定了,而是由前一次在同一锁上的自旋时间以及锁拥有者的状态来决定:
|
||||||
|
- 如果在同一锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机会认为这次自旋也很有可能再次成功,其会允许自旋等待持续相对更长的时间,例如100个循环
|
||||||
|
- 如果对于某个锁,自旋很少成功获得过,那么在以后获得这个锁时可能会省略自旋过程,以避免浪费处理器资源
|
||||||
|
|
||||||
|
拥有自适应自旋,随着程序的运行和性能监控信息的不断完善,虚拟机对程序锁状况的预测会越来越准确
|
||||||
|
|
||||||
|
### 锁消除
|
||||||
|
在即时编译器运行时,对于`代码上要求同步,但经检测后不可能存在共享数据竞争`的锁进行消除。锁消除的主要判断依据为`逃逸分析`。如果判断在一段代码中,堆上所有的数据都不可能逃逸出去被其他线程访问到,那么可以将其看作是栈上数据,认为它们是线程私有的,自然无需进行同步加锁。
|
||||||
|
|
||||||
|
### 锁粗化
|
||||||
|
原则上,在编写代码时,会倾向将同步块的作用范围限制的尽可能小,只在共享数据的实际作用域中才进行同步。这样,可以令需要同步的操作数量尽可能变少,如存在锁竞争,能够尽快拿到锁。
|
||||||
|
|
||||||
|
但是,如果存在一系列的连续操作都对同一个对象反复加锁/释放锁,甚至加锁操作出现在循环中,那么即使没有线程竞争,频繁的对锁进行获取和释放也会带来不必要的性能损耗。
|
||||||
|
|
||||||
|
故而,如果虚拟机检测到有这样一串零碎操作都对同一对象加锁,会将加锁的同步范围拓展(粗化)到整个操作序列外部。
|
||||||
|
|
||||||
|
### 轻量级锁
|
||||||
|
在jdk 1.6中,加入了轻量级锁这一新型锁机制。`轻量级`是相对传统基于操作系统互斥量实现的锁来说的。
|
||||||
|
|
||||||
|
轻量级锁并非用于替代重量级锁,其用途是在没有多线程竞争的前提下,减少传统重量级锁使用操作系统互斥量带来的性能消耗。
|
||||||
|
|
||||||
|
#### Hotspot对象头内存布局
|
||||||
|
在Hotspot虚拟机对象头中包含两部分信息:
|
||||||
|
- 第一部分存储对象自身的运行时数据(Mark Word),如:
|
||||||
|
- hash码
|
||||||
|
- gc分代年龄等
|
||||||
|
- 存储指向方法区对象类型数据的指针
|
||||||
|
|
||||||
|
> 如果是数组类型,对象头中还会由额外部分用于存储数组长度
|
||||||
|
|
||||||
|
对象头信息是和对象自身定义数据无关的额外存储成本,为了考虑虚拟机空间效率,Mark Word被设计为了非固定的数据结构,以在尽量少的空间内存储更多信息。
|
||||||
|
|
||||||
|
Mark Word会根据对象状态复用自身的存储空间,例如在32位Hotspot虚拟机中:
|
||||||
|
- 如果对象处于未锁定状态下,Mark Word的32bit空间内容如下:
|
||||||
|
- 25bit用于存储对象的hash码
|
||||||
|
- 4bit用于存储对象的分代年龄
|
||||||
|
- 2bit用于存储锁标志位
|
||||||
|
- 1bit固定为0
|
||||||
|
|
||||||
|
在其他状态下,Mark Word的存储内容如下所示:
|
||||||
|
|
||||||
|
| 存储内容 | 标志位 | 状态 |
|
||||||
|
| :-: | :-: | :-: |
|
||||||
|
| 对象hash码和对象分代年龄 | 01 | 未锁定 |
|
||||||
|
| 指向锁记录的指针 | 00 | 轻量级锁定 |
|
||||||
|
| 指向重量级锁的指针 | 10 | 膨胀(重量级锁定) |
|
||||||
|
| 空,不需要记录信息 | 11 | GC标记 |
|
||||||
|
| 偏向线程ID、偏向时间戳、对象分代年龄 | 01 | 可偏向 |
|
||||||
|
|
||||||
|
#### 轻量级锁加锁过程
|
||||||
|
在代码进入同步块时,如果此时同步对象没有被锁定,
|
||||||
|
- 虚拟机将首先`在当前线程的栈帧中建立一个名为锁记录的空间`,用于存储对象目前的Mark Record拷贝。
|
||||||
|
- 然后,虚拟机将会尝试使用CAS将对象的Mark Word更新为指向该锁记录的指针,`若更新成功,则线程则持有了该对象的锁`。
|
||||||
|
- 之后,会将对象Mark Word的标志位改为00,代表对象处于轻量级锁定状态
|
||||||
|
- 如果CAS更新Mark Word失败,虚拟机会检查Mark Word是否指向当前线程的栈帧,如果是,说明当前线程已经拥有了该锁,直接执行同步代码块
|
||||||
|
- 如果Mark Word指向的不是当前线程的栈帧,那么代表该锁已经被其他线程抢先持有,此时,轻量级锁不再有效,需要膨胀为重量级锁,锁标记位会变为10,此时Mark Word中需要存储指向重量级锁(互斥量)的指针,后面等待的线程需要进入阻塞状态
|
||||||
|
|
||||||
|
#### 轻量级锁的解锁过程
|
||||||
|
轻量级锁的解锁过程也是通过CAS来操作的
|
||||||
|
- 如果Mark Word仍然指向线程的锁记录,那么会使用CAS将线程中复制的Mark Word替换回来,若替换成功,则整个解锁过程成功
|
||||||
|
- 如果替换失败,说明有其他线程尝试获取过锁,那么需要在释放锁的同时唤醒被挂起的线程
|
||||||
|
|
||||||
|
轻量级锁仅在`整个同步周期内都不存在竞争`这一场景下能够提高性能。如果没有竞争,轻量级锁使用CAS规避了互斥量带来的开销。
|
||||||
|
|
||||||
|
但是,如果存在锁竞争,那么除了互斥量外,还额外发生了CAS操作,故而在存在锁竞争的场景下轻量级锁比重量级锁更慢。
|
||||||
|
|
||||||
|
### 偏向锁
|
||||||
|
偏向锁也是jdk 1.6中引入的优化,其目的是进一步优化在无竞争情况下的程序性能。轻量级锁是在无竞争条件下用CAS操作去减少系统互斥量带来的开销;而偏向锁则是在无竞争条件下消除所有同步开销。
|
||||||
|
|
||||||
|
偏向锁代表其会偏向第一个获得其的线程,如果在后续执行的过程中,该所没有被其他线程获取,那么持有偏向锁的线程将永远不需要再同步。
|
||||||
|
|
||||||
|
如果虚拟机使用了偏向锁,那么在锁对象第一次被线程获取时,会将对象头中标志位设置为`01`,即偏向模式。同时,使用CAS操作将获取该锁的线程ID记录到对象的Mark Word中。若CAS成功,那么持有偏向锁的线程后续每次进入到该锁相关的同步代码中时,虚拟机都可以不再执行任何同步操作。
|
||||||
|
|
||||||
|
当有另一个线程尝试去获取该锁时,偏向模式将结束:
|
||||||
|
- 若当前对象处于未锁定状态,那么撤销偏向后,恢复到未锁定状态
|
||||||
|
- 若当前对象处于已锁定状态,那么撤销偏向后,恢复到轻量级锁定状态
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user