Monitor 被翻译为监视器或管程,是操作系统层次的数据结构
每个 Java 对象都可以关联一个 Monitor 对象
如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针
Monitor 结构如下
现模拟多线程竞争Synchronized锁对象的流程
刚开始 Monitor 中 Owner 为 null
当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor中只能有一个 Owner
在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行 synchronized(obj),就会进入EntryList BLOCKED(俗称阻塞队列)
Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争的时是非公平的
图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程
注意:
被synchronized的对象,对象头的MarkWord字段会指向Monitor地址,具体来看下对象头是什么结构
以 32 位虚拟机为例
普通对象:
数组对象:
其中 Mark Word 结构为:
Mark Word记录了对象和锁有关的信息,当这个对象被synchronized关键字当成同步锁时,围绕这个锁的一系列操作都和Mark Word有关
Mark Word在32位JVM中的长度是32bit,在64位JVM中长度是64bit
Mark Word在不同的锁状态下存储的内容不同,在32位JVM中是这么存的:
其中无锁和偏向锁的锁标志位都是01,只是在前面的1bit区分了这是无锁状态还是偏向锁状态
一个对象创建时:
-XX:BiasedLockingStartupDelay=0
来禁用延迟-XX:-UseBiasedLocking
禁用偏向锁JDK1.6以后的版本在处理同步锁时存在锁升级的概念,JVM对于同步锁的处理是从偏向锁开始的,随着竞争越来越激烈,处理方式从偏向锁升级到轻量级锁,最终升级到重量级锁
64 位虚拟机 Mark Word:
Klass Word也占32个字节,该区域用来表示指向该对象对应类的指针
Array Length为数组对象独占区域,用来指明数组长度的
轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化
轻量级锁对使用者是透明的,即语法仍然是 synchronized
假设有两个方法同步块,利用同一个对象加锁
class TestSyn{
static final Object obj = new Object() ;
public static void method1(){
synchronized (obj){
//同步块A
method2();
}
}
public static void method2(){
synchronized (obj){
//同步块B
}
}
}
创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word
让锁记录中 Object reference 指向锁对象,并尝试用 cas (交换并设置)替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录
如果 cas 替换成功,对象头中存储了锁记录地址和状态 00 (代表轻量级锁),表示由该线程给对象加锁,这时图示如下
如果 cas 失败,有两种情况
如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀(也称锁升级)过程
如果是自己执行了 synchronized 锁重入(与本线程竞争),那么再添加一条 Lock Record 作为重入的计数
当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一
当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头
成功,则解锁成功
失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁
如有以下流程:
当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁
这时 Thread-1 加轻量级锁失败,进入锁膨胀流程,为 Object 对象申请 Monitor 锁(重量级锁),让 Object 指向重量级锁地址,然后自己进入 Monitor 的 EntryList BLOCKED阻塞队列
当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞
自旋重试成功的情况:
自旋重试失败的情况:
注意:
轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作
Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有
class TestSyn{
static final Object obj = new Object() ;
public static void method1(){
synchronized (obj){
//同步块A
method2();
}
}
public static void method2(){
synchronized (obj){
//同步块B
method3();
}
}
public static void method3(){
synchronized (obj){
//同步块C
}
}
}
轻量级锁下,主线程每次申请锁都需要CAS
偏向锁下。无需CAS
调用了对象的 hashCode,但偏向锁的对象 MarkWord 中存储的是线程 id,如果调用 hashCode 会导致偏向锁被撤销
轻量级锁会在锁记录中记录 hashCode
重量级锁会在 Monitor 中记录 hashCode
故轻量级重量级锁调用hashCode是没有问题的
在调用 hashCode 后使用偏向锁,记得去掉 -XX:-UseBiasedLocking
当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁,过程上面已经写过
锁对象一旦调用wait/notify会导致该对象的偏向锁状态被撤销
参考:
黑马程序员《全面深入学习高并发多线程》
说到了wait/notify,那么继续扯扯wait/notify,wait/notify是用来实现线程通信协作的
接synchronized,当一个线程暂时不满足条件,应该进去等待队列,等着另一个线程执行完毕,线程满足条件,由另一个线程唤醒之
注意,wait/notify需要在synchroniezd代码块中才能使用
API 介绍
这套组合与wait/notify是一样的效果,但这套组合要由于wait/notify,它们是 LockSupport 类中的方法
// 暂停当前线程
LockSupport.park();
// 恢复某个线程的运行
LockSupport.unpark(暂停线程对象)
先 park 再 unpark
与 Object 的 wait & notify 相比
Park/UnPark原理
每个线程都有自己的一个 Parker 对象,由三部分组成 _counter , _cond 和 _mutex
打个比喻
线程就像一个旅人,Parker 就像他随身携带的背包,条件变量cond就好比背包中的帐篷。_counter 就好比背包中的备用干粮(0 为耗尽,1 为充足)
调用 park 就是要看需不需要停下来歇息
如果备用干粮耗尽,那么钻进帐篷歇息
如果备用干粮充足,那么不需停留,继续前进
调用 unpark,就好比令干粮充足
如果这时线程还在帐篷,就唤醒让他继续前进
如果这时线程还在运行,那么下次他调用 park 时,仅是消耗掉备用干粮,不需停留继续前进
因为背包空间有限,多次调用 unpark 仅会补充一份备用干粮
- 当前线程调用 Unsafe.park() 方法
- 检查 _counter(counter) ,本情况为 0,这时,获得 _mutex 互斥锁,类比monitor的owner
- 线程进入 _cond 条件变量阻塞,类比monitor的waitset
- 设置 _counter = 0
- 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
- 唤醒 _cond 条件变量中的 Thread_0
- Thread_0 恢复运行
- 设置 _counter 为 0
- 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
- 当前线程调用 Unsafe.park() 方法
- 检查 _counter ,本情况为 1,这时线程无需阻塞,继续运行
- 设置 _counter 为 0