共计 6284 个字符,预计需要花费 16 分钟才能阅读完成。
前言
本篇文章次要学习 synchronized
关键字在 JDK1.6
引入的 偏差锁 和轻量级锁 ,并围绕synchronized
关键字的锁的降级进行展开讨论。本篇文章探讨的锁是通过 synchronized
加的锁,是不同于 java.util.concurrent.locks.Lock
的另外一种加锁机制,后续文中提及 锁,均指 synchronized
关键字的锁。
参考资料:《Java 并发编程的艺术》
注释
一. 锁的应用
synchronized
能够用于润饰 一般办法 , 静态方法 和代码块 ,拜访被synchronized
关键字润饰的内容须要先获取锁,获取的这个锁具体是什么,这里暂不探讨,上面先举例子来看一下 synchronized
关键字如何应用。
public class SynchronizedLearn {
// 润饰一般办法
public synchronized void normalSyncMethod() {......}
// 润饰静态方法
public static synchronized void staticSyncMethod() {......}
// 润饰代码块
public void syncCodeBlock() {synchronized (SynchronizedLearn.class) {......}
}
}
上述例子中,应用 synchronized
关键字润饰代码块时,传入了 SynchronizedLearn
类的类对象,实际上,synchronized
关键字无论是润饰办法还是润饰代码块,均须要传入一个对象,咱们这里能够将传入的这个对象了解为 锁
,只不过在润饰办法时,会隐式地传入对象作为锁,规定如下。
- 润饰一般办法时,隐式传入的对象为持有一般办法的实例对象自身;
- 润饰静态方法时,隐式传入的对象为持有静态方法的类的类对象。
咱们称由 synchronized
关键字润饰的办法为同步办法,联合上述规定,对由 synchronized
关键字润饰的同步办法的拜访有如下留神点。
1. 实例对象的所有一般同步办法同一时刻只能由一个线程拜访
给出一个例子如下所示。
public class SynchronizedLearn {public synchronized void normalSyncMethod1() {......}
public synchronized void normalSyncMethod2() {......}
}
某时刻线程 A 和线程 B 都持有 SynchronizedLearn
的同一个实例 synchronizedLearn,并且线程A 胜利调用实例 synchronizedLearn 的normalSyncMethod1()
办法,此时在线程 A 执行完 normalSyncMethod1()
办法以前,线程 B 都无法访问 normalSyncMethod1()
和normalSyncMethod2()
办法。
2. 类的所有动态同步办法同一时刻只能由一个线程拜访
给出一个例子如下所示。
public class SynchronizedLearn {public static synchronized void staticSyncMethod1() {......}
public static synchronized void staticSyncMethod2() {......}
}
某时刻线程 A 胜利调用 SynchronizedLearn
类的 staticSyncMethod1()
办法,此时在线程 A 执行完 staticSyncMethod1()
办法以前,线程 B 都无法访问 staticSyncMethod1()
和staticSyncMethod2()
办法。
3. 类的动态同步办法和类实例的一般同步办法同一时刻能够由不同线程拜访
给出一个例子如下所示。
public class SynchronizedLearn {public synchronized void normalSyncMethod() {......}
public static synchronized void staticSyncMethod() {......}
}
某时刻线程 A 持有 SynchronizedLearn
的实例 synchronizedLearn 并调用了其 normalSyncMethod()
办法,无论线程 A 是否执行完 normalSyncMethod()
办法,线程 B 都能够拜访 SynchronizedLearn
类的 staticSyncMethod()
办法。
二. 锁是什么
上一大节中提到:synchronized
关键字无论是润饰办法还是润饰代码块,均须要传入一个对象,这个对象能够了解为锁 。在JVM
中通过 monitorenter 和monitorexit指令来保障同步代码块的线程平安(同步办法是另外一种形式,由具体虚拟机决定其实现,这里不做探讨),monitorenter指令是在编译后插入到同步代码块的起始地位,monitorexit指令是在编译后插入到同步代码块的完结地位,当线程执行到 monitorenter 指令时,会尝试获取通过 synchronized
关键字传入的对象相关联的 Monitor
对象的所有权,即获取锁,当线程执行到 monitorexit 指令时,会放弃 Monitor
对象的所有权,即开释锁。
下面提到的 Monitor
是一种同步机制,个别由一个对象来实现,称之为 Monitor
对象,在 Java
中每一个 Java
对象都与一个 Monitor
对象相关联,具体的关联关系能够了解为 共生共灭 。在HotSpot
虚拟机中,Monitor
对象是 ObjectMonitor
对象,获取 synchronized
关键字传入的对象实际上就是获取和该对象关联的 ObjectMonitor
,其由c++
实现,如下所示。
ObjectMonitor() {
_header = NULL;
_count = 0;
_waiters = 0,
_recursions = 0; // 重入次数
_object = NULL;
_owner = NULL; // 示意以后持有 Monitor 的线程
_WaitSet = NULL; // 期待队列
_WaitSetLock = 0;
_Responsible = NULL;
_succ = NULL;
_cxq = NULL;
FreeNext = NULL;
_EntryList = NULL; // 同步队列
_SpinFreq = 0;
_SpinClock = 0;
OwnerIsThread = 0;
}
ObjectMonitor
对象中保护了两个队列,别离是 _EntryList
和_WaitSet
,其中 _EntryList
是同步队列,每一个想要获取 ObjectMonitor
对象所有权的线程会进入该队列,如果获取 ObjectMonitor
对象所有权失败,则阻塞在该队列中,直到胜利获取到 ObjectMonitor
对象所有权才会从 _EntryList
中移除,而 _WaitSet
是期待队列,当持有 ObjectMonitor
对象所有权的线程调用和 ObjectMonitor
对象关联的 Java
对象的 wait()
办法时,就会开释 ObjectMonitor
对象的所有权并进入 _WaitSet
队列中期待。那么到这里能够发现,Monitor
对象和队列同步器 AbstractQueuedSynchronizer
很像,所以不太谨严的将 synchronized
的锁机制与 java.util.concurrent.locks.Lock
的锁机制进行一个比对:Java
中的所有对象都是一把锁,能够和 ReentrantLock
这样的锁绝对应,而每个 Java
对象与一个 Monitor
对象相关联,能够和 ReentrantLock
这样的锁的自定义同步器绝对应。
三. 锁的降级
在 JDK1.6
引入了 偏差锁 和轻量级锁 ,用于解决synchronized
关键字太过 分量 的问题。已知 synchronized
关键字润饰办法或者代码块时会传入对象作为锁,实际上 synchronized
关键字的锁存在于传入对象的对象头中,对于非数组类型的对象的对象头占用 2 个字宽,32/64 位虚拟机 中每字宽占用大小为32/64 bits,如下表所示。
长度 | 内容项 | 阐明 |
---|---|---|
32/64 bits | Mark Word | 存储对象 hashCode 或锁信息 |
32/64 bits | Class Metadata Address | 存储指向对象类型数据的指针 |
上表中的 Mark Word 在无锁状态下存储的数据为对象 hashCode,在 偏差锁 , 轻量级锁 和重量级锁 状态下存储的数据为锁信息,如下所示。
在不通过设置 JVM 参数被动敞开偏差锁的状况下偏差锁默认开启,并随着竞争的逐步强烈,锁会逐渐降级为 轻量级锁 和重量级锁 ,在第二大节中提及的ObjectMonitor
对象,理论就是锁收缩为 重量级锁 之后会应用到的数据。上面将对偏差锁,轻量级锁和重量级锁及其锁降级过程进行剖析。
1. 偏差锁
通常,锁被认定为不会存在多线程竞争,并且锁会被同一线程反复获取,这种状况下,锁会偏差某一个线程,即偏差锁。上面别离对 偏差锁的获取 , 偏差锁的重偏差 和偏差锁的撤销 进行剖析。
1)偏差锁的获取和重偏差
一开始,锁对象 Mark Word 中的 是否可偏差 标记位为 1,锁标记位 为 01,且线程 ID 为空,这示意锁 可偏差且未偏差 。如果线程判断锁是可偏差且未偏差状态,那么线程会基于CAS 将锁对象 Mark Word 的线程 ID 字段设置为以后线程 ID,如果设置胜利,示意胜利获取到偏差锁,如果设置失败,示意呈现了竞争,须要执行 偏差锁的撤销 逻辑。
如果锁对象 Mark Word 中的 是否可偏差 标记位为 1,锁标记位 为 01,且线程 ID 不为空,此时获取锁的线程须要将锁对象 Mark Word 的Epoch
字段值与锁对象所属类的类信息中的 Epoch
字段值进行比拟,如果相等,则锁是 可偏差且已偏差 状态,如果不相等,则锁是 可偏差且重偏差 状态,依据不同的状态获取偏差锁的逻辑如下所示。
- 如果是
可偏差且已偏差
状态,则判断锁对象 Mark Word 的线程 ID 字段是否和以后线程的线程 ID 相等,若相等,示意以后线程是锁偏差的线程,以后线程能够胜利获取偏差锁,若不相等,示意产生了锁竞争,此时须要执行 偏差锁的撤销 逻辑; - 如果是
可偏差且重偏差
状态,示意偏差锁能够被线程获取(等同于 可偏差且未偏差 状态),则以后线程能够以 CAS 形式来竞争获取偏差锁。
因为偏差锁 不会有被动开释锁 这个操作,所以某线程获取到偏差锁并执行完同步代码后,只管这个线程不再须要偏差锁,然而锁对象 Mark Word 的线程 ID 字段仍旧为这个线程的 ID,所以偏差锁会应用锁对象Mark Word 中的 Epoch
字段实现 重偏差 逻辑,即便得呈现上述情况时,另外的线程仍旧能够获取到偏差锁。Eopch
字段的实现逻辑如下所示。
- 偏差锁的锁对象 Mark Word 中会存储一份
Epoch
值,同时在偏差锁对象所属类的类信息中也会存储一份Epoch
值; - 每达到 全局平安点(该工夫点没有正在执行的字节码),类信息中的
Epoch
值会加 1,失去Epoch_new
,而后遍历类的所有实例对象,并判断这些对象是否还作为偏差锁被某个线程持有,如果是,则将Epoch_new
赋值给对象 Mark Word 的Epoch
字段; - 当呈现锁对象 Mark Word 中的线程 ID 字段值不会空时,就将 Mark Word 的
Epoch
字段值与类信息中的Epoch
字段值进行比拟,如果相等,表明该锁还被某线程持有,如果不相等,则表明锁未被持有,该锁能够从新偏差某个线程。
3)偏差锁撤销
偏差锁的撤销,须要等到 全局平安点 ,而后依据锁对象Mark Word 中的线程 ID 找到被偏差的线程并暂停,再而后判断被偏差线程是否执行完同步代码,最初依据判断后果执行不同的偏差锁撤销逻辑,如下所示。
- 如果被偏差线程曾经执行完同步代码,则锁会从偏差锁状态被置为无锁状态,此时锁对象 Mark Word 中的是否可偏差字段为 0,示意不可偏差,此时的锁对象是 不可偏差 状态,后续如果须要获取该锁,间接以 轻量级锁 的形式获取;
- 如果线程还未执行完同步代码,则偏差锁会间接降级为轻量级锁。
最初会唤醒在全局平安点被暂停的线程。
2. 轻量级锁
在偏差锁状态下,如果产生锁竞争,则偏差锁会降级为轻量级锁,或者通过设置 JVM 参数禁用偏差锁,获取锁时会间接获取轻量级锁。
1)轻量级锁的获取
线程执行同步代码前,若锁对象为无锁状态,则会在以后线程栈帧中创立用于存储 锁记录(Lock Record)的内存空间,而后将锁对象的 Mark Word 复制到以后线程栈帧的锁记录中,称复制到锁记录中的 Mark Word 为Displaced Mark Word,同时锁记录中的 owner 指针会指向锁对象,再而后基于 CAS 形式将锁对象的 Mark Word 替换为指向以后线程栈帧锁记录的指针,如果替换胜利,示意胜利获取到轻量级锁,如果替换失败,则示意产生了竞争,以后线程进入自旋状态尝试获取锁。
锁记录的构造如下所示。
2)轻量级锁的重入
当线程要获取锁时,如果锁对象曾经是轻量级锁,此时判断锁对象头的指向线程栈帧锁记录的指针是否指向以后线程栈帧锁记录,如果不是,则示意其它线程持有该轻量级锁,则以后线程进入自旋状态尝试获取锁,如果是,则示意以后线程持有该轻量级锁,此时是 锁重入。
锁重入时,持有锁的线程会在线程栈帧又创立存储锁记录的内存空间,同时 Displaced Mark Word 为空,owner指针指向锁对象。每重入一次,就会创立一次 Displaced Mark Word 为空的锁记录内存空间,如下所示。
3)轻量级锁的降级
线程在获取轻量级锁时,有两种状况会获取失败并进入自旋获取锁的状态,如下所示。
- 线程将锁对象的 Mark Word 复制到线程栈帧锁记录后,如果基于 CAS 形式将锁对象的 Mark Word 替换为指向以后线程栈帧锁记录的指针失败,此时会进入自旋获取锁的状态;
- 线程获取锁时,如果锁对象是轻量级锁状态,但曾经被其它线程持有,此时会进入自旋获取锁的状态。
因为线程自旋获取锁会耗费 CPU 资源,所以不会让自旋获取锁的线程始终自旋,当自旋达到肯定次数时,会判断自旋获取锁失败,此时会让轻量级锁降级为重量级锁,重量级锁的锁对象 Mark Word 会变成指向锁对象关联的 ObjectMonitor 对象的指针,锁标记位会置为 10,同时阻塞自旋获取锁失败的线程。
4)轻量级锁的开释
持有轻量级锁的线程退出同步代码时会开释轻量级锁,会基于 CAS 形式将线程栈帧锁记录的 Displaced Mark Word 替换回到锁对象的 Mark Word 中,如果替换胜利,表明胜利开释轻量级锁,如果替换失败,表明产生了锁竞争且锁曾经降级为重量级锁,此时线程开释锁并唤醒在重量级锁上期待的线程。
3. 重量级锁
当锁从轻量级锁状态降级为重量级锁状态后,此时锁是一种 乐观锁,所有竞争获取锁失败的线程会被阻塞,直到持有锁的线程开释锁则会又退出对锁的竞争中。
重量级锁的锁对象的 Mark Word 为指向锁对象关联的 ObjectMonitor 对象的指针,重量级锁的同步语义也是依赖 ObjectMonitor 对象来实现,该局部内容在第二节中曾经进行了剖析,这里不再反复探讨。
总结
synchronized
关键字的锁有四种状态:无锁状态 , 偏差锁状态 , 轻量级锁状态 和重量级锁状态 ,这四种状态会随着竞争的逐步强烈而顺次降级。应用 偏差锁 是为了升高 锁总是会被同一线程反复获取场景
下获取锁的开销,应用 轻量级锁 是为了让竞争获取锁的线程不被阻塞从而防止 线程用户态和内核态之间切换
的耗费,而当锁降级到 重量级锁 后,此时 乐观
地认为锁资源竞争很强烈,每个竞争锁资源失败的线程间接被阻塞从而 防止自旋获取锁时对 CPU 资源的耗费
。