前言
线程并发系列文章:
Java 线程基础
Java 线程状态
Java “优雅”地中断线程-实践篇
Java “优雅”地中断线程-原理篇
真正理解Java Volatile的妙用
Java ThreadLocal你之前了解的可能有误
Java Unsafe/CAS/LockSupport 应用与原理
Java 并发"锁"的本质(一步步实现锁)
Java Synchronized实现互斥之应用与源码初探
Java 对象头分析与使用(Synchronized相关)
Java Synchronized 偏向锁/轻量级锁/重量级锁的演变过程
Java Synchronized 重量级锁原理深入剖析上(互斥篇)
Java Synchronized 重量级锁原理深入剖析下(同步篇)
Java并发之 AQS 深入解析(上)
Java并发之 AQS 深入解析(下)
Java Thread.sleep/Thread.join/Thread.yield/Object.wait/Condition.await 详解
Java 并发之 ReentrantLock 深入分析(与Synchronized区别)
Java 并发之 ReentrantReadWriteLock 深入分析
Java Semaphore/CountDownLatch/CyclicBarrier 深入解析(原理篇)
Java Semaphore/CountDownLatch/CyclicBarrier 深入解析(应用篇)
最详细的图文解析Java各种锁(终极篇)
线程池必懂系列
上篇分析了重量级锁在线程互斥场景下加锁、释放锁的过程,本篇将分析重量级锁在线程同步下的等待、通知机制。
通过本篇文章,你将了解到:
1、为什么 wait/notify/notifyAll 需要上锁
2、wait/notify/notifyAll 源码入口
3、Object.wait(xx) 流程解析
4、Object.notify()/Object.notifyAll() 流程解析
5、wait/notify/notifyAll 流程图
6、线程互斥同步下 锁的流程图
7、wait/notify/notifyAll 疑难点解析
1、为什么 wait/notify/notifyAll 需要上锁
一个Demo
线程同步需要在某种条件下调用wait/notify进行同步,先来看简单的例子:
public class TestThread {
static Object object = new Object();
static int count = 1;
public static void main(String args[]) {
//线程 A 消费者
new Thread(new Runnable() {
@Override
public void run() {
try {
count--;
if (count == 0) {
//count == 0 才会等待
object.wait();
}
} catch (Exception e) {
}
}
}).start();
//线程 B 生产者
new Thread(new Runnable() {
@Override
public void run() {
try {
//生产好了就通知线程A
count++;
object.notify();
} catch (Exception e) {
}
}
}).start();
}
}
上述功能很简单:线程B生产东西(增加count值),线程A消费东西(减少count值),线程A发现没东西可用了就调用wait挂起等待相应的条件满足后再次运行。
此处的条件即是:count的值。
正常情况下是:线程A等待count值,线程B通知线程A count值已经准备好了,这就是线程之间的同步。
wait 之前为什么需要获取锁
现在从多线程并发的角度来看这Demo,可能的运行顺序如下:
1、count初始值为1。
2、线程A先执行到count==0,准备调用object.wait()。
3、此时线程B已经修改好了count值,并且调用了Object.notify()。
4、线程A此时调用Object.wait()后,因为错过了Object.notify(),所以就永远阻塞于此处。
导致上面问题的原因是:count是线程间共享的,对它的修改存在并发问题,因此需要加锁来实现互斥访问count。
notify 之前为什么需要获取锁
你也许会说:既然锁是为了保护count,那么只保护对应的共享变量即可,notify可以不上锁啊。如下代码:
//线程A
synchronized (object) {
count--;
if (count == 0) {
//count == 0 才会等待
object.wait();
}
}
//线程B
synchronized (object) {
//生产好了就通知线程A
count++;
}
object.notify();
notify 的目的是将等待队列里的线程插入到同步队列里,假设是notify没有在同步代码块里,那么线程B修改count值后释放锁,因为还没有notify,因此A没有移动到同步队列里,最终无法唤醒线程A,A就会一直阻塞等待。
notifyAll也是一样的道理。
notify具体原理接下来会详细分析。
JVM如何避免不正常地调用
wait/notify/notifyAll 需要在同步块里调用,而用户不一定这么操作,因此JVM会在调用wait/notify/notifyAll 时检测当前线程是否已经获取了锁,没有锁则会抛出异常。
#ObjectMonitor.cpp
void ObjectMonitor::notify(TRAPS) {
CHECK_OWNER();
...
}
void ObjectMonitor::wait(jlong millis, bool interruptible, TRAPS) {
...
CHECK_OWNER();
...
}
void ObjectMonitor::notifyAll(TRAPS) {
CHECK_OWNER();
...
}
#define CHECK_OWNER() \
do { \
if (THREAD != _owner) {
//当前线程不是重量级锁的获得者 \
if (THREAD->is_lock_owned((address) _owner)) { \
//当前线程是之前轻量级锁的获得者
_owner = THREAD ; /* Convert from basiclock addr to Thread addr */ \
_recursions = 0; \
OwnerIsThread = 1 ; \
} else {
//没有获取抛出异常 \
TEVENT (Throw IMSX) ; \
THROW(vmSymbols::java_lang_IllegalMonitorStateException()); \
} \
} \
} while (false)
可以看出,调用wait/notify/notifyAll的时候会调用宏CHECK_OWNER()去检测当前线程是否获取了锁,没有则抛出IllegalMonitorStateException 异常。
小结:
wait/notify/notifyAll 需要包在synchronized 同步块里的原因是保护同步的条件在并发场景下能够被正确访问。
2、wait/notify/notifyAll 源码入口
wait/notify/notifyAll 方法是声明在Java顶级类Object.java里的,通过寻找发现是native层实现的。
#Object.c
static JNINativeMethod methods[] = {
{"hashCode", "()I", (void *)&JVM_IHashCode},
{"wait", "(J)V", (void *)&JVM_MonitorWait},
{"notify", "()V", (void *)&JVM_MonitorNotify},
{"notifyAll", "()V", (void *)&JVM_MonitorNotifyAll},
{"clone", "()Ljava/lang/Object;", (void *)&JVM_Clone},
};
可以看到是动态注册的JNI函数。
以wait为例,继续寻找:
#jvm.cpp
JVM_ENTRY(void, JVM_MonitorWait(JNIEnv* env, jobject handle, jlong ms))
...
ObjectSynchronizer::wait(obj, ms, CHECK);
JVM_END
又到了熟悉的ObjectSynchronizer类里了。
3、Object.wait(xx) 流程解析
找到了底层源码的入口,接下来就比较简单了。
#synchronizer.cpp
void ObjectSynchronizer::wait(Handle obj, jlong millis, TRAPS) {
if (UseBiasedLocking) {
//如果是偏向锁,则撤销
BiasedLocking::revoke_and_rebias(obj, false, THREAD);
assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now");
}
...
//膨胀为重量级锁
ObjectMonitor* monitor = ObjectSynchronizer::inflate(THREAD, obj());
DTRACE_MONITOR_WAIT_PROBE(monitor, obj(), THREAD, millis);
//调用ObjectMonitor的wait函数
monitor->wait(millis, true, THREAD);
...
}
此处可以看出,调用了wait函数后synchronized 膨胀为重量级锁了。
此时调用已经流转到ObjectMonitor里了。
#ObjectMonitor.cpp
void ObjectMonitor::wait(jlong millis, bool interruptible, TRAPS) {
...
if (interruptible && Thread::is_interrupted(Self, true) && !HAS_PENDING_EXCEPTION) {
...
//调用wait 的时候线程可以被中断
THROW(vmSymbols::java_lang_InterruptedException());
return ;
}
//构造节点,封装了当前线程
ObjectWaiter node(Self);
//节点状态为TS_WAIT
node.TState = ObjectWaiter::TS_WAIT ;
Self->_ParkEvent->reset() ;
OrderAccess::fence(); // ST into Event; membar ; LD interrupted-flag
//操作等待队列需要获取锁
Thread::SpinAcquire (&_WaitSetLock, "WaitSet - add") ;
//将当前节点加入等待队列里------->(1)
AddWaiter (&node) ;
//释放等待队列的锁
Thread::SpinRelease (&_WaitSetLock) ;
...
//释放锁+唤醒线程------->(2)
exit (true, Self) ; // exit the monitor
...
{ // State transition wrappers
OSThread* osthread = Self->osthread();
OSThreadWaitState osts(osthread, true);
{
...
if (interruptible && (Thread::is_interrupted(THREAD, false) || HAS_PENDING_EXCEPTION)) {
// Intentionally empty
} else
if (node._notified == 0) {
//挂起自己------->(3)
if (millis <= 0) {
Self->_ParkEvent->park () ;
} else {
ret = Self->_ParkEvent->park (millis) ;
}
}
...
} // Exit thread safepoint: transition _thread_blocked -> _thread_in_vm
//这之后是线程被唤醒后的操作
...
ObjectWaiter::TStates v = node.TState ;
if (v == ObjectWaiter::TS_RUN) {
//正常获取锁的流程
enter (Self) ;
} else {
//此时v的状态是在同步队列里--------------->(4)
ReenterI (Self, &node) ;
node.wait_reenter_end(this);
}
...
} // OSThreadWaitState()
}
列出了4个重点:
(1)
在上篇线程互斥加锁、释放锁的流程中可知:引入了同步队列(_cxq、_EntryList)。
竞争锁失败时最终会加入到同步队列里,当线程释放锁后会从同步队列里取出节点唤醒。
而在线程同步过程中,当调用wait函数后,节点会被加入到等待队列_WaitSet里。
来看看源码是如何实现的:
#ObjectMonitor.cpp
inline void ObjectMonitor::AddWaiter(ObjectWaiter* node) {
if (_WaitSet == NULL) {
//等待队列里没有节点,则当前节点被当作头节点
_WaitSet = node;
node->_prev = node;
node->_next = node;
} else {
//节点插入到队列尾部
ObjectWaiter* head = _WaitSet ;
ObjectWaiter* tail = head->_prev;
assert(tail->_next == head, "invariant check");
tail->_next = node;
head->_prev = node;
node->_next = head;
node->_prev = tail;
}
}
_pre指的是前驱节点,_next指的是后驱节点。
_WaitSet 是双向循环链表。
如上图所示,线程A先调用wait(xx)方法,接着是线程B,最后是线程C。线程A在队列头,线程C在队列尾。
(2)
exit(xx)在上篇文章已经分析过了,其主要功能:
释放锁,然后唤醒同步队列里的节点。
(3)
还是调用ParkEvent挂起自己。
(4)
当线程被唤醒后(此时线程是在同步队列里),调用ReenterI(xx)竞争锁。
void ATTR ObjectMonitor::ReenterI (Thread * Self, ObjectWaiter * SelfNode) {
...
for (;;) {
//尝试一次加锁
if (TryLock (Self) > 0) break ;
//自旋加锁
if (TrySpin (Self) > 0) break ;
{
//加锁失败,挂起
jt->set_suspend_equivalent();
if (SyncFlags & 1) {
Self->_ParkEvent->park ((jlong)1000) ;
} else {
Self->_ParkEvent->park () ;
}
}
//被唤醒后继续加锁
if (TryLock(Self) > 0) break ;
...
}
//走到这,表示获取锁成功
//从同步队列里移出节点
UnlinkAfterAcquire (Self, SelfNode) ;
}
值得注意的是:因为已经在同步队列里,所以即使抢占锁失败后也不会加入到同步队列里了。
总结来说,调用Object.wait(xx) 方法底层主要做了四件事:
1、封装节点并加入到等待队列里。
2、释放锁并唤醒同步队列里的线程。
3、挂起自己。
4、被唤醒后继续竞争锁。
4、Object.notify()/Object.notifyAll() 流程解析
既然调用了wait(xx)后线程被挂起了,那么它什么时候被移出等待队列并且被唤醒呢?接着来看看Object.notify()。
notify 解析
与Object.wait(xx)方法入口类似:
#synchronizer.cpp
void ObjectSynchronizer::notify(Handle obj, TRAPS) {
if (UseBiasedLocking) {
//偏向锁,先撤销
BiasedLocking::revoke_and_rebias(obj, false, THREAD);
}
markOop mark = obj->mark();
if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) {
//是轻量级锁并且锁被当前线程持有的话,直接退出。
return;
}
//膨胀为重量级锁,并调用ObjectMonitor.notify()函数
ObjectSynchronizer::inflate(THREAD, obj())->notify(THREAD);
}
最终还是调用了ObjectMonitor.notify()函数:
void ObjectMonitor::notify(TRAPS) {
...
if (_WaitSet == NULL) {
//等待队列为空,直接返回
return ;
}
//notify 策略,默认是2
int Policy = Knob_MoveNotifyee ;
//获取操作等待队列的锁
Thread::SpinAcquire (&_WaitSetLock, "WaitSet - notify") ;
//将队头节点移出队列-------->(1)
ObjectWaiter * iterator = DequeueWaiter() ;
if (iterator != NULL) {
...
ObjectWaiter * List = _EntryList ;
if (List != NULL) {
//修改节点状态为TS_ENTER
...
}
if (Policy == 0) { // prepend to EntryList
//插入_EntryList 头部
...
} else
if (Policy == 1) { // append to EntryList
//插入到_EntryList 尾部
...
} else
if (Policy == 2) { // prepend to cxq
// 默认策略-------------->(2)
if (List == NULL) {
//_EntryList 为空,则将节点插入到_EntryList 头
iterator->_next = iterator->_prev = NULL ;
_EntryList = iterator ;
} else {
//修改状态为TS_CXQ
iterator->TState = ObjectWaiter::TS_CXQ ;
for (;;) {
ObjectWaiter * Front = _cxq ;
iterator->_next = Front ;
//插入到_cxq头部
if (Atomic::cmpxchg_ptr (iterator, &_cxq, Front) == Front) {
break ;
}
}
}
} else
if (Policy == 3) { // append to cxq
//插入到_cxq尾部
...
} else {
//都不满足,直接唤醒线程
ParkEvent * ev = iterator->_event ;
iterator->TState = ObjectWaiter::TS_RUN ;
OrderAccess::fence() ;
ev->unpark() ;
}
...
}
//释放等待队列的锁
Thread::SpinRelease (&_WaitSetLock) ;
...
}
依然列出了两个重点:
(1)
将节点从等待队列里移出:
#ObjectMonitor.cpp
inline ObjectWaiter* ObjectMonitor::DequeueWaiter() {
//取队头节点
ObjectWaiter* waiter = _WaitSet;
if (waiter) {
DequeueSpecificWaiter(waiter);
}
return waiter;
}
inline void ObjectMonitor::DequeueSpecificWaiter(ObjectWaiter* node) {
...
ObjectWaiter* next = node->_next;
if (next == node) {
//后驱节点与当前节点一致,说明当前等待队列里只有一个节点
//队列置空
_WaitSet = NULL;
} else {
//从队列里移除当前节点,当前节点是队头节点
ObjectWaiter* prev = node->_prev;
next->_prev = prev;
prev->_next = next;
if (_WaitSet == node) {
//将_WaitSet往后移动,指向下一个节点
_WaitSet = next;
}
}
node->_next = NULL;
node->_prev = NULL;
}
(2)
将队头节点从等待队列里取出后,该怎么移动到同步队列里(_cxq、_EntryList)不同策略有不同的操作,以默认策略为例:
1、如果_EntryList为空,则将节点加入到_EntryList队列头部。
2、否则将节点加入到_cxq队列头部。
还是以线程A、B、C为例,当三者调用wait(xx)方法阻塞自己后,等待队列节点顺序为:A-->B-->C(从头到尾),现在另一个线程D调用notify()方法后,等待队列如下:
总结来说,调用Object.notify() 方法底层主要就做了一件事:
将节点从等待队列里移出并加入到同步队列里。
同时通过源码也解释了两个问题:
1、notify 操作没有释放锁。
2、notify 操作正常流程下没有唤醒线程。
notifyAll 解析
顾名思义,就是通知所有的等待节点。
notify是将单个节点从等待队列挪到同步队列,而notifyAll是将等待队列的节点逐个全部挪到同步队列,具体的代码就不贴了,主要看看默认策略下的处理:
#ObjectMonitor.cpp
if (Policy == 2) { // prepend to cxq
// prepend to cxq
iterator->TState = ObjectWaiter::TS_CXQ ;
for (;;) {
ObjectWaiter * Front = _cxq ;
iterator->_next = Front ;
if (Atomic::cmpxchg_ptr (iterator, &_cxq, Front) == Front) {
break ;
}
}
}
与notify不同的是,此处是直接插入到_cxq头部了,也就是说原本在等待队列末尾的节点反而排在了同步队列的前面。
5、wait/notify/notifyAll 流程图
至此,三者原理已经剖析完毕,将三者关系用图串联起来:
6、线程互斥同步下 锁的流程图
结合上篇、本篇文章,已经将互斥与同步下锁的实现流程分析过了,接下来将这两者结合起来看,希望我们能从全局中把控整个流程。
先看一段伪代码:
//线程A调用
synchronized(object) {
object.wait();
//doSomething
}
//线程B调用
synchronized(object) {
object.notify();
//doSomething
}
之前你兴许会有以下疑惑:
1、线程A成功获取锁后,线程B再次获取锁会失败,线程B该如何自处?
2、线程A成功获取锁后,调用wait()方法,此时线程A处在什么状态?
3、线程A退出临界区释放锁后,又做了什么?
4、线程B调用notify()方法,发生了什么?
...
1、线程A成功获取锁后,线程B再次获取锁会失败,线程B该如何自处?
答:
线程B获取锁失败将自己插入到同步队列里,同步队列包括两个队列:_cxq和_EntryList。
此时插入到_cxq的队列的头部。
线程B将自己挂起。
2、线程A成功获取锁后,调用wait()方法,此时线程A处在什么状态?
答:
线程A将自己加入到等待队列了(_WaitSet),方式是插入到等待队列的头部。
释放占用的锁。
线程A将自己挂起。
3、线程A退出临界区释放锁后,又做了什么?
答:
线程A退出临界区后,先释放锁。
然后从同步队列里唤醒等待锁的线程。
根据不同的策略有不同的处理方式,以默认方式为例:
若是_EntryList队列不为空,则取出_EntryList队头节点并唤醒。
若是_EntryList为空,将_EntryList指向_cxq,并取出队头节点唤醒。
4、线程B调用notify()方法,发生了什么?
答:
线程B将等待队列里的头节点取出,并插入到同步队列里。
根据不同的策略有不同的处理方式,以默认方式为例:
如果_EntryList为空,则将节点加入到_EntryList队列头部。
否则将节点加入到_cxq队列头部。
最后用图表示如下:
总结以下,重量级锁的理解核心:
1、锁住的是ObjectMonitor里的_owner字段。
2、操作的是同步队列(_cxq/_EntryList)和等待队列(_WaitSet)。
7、wait/notify/notifyAll 疑难点解析
网上有很多解释重量级锁相关知识的文章,有些解释可能比较牵强。通过本篇的源码分析,相信你已经能够辨别,下面举例几个常出现的疑难点:
问:notify 唤醒线程是随机的吗?
答:
notify在正常的流程下并不会唤醒线程,而只是将等待队列里的节点根据一定的策略挪动到同步队列里。挪动的策略是:选取等待队列里的第一个线程挪到同步队列。也就是说同步队列是满足FIFO,notify的调用不是随机的。
而线程从等待队列取出后放到同步队列的哪个位置要看具体的模式,当某个线程释放锁后,会根据某个模式唤醒同步队列里的线程(具体模式/策略请查看第六点分析)。
也就是说线程虽然从等待队列里出来了,但是不一定就排在同步队列的第一个,也就是说下一个获取锁的线程不一定是它。这也即是官方注释说notify是随机唤醒的意思
问:notify/notifyAll 唤醒的例子
答:
public class TestThread {
static Object object = new Object();
static Thread a, b, c;
public static void main(String args[]) {
a = new Thread(new Runnable() {
@Override
public void run() {
try {
synchronized (object) {
System.err.println("A before wait " + System.nanoTime());
b.start();
Thread.sleep(1000);
object.wait();
System.err.println("A after wait " + System.nanoTime());
}
} catch (Exception e) {
}
}
});
a.start();
b = new Thread(new Runnable() {
@Override
public void run() {
try {
synchronized (object) {
System.err.println("B before wait " + System.nanoTime());
c.start();
Thread.sleep(1000);
object.wait();
System.err.println("B after wait " + System.nanoTime());
}
} catch (Exception e) {
}
}
});
c = new Thread(new Runnable() {
@Override
public void run() {
try {
synchronized (object) {
System.err.println("C before wait " + System.nanoTime());
object.notify();
object.notify();
System.err.println("C after wait " + + System.nanoTime());
}
} catch (Exception e) {
}
}
});
}
}
如上,有线程A、B、C,A启动B、B启动C。
打印如下:
A比B先进同步队列,因此第一个notify的时候先将A移动到_EntryList里,第二个notify将B移动到_cxq头部,最后唤醒的时候优先从_EntryList里取,再从_cxq取。
当将两个notify换成一个notifyAll的时候,结果如下:
调用notifyAll的时候和单独调用多次notify的结果是相反的,这里是先唤醒了B,再唤醒了A,与我们之前的理论分析一致。
问:什么是虚假唤醒?
答:
public class TestThread {
static Object object = new Object();
static Thread a, b, c;
static int count = 0;
public static void main(String args[]) {
a = new Thread(new Runnable() {
@Override
public void run() {
try {
synchronized (object) {
System.err.println("A before wait " + System.nanoTime());
if (count == 0)
object.wait();
count--;
System.err.println("A count:" + count + " " + System.nanoTime());
}
} catch (Exception e) {
}
}
});
a.start();
b = new Thread(new Runnable() {
@Override
public void run() {
try {
synchronized (object) {
System.err.println("B before wait " + System.nanoTime());
if (count == 0)
object.wait();
count--;
System.err.println("B count:" + count + " " + System.nanoTime());
}
} catch (Exception e) {
}
}
});
b.start();
try {
//尽量确保线程A、B都已经运行
Thread.sleep(1000);
} catch (Exception e) {
}
c = new Thread(new Runnable() {
@Override
public void run() {
try {
synchronized (object) {
System.err.println("C before wait " + System.nanoTime());
count++;
object.notifyAll();
System.err.println("C after wait " + +System.nanoTime());
}
} catch (Exception e) {
}
}
});
c.start();
}
}
如上有线程A、B、C。
A、B开启后判断count == 0,于是调用wait()进行挂起,C修改(生产)count后通知所有等待的线程,A、B被唤醒后修改(消费)count,最后打印如下:
可以看到,A被唤醒后拿到的count==-1,这并不是想要的数值。想象一下,若是C往队列里添加了一个元素,A、B被唤醒后都从队列里取出元素,现在元素已经被B取出了,A再取的时候会发生异常。
这就是大家熟知的虚假唤醒,修改一下条件即可预防此种问题:
System.err.println("A before wait " + System.nanoTime());
while (count == 0)
object.wait();
count--;
System.err.println("A count:" + count + " " + System.nanoTime());
当线程被唤醒后,继续查看条件变量,不满足就再次挂起。
当然也不一定非得要加while,要看具体场景,比方说只有一个线程A调用wait,另一个线程B调用notify,这时候的A里没必要加。如果你不确定是否需要加或者不想区分场景是否加,那最好加上,毕竟也只是多了一次判断而已,更加保险。
至此,Synchronized相关知识已经分析完毕,接下来将重点分析AQS,并横向和Synchronized比较。
本文源码基于jdk1.8,运行环境也是jdk1.8。
因此上述demo在你的环境下可能有不同的效果,请注意甄别。虽然不同版本可能有不同的策略,但是核心思想都是一致的。