Java-多线程-wait/notify

版权声明:欢迎转载,请注明作者和出处 https://blog.csdn.net/baichoufei90/article/details/85166442

Java-多线程-wait/notify

摘要

wait notify 还有个notifyAll都是线程通信的常用手段。本文会简要介绍其底层实现原理,并和Conditionawaitsignal方法作对比。

有一个先导概念就是对象锁和类锁,他们其实都是对象监视器Object Monitor,只不过类锁是类对象的监视器,可以看另一篇文章:
Java-并发-锁-synchronized之对象锁和类锁

在调用wait和notify之前必须持有对象锁,那么就必须了解synchronized,可以参阅文章:
Java-并发-锁-synchronized

更多关于Java锁的信息,可参考文章:Java-并发-关于锁的一切

0x01 wait

1.1 基本概念

  • 作用
    顾名思义,wait其实就是线程用来做阻塞等待的。
  • 超时参数
    在JDK的Object中,wait方法分为带参数和无参数版本,这里说的参数就是等待超时的参数。
  • 中断
    其他线程在当前线程执行wait之前或正在wait时,对当前线程调用中断interrupted方法,会抛出InterruptedException,且中断标记会被自动清理。

先看没有参数的版本:

/**
 * 让当前线程等待到指定Object,直到其他线程调用该对象的notify或notifyAll方法唤醒
 * 该方法等价于调用wait(0)
 * 
 * 注意 调用该方法前提是拥有该对象的对象锁。否则会报错抛出IllegalMonitorStateException
 * 
 * 当拥有对象锁并调用wait方法时,会释放对象锁,
 * 然后等待,直到其他线程调用该对象的notify或notifyAll方法唤醒那些wait在该对象锁上的线程。
 * 唤醒之后,该线程会尝试去获取对象锁,拿不到就等到直到拿到
 * 拿到对象锁后继续执行程序。
 *
 * 在单参数的wait方法版本中,中断和意料之外的唤醒是可能的所以应该这么做:
 *     synchronized (obj) {
 *         while (condition does not hold)
 *             obj.wait();
 *         ... // Perform action appropriate to condition
 *     }
 *
 * @throws  IllegalMonitorStateException  调用线程未持有该对象的对象锁.
 * @throws  InterruptedException 
 * @see        java.lang.Object#notify()
 * @see        java.lang.Object#notifyAll()
 */
public final void wait() throws InterruptedException {
    wait(0);
}

再看看带1个参数版本的wait方法:

/**
 * 让当前线程等待到指定Object,直到其他线程调用该对象的notify或notifyAll方法唤醒
 * 或是指定wait超时时间耗尽
 * 
 * 注意 调用该方法前提是拥有该对象的对象锁。否则会报错抛出IllegalMonitorStateException
 * 
 * 该方法的原理:
 * 1.调用wait方法的线程将自己加入该对象的等待结合中
 * 2.然后放弃所有和该对象相关的同步锁声明
 * 3.该调用线程随后就不能被调度器调度执行了,进入休眠状态直到以下情况发生:
 *     1.其他线程对目标对象调用notify方法,刚好选中该线程被唤醒
 *     2.其他线程对目标对象调用notifyAll方法唤醒所有线程
 *     3.其他线程对该线程调用interrupt方法发起中断
 *     4.指定的超时时间耗尽,前提是超时时间不是0
 * 4.该线程被唤醒后,从等待该对象的集合中移除,又可以被调度执行了
 * 5.此时会跟其他线程一起竞争该对象的同步锁
 * 6.一旦该线程拿到对象同步锁,所有在wait方法执行前的同步说明都重新起效
 * 7.然后该线程就从wait方法中返回了,该过程结束
 * 
 * 除了上面提到的几种唤醒场景,还有一种极少发生的情况会唤醒线程,称为`伪唤醒`
 * 为了预防,所以应该这么做:
 *    synchronized (obj) {
 *         while (condition does not hold)
 *             obj.wait();
 *         ... // Perform action appropriate to condition
 *    }
 *
 * <p>If the current thread is {@linkplain java.lang.Thread#interrupt()
 * interrupted} by any thread before or while it is waiting, then an
 * {@code InterruptedException} is thrown.  This exception is not
 * thrown until the lock status of this object has been restored as
 * described above.
 * 这段话没看的太明白?
 *
 * 注意这个wait方法只会让该线程释放当前Object的对象锁,而不会放弃拥有的其他对象锁!
 * 
 *
 * @param      timeout   the maximum time to wait in milliseconds.
 * @throws  IllegalArgumentException      if the value of timeout is
 *               negative.
 * @throws  IllegalMonitorStateException  if the current thread is not
 *               the owner of the object's monitor.
 * @throws  InterruptedException 
 * @see        java.lang.Object#notify()
 * @see        java.lang.Object#notifyAll()
 */
public final native void wait(long timeout) throws InterruptedException;

1.2 实现原理

可以先点击这里回顾下关于ObjectWatier的知识。

然后我们继续分析底层源码。

wait/notify/notifyAll代码主要在jdk8/hotspot/src/share/vm/runtime/synchronizer.cpp里。

1.2.1 ObjectSynchronizer::wait

下面看看wait方法底层实现,摘录部分核心代码如下:

// 注意,必须使用重量级monitor来处理wait方法
// 第一个参数是句柄指向了我们wait的目标Object
// 第二个参数是wait的毫秒数
// 第三个是调用wait的线程
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");
  }
  if (millis < 0) {
  // wait超时时间不可小于0
    TEVENT (wait - throw IAX) ;
    THROW_MSG(vmSymbols::java_lang_IllegalArgumentException(), "timeout value is negative");
  }
  // 膨胀为重量级锁,得到该ObjectMonitor
  ObjectMonitor* monitor = ObjectSynchronizer::inflate(THREAD, obj());
  // 调用该monitor的wait方法
  monitor->wait(millis, true, THREAD);
}

1.2.2 ObjectSynchronizer::wait

ObjectMonitor相关代码在

  • /Users/chengc/cc/work/projects/jdk8/hotspot/src/share/vm/runtime/objectMonitor.hpp
  • /Users/chengc/cc/work/projects/jdk8/hotspot/src/share/vm/runtime/objectMonitor.cpp

下面看看wait方法,摘录部分核心代码如下:

void ObjectMonitor::wait(jlong millis, bool interruptible, TRAPS){
// 以本身的ObjectMonitor创建一个ObjectWaiter
ObjectWaiter node(Self);
// 将该ObjectWaiter状态设为TS_WAIT
node.TState = ObjectWaiter::TS_WAIT ;
// 在这个AddWaiter时出现线程竞争的情况很少,所以采用了轻量级的自旋锁
Thread::SpinAcquire (&_WaitSetLock, "WaitSet - add") ;
// 添加该ObjectWaiter node到双向链表WaitSet中
AddWaiter (&node) ;
Thread::SpinRelease (&_WaitSetLock) ;
// 累加waiter
_waiters++;
// 释放ObjectMonitor
// 当调用返回后,其他线程就可以使用enter()方法竞争ObjectMonitor了
exit (true, Self) ;    
// 线程现在可以用park()方法阻塞了
// 代码注释说以后要 change the following logic to a loop of the form
//  while (!timeout && !interrupted && _notified == 0) park()               
}

1.2.3 ObjectMonitor::AddWaiter

使用了队列的尾插法,到WaitSet

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;
  }
}

1.2.4 os::PlatformEvent::park()

该方法在jdk8/hotspot/src/os/linux/vm/os_linux.cpp,主要是通过以下代码实现阻塞:

pthread_mutex_lock(_mutex)
while (_Event < 0) {
   status = pthread_cond_wait(_cond, _mutex);
   // for some reason, under 2.7 lwp_cond_wait() may return ETIME ...
   // Treat this the same as if the wait was interrupted
   if (status == ETIME) { status = EINTR; }
   assert_status(status == 0 || status == EINTR, status, "cond_wait");
}
pthread_mutex_unlock(_mutex);

0x02 notify

2.1 基本概念

  • 该方法用来任意唤醒一个在对象锁的等待集的线程(其实看了源码会发现不是任意的,而是一个WaitQueue,FIFO)。
  • 但要注意,被唤醒的线程不会马上开始运行,因为对象锁还被调用notify的线程拥有,直到退出synchronized块。
  • 唤醒后的线程跟其他线程一起竞争该同步对象锁。
  • 注意,该方法和wait方法一样也必须是拥有该对象同步对象锁的线程才能调用,否则抛出IllegalMonitorStateException
public final native void notify();

2.2 实现原理

2.2.1 ObjectSynchronizer::notify

void ObjectSynchronizer::notify(Handle obj, TRAPS) {
 // 也是先用偏向锁
 if (UseBiasedLocking) {
    BiasedLocking::revoke_and_rebias(obj, false, THREAD);
    assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now");
  }
  // 获取对象头的MarkWord
  markOop mark = obj->mark();
  if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) {
  // 如果拥有的是轻量级锁就直接返回了
    return;
  }
  // 否则膨胀为重量级锁,调用得到的ObjectMonitor的notify方法
  ObjectSynchronizer::inflate(THREAD, obj())->notify(THREAD);
}

2.2.2 ObjectMonitor::notify(TRAPS)

摘录部分核心代码如下:

void ObjectMonitor::notify(TRAPS) {
  // 检查当前线程是否拥有该ObjectMonitor
  CHECK_OWNER();
  ObjectWaiter* iterator;
  if (_WaitSet == NULL) {
  // 如果WaitSet为空就返回了
    TEVENT (Empty-NotifyAll) ;
    return ;
  }
  // 自旋锁方式获取_WaitSetLock
  Thread::SpinAcquire (&_WaitSetLock, "WaitSet - notify") ;
  // 获取WaitSet首节点,并从WaitSet移除该节点
  ObjectWaiter * iterator = DequeueWaiter() ;
  // 在此之后根据Knob_MoveNotifyee不同,对该节点做不同处理,如加入EntryList等
  // 也就是说让该线程能重新竞争ObjectMonitor

  // 最后释放_WaitSetLock
  Thread::SpinRelease (&_WaitSetLock) ;
}

0x03 notifyAll

3.1 基本概念

  • 该方法用来唤醒所有在对象锁的等待集的线程。
  • 但要注意,被唤醒的线程不会马上开始运行,因为对象锁还被调用notifyAll的线程拥有。
  • 唤醒后的线程跟其他线程一起竞争该同步对象锁。
  • 注意,该方法和wait方法一样也必须是拥有该对象同步对象锁的线程才能调用,否则抛出IllegalMonitorStateException
public final native void notifyAll();

3.2 实现原理

跟notify差不多,其实就是循环的方式把WaitSet里的线程节点全部取出,放入EntryList。

0x04 wait与sleep比较

经常面试会问这个问题,往往我们都是网上查资料死记硬背。现在我们都看完了源码(sleep源码点这里),可以得出以下结论

  1. wait会释放ObjectMonitor控制权;sleep不会
  2. wait逻辑复杂,需要首先调用synchronized获取ObjectMonitor控制权,才能调用wait,且wait后还有放入WaitSet逻辑,唤醒时还有一系列复杂操作;而sleep实现简单,不需要别的线程唤醒
  3. wait与sleep都能被中断(除了sleep(0),当然对他中断没有意义)

0x05 Condition.await/signal对比wait/notify

关于Condition介绍可以参考这篇文章:Java-并发-Condition

5.1 Condition和Object关系

等待 唤醒 唤醒全部
Object wait notify notifyAll
Condition await signal signalAll

5.2 wait和await对比

中断 超时精确 Deadline
wait 可中断 可为纳秒 不支持
await 支持可中断/不可中断 可为纳秒 支持

5.3 notify和signal对比

全部唤醒 唤醒顺序 执行前提 逻辑
notify 支持,notifyAll 随机(jdk写的,其实cpp源码是一个wait_queue,FIFO) 拥有锁 从wait_list取出,放入entry_list,重新竞争锁
signal 支持,signalAll 顺序唤醒 拥有锁 从condition_queue取出,放入wait_queue,重新竞争锁

5.4 底层原理对比

  • Object的阻塞和唤醒,前基于synchronized的。底层实现是在cpp级别,调用synchronized的线程对象会放入entry_list,竞争到锁的线程处于active状态。调用wait方法后,线程对象被放入wait_queue。而notify会按FIFO方法从wait_queue中取得一个对象并放回entry_list,这样该线程可以重新竞争synchronized同步锁了。
  • Condition的阻塞唤醒,是基于lock的。lock维护了一个wait_queue,用于存放等待锁的线程。而Condition也维护了一个condition_queue。当拥有锁的线程调用await方法,就会被放入condition_queue;当调用signal方法,会从condition_queue选头一个满足要求的节点移除然后放入wait_queue,重新竞争lock。

5.5 应用场景对比

  • Object使用比较单一,只能针对一个条件。
  • 一个ReentrantLock可以有多个Condition,对应不同条件。比如在生产者消费者可以这样实现:
private static ReentrantLock lock = new ReentrantLock();
	
private static Condition notEmpty = lock.newCondition();
private static Condition notFull = lock.newCondition();

// 生产者
public void produce(E item) {
	lock.lock();
	try {
		while(isFull()) {
		// 数据满了,生产者就阻塞,等待消费者消费完后唤醒
			notFull.await();
		}
		
		// ...生产数据代码
		
		// 唤醒消费者线程,告知有数据了,可以消费
		notEmpty.signal();
	} catch (InterruptedException e) {
		e.printStackTrace();
	} finally {
		lock.unlock();
	}
}

// 消费者
public E consume() {
	lock.lock();
	try {
		while(isEmpty()) {
		// 数据空了,消费者就阻塞,等待生产者生产数据后唤醒
			notEmpty.await();
		}
		
		// ...消费数据代码
		
		// 唤醒生产者者线程,告知有数据了,可以消费
		notFull.signal();
		return item;
	} catch (InterruptedException e) {
		e.printStackTrace();
	} finally {
		lock.unlock();
	}
	return null;
}

这样好处就很明显了。如果使用Object,那么唤醒的时候也许就唤醒了同类的角色线程。而使用condition可以在只有一个锁的情况下,实现我们想要的只唤醒对方角色线程的功能。

0x06 总结

Object的阻塞和唤醒,是基于synchronized的。底层实现是在cpp级别。整个流程串起来如下:

  1. 调用synchronized的线程对象会放入entry_list
  2. 成功竞争到锁的那个线程处于active状态
  3. 调用wait方法后,线程对象被放入wait_queue
  4. notify会按FIFO方法从wait_queue中取得一个对象,并放回entry_list
  5. 调用wait的线程释放锁
  6. 此后该线程可以重新竞争synchronized同步锁了
  7. 竞争到锁的程序,可以继续同步块中的执行代码了

更多关于Java锁的信息,可参考文章:Java-并发-关于锁的一切

0xFF 参考文档

JVM源码分析之Object.wait/notify实现

猜你喜欢

转载自blog.csdn.net/baichoufei90/article/details/85166442