通过上一篇我们知道通过内存屏障,在反编译的汇编语言中我们看到基于lock cmpxchg和lock addl 前置指令解决了synchronized和volatile的可见性和有序性问题。也在反汇编中看到了synchronized中的monitorenter和monitorexit,这是synchronized实现原子性的关键,并且monitorexit出现了两次,就是为了包装程序在正常退出和异常退出的情况下都执行执行唤醒操作。monitor可以叫监视器,也可以翻译为管程,并不是java特有的产物。并发过程中除了有线程原子性的问题,我们还需要解决线程间的互斥还得数据同步。
在解决并发问题的历史过程中,最早是使用信号量处理,在juc包中Semaphore还有其实现支持,将在编发编程工具中分析。往后才是管程模型的提出,但是管程和信号量是等价的,即管程能实现的功能(或效果)信号量也能实现。在java中synchronized就是管程模型的实现,只是有c语言实现不方便查看,但是在JDK5之后juc(java.util.concurrent)包下面的API就是基于volatile(解决了可见性和有序性)+ CAS 模拟管程实现。实现的核心由AQS(AbstractQueuedSynchronizer)实现,CAS使用sun.misc.UnSafe的CAS相关API实现。所以当前先使用ReentrantLock和Condition模拟管程实现【先大概看看管程是什么东西】,实现线程安全的原子性,即操作的过程对外不可见,外面不能看见执行中的中间状态,那么管程的思想就是执行的过程封装起来,满足条件后再唤醒其他(线程)操作,也就解决了原子性。
/**
* 使用{@link ReentrantLock} 和 {@link Condition} 模拟管程模型;在synchronized中只能有一个条件队列,
* 而当前可以创建多个Condition即可
*
* <p>
* 对于入队操作,如果队列已满,就需要等待直到队列不满,所以这里用了notFull.await();。
* 对于出队操作,如果队列为空,就需要等待直到队列不空,所以就用了notEmpty.await();。
* 如果入队成功,那么队列就不空了,就需要通知条件变量:队列不空notEmpty对应的等待队列。
* 如果出队成功,那就队列就不满了,就需要通知条件变量:队列不满notFull对应的等待队列。
*
* <p>
* 我们知道 {@link Object#wait()}、{@link Object#notify()}、{@link Object#notifyAll()} 只能在 synchronized中使用
* 同样 {@link Condition#await()}、{@link Condition#signal()}、{@link Condition#signalAll()} 只能在Lock&Condition中使用
* 并且这三个方法一一对应, 需要注意上面三个是{@link Object}的方法,所以在下面的{@link Condition}中同样存在,千万别调用错了
*
* @author kevin
* @date 2020/7/29 23:51
* @since 1.0.0
*
* @see LinkedBlockingQueue#takeLock
* @see LinkedBlockingQueue#putLock
*
* @see LinkedBlockingQueue#notFull
* @see LinkedBlockingQueue#notEmpty
*/
public class BlockedQueue {
/** 创建一个公平锁 */
final Lock lock = new ReentrantLock(true);
/** 条件变量:队列不满 */
final Condition notFull = lock.newCondition();
/** 条件变量:队列不空 */
final Condition notEmpty = lock.newCondition();
/**
* 入队操作
*/
void enqueue() throws InterruptedException {
lock.lock();
try {
while (队列已满) {
// 等待队列不满
notFull.await();
}
// 省略入队操作
// 入队后通知可出队
notEmpty.signal();
} finally {
lock.unlock();
}
}
void dequeue() throws InterruptedException {
lock.lock();
try {
while (队列已空) {
// 等待队列不空
notEmpty.await();
}
// 省略出队列操作,省略一万行
// 出队列后,通知可入队
notFull.signal();
} finally {
lock.unlock();
}
}
}
管程模型分为三种(Java选择了MESA管程模型):
1)、Hasen模型
Hasen模型要求notify放到最后,这样T2线程通知T1后,T2线程就结束了,然后T1执行完,这样就能保证同一时刻只有一个线程在执行。
2)、Hoare模型
Hoare模型里面,T2线程通知完T1线程后,T2马上阻塞,T1马上执行;等T1执行完之后再唤醒T2线程,也能保证同一时刻只有一个线程在执行,但是T2多了一次阻塞唤醒操作。
3)、MESA管程模型(Java使用MESA模型实现)
MESA模型中,T2唤醒T1之后,T2还是会接着执行,T1并不立即执行,仅仅是从条件变量队列到等待队列中。
好处:notify(或notifyAll)、signal(或signalAll)不用放到代码的最后,T2也没有多余的阻塞唤醒操作
坏处:T1执行的时候,可能曾经满足过条件,现在已经不能满足了,需要增加循环验证条件方式(个人理解也算是乐观锁的思想)。
MESA管程模型:
看到上图的Java MESA管程模型,就理解synchronized与看似从天而降的的Object的 wait、notify、notifyAll方法,synchronized可以修饰
1、方法块前提是括号中需要有对象即Object
2、修饰普通方法,修饰的是 Object对象
3、修饰静态方法,修饰的是 .class,可以理解为也是Object对象
所以synchronized关键字,处理的是Object对象(其子类)的对象头的状态,后续再详细分析。上中的队列模型即反汇编之后看到的monitorenter、monitorexit,底层封装了条件满足后,对队列中的 wait(等待)、notify(notifyAll 唤醒)操作。结合c底层的对应的队列,和管程对象,可以理解为下图:
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 ;
}
JVM内部使用了ACC_SYNCHRONIZED访问标志位来区分一个方法或代码是否为同步,如果程序执行时发现该标志则该线会先获取synchronized锁的对象的Monitor(管程)并设置标志再执行同步程序。JVM中的同步是基于进入和退出管程(Monitor)对象实现的,每个对象实例都会有一个Monitor,并且可以和对象一个创建和销毁。java中的Monitor是由C++ 的ObjectMonitor.hpp 实现,当多个线程访问该ACC_SYNCHRONIZED时,多个线程会先放入ContentionList和_EntryList中,处于block状态的线程都会被加入到该列表。
ObjectMonitor是靠底层的Mutex Lock来实现互斥的,线程申请 Mutex 成功,则持有该 Mutex,其它线程将无法获取到该 Mutex,竞争失败的线程会再次进入 ContentionList 被挂起;线程调用wait就会释放当前持有的Mutex Lock并且当前线程进入WaitSet集合。ObjectMonitor依赖底层的操作系统实现,所以存在用户态和内核态的切换,所以性能开销比较大。
获取管程(Object Monitor)后会设置自己的标志位,已经性能开销的优化可以参见:并发编程基础 - synchronized锁优化
注意:
除非经过深思熟虑(或者有非常的把握)尽量使用Object的 notifyAll(不要使用notify)、Condition的 signalAll(不要使用signal),除非满足以下三个条件【否则可能照成某些线程的饥饿等】:
1)、所以等待线程拥有相同的等待条件
2)、所以等待线程被唤醒后,执行相同的操作
3)、只需要唤醒一个线程