本文参考于《Java并发编程的艺术》
开始学习吧⬇️
LockSupport工具
当需要阻塞或唤醒一个线程的时候,都会使用LockSupport工具类来完成。
LockSupport定义了一组park开头的方法用来阻塞当前线程,unpark的方法来唤醒被阻塞的线程。
方法源码如下:
//阻塞当前线程,如果调用unpark方法或者当前线程被中断,才能从park返回
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(false, 0L);
setBlocker(t, null);
}
//阻塞当前线程,最长不超过nanos纳秒
public static void parkNanos(long nanos) {
if (nanos > 0)
UNSAFE.park(false, nanos);
}
//阻塞当前线程,直到deadline时间
public static void parkUntil(long deadline) {
UNSAFE.park(true, deadline);
}
//唤醒处于阻塞状态的线程
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}
有同学去LockSupport类中去研究源码,就发现你这不对啊,源码里面的parkNanos(Object blocker, long nanos) 参数还多一个blocker呢,你都不说,及其敷衍,不看了!
所以,为了留住读者们,那我必然提一嘴👄
Java 6之后,LockSupport就赠加了3个方法,不仅是parkNanos(Object blocker, long nanos) ,还有park(Object blocker)和parkUntil(Object blocker, long deadline)。这时候就有人发现了,没错,都增加了blocker这个对象。这3个方法都是用于实现阻塞当前线程的功能,其中blocker就是用来标记当前线程在等待的对象,其实也就是阻塞对象。
blocker对象的用途是什么呢?
问得好!主要用户问题的排查和系统监控。
在Java 5之前,当前阻塞线程在一个对象上时,通过线程dump能够查看该线程的阻塞对象,但是,Lock的工具类给忘了!!忘了!!
所以Java 6马上就安排上了含有阻塞对象的park方法,代替之前的park方法。
来个对比图看看区别:
通过对比图 可以看到,都是阻塞10秒,但是有阻塞对象的parkNanos就可以给我们传递更多的线程信息。
Condition接口
任意一个Java对象,都拥有一组监视器方法,主要包括wait()、wait(long timeout)、notify()和notifyAll()方法,这些方法与synchronized配合可以实现等待/通知模式。
Condition接口也提供了类似的监视器方法,与Lock配合可以实现等待/通知模式。
二者的区别如下:
接口示例
Condition是依赖Lock对象的。Condition对象是由Lock对象创建出来的。
Condition定义了等待/通知两种类型的方法,当前线程调用这些方法时,需要提前获取到Condition对象关联的锁。
具体使用方式如下:
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void conditionWait() throws InterruptedException{
lock.lock();
try {
condition.await();
}finally {
lock.unlock();
}
}
public void conditionSignal() throws InterruptedException{
lock.lock();
try{
condition.signal();
}finally {
lock.unlock();
}
}
当调用await方法后,当前线程会释放锁并在此等待,而其他线程调用Condition对象的signal方法,通知当前线程后,当前线程才会从await方法返回,并且在返回前已经获取了锁。
获取一个Condition必须通过Lock的newCondition()方法。
Condition定义的部分方法如下,更多方法可以去源码查看。
//当前线程进入等待状态直到被通知或中断
void await() throws InterruptedException;
//当前线程进入等待状态直到被通知
void awaitUninterruptibly();
//当前线程进入等待状态直到被通知,中断或超时。返回值表示剩余时间
long awaitNanos(long nanosTimeout) throws InterruptedException;
//当前线程进入等待状态直到被通知,中断或到某个时间。
//如果没有到指定时间就被通知的,方法返回true,否则,表示到了时间,返回false
boolean awaitUntil(Date deadline) throws InterruptedException;
//唤醒一个登载线程
void signal();
//唤醒所有等待线程
void signalAll();
实现分析
ConditionObject是同步器AbstractQueuedSychronizer的内部类,因为Condition的操作都需要获取相关联的锁。每个Condition对象都包含着一个队列,该队列是Condition对象实现等待/通知功能的关键。
等待队列
等待队列是一个FIFO的队列,在队列中的每个节点都包含了一个线程引用,该线程就是在Condition对象上等待的线程,如果一个线程调用了Condition.await()方法,那么该线程将会释放锁,进入等待状态。
Condition拥有首节点和尾节点。等待队列的基本结构如下图
如图, 新增节点只需要将尾节点nextWaiter指向它,并更新尾节点即可。由于await方法是必定获取了锁的线程,因此是线程安全的。
等待
调用Condition的await()方法或者其他以await开头的方法,会使当前线程进入等待队列并释放锁,同时状态变为等待状态。当从await()方法返回时,当前线程一定获取了相关锁。
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
//当前线程加入等待队列
Node node = addConditionWaiter();
//释放锁
int savedState = fullyRelease(node);
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
我们来看一下源码。首先还是老习惯,先看注释
实现可中断条件等待。
- 如果当前线程被中断,则抛出InterruptedException。
- 保存getState返回的锁定状态
- 以保存的状态作为参数调用release,如果失败,将引发IllegalMonitorStateException。
- 获取锁,直到发出信号或中断。
- 通过调用的专用版本重新获取acquire,保存状态作为参数。
- 如果在步骤4中被阻止时中断,则抛出InterruptedException。
当等待队列中的节点被唤醒,则唤醒节点的线程开始尝试获取同步状态。如果不是通过其他线程调用 Condition.signal()方法唤醒,而是对等待线程进行中断,则会抛出InterruptedException异常。
通知
调用Condition.signal()方法,将等待时间最长的线程(如果存在)从该条件的等待队列移动到所属锁的等待队列。
public final void signal() {
//判断是否获取了锁的线程
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
如果isHeldExclusily返回false,则会抛出IllegalMonitorStateException。调用该方法的条件是当前线程必须获取了锁。接着获取等待队列的首节点,将其移动到同步队列并使用Support唤醒节点中的线程。
节点从等待队列移动到同步队列的过程如图所示,
通过调用同步器的enq方法,等待队列中的头节点线程安全的移动到同步队列。当节点移动到同步队列后,当前线程再使用LockSupport唤醒该节点的线程。被唤醒后的线程,将从awit()方法中的while循环中退出,也就是 isHeldExclusily返回true。然后调用同步器的acquireQueued()方法加入到获取同步状态的竞争。成功获取锁后,被唤醒的线程将从await()方法返回。
直观看一下就是
- 调用同步器的enq(头节点移动到同步队列)
- 使用LockSupport唤醒该节点的线程(从awit方法退出循环)
- 调用同步器的acquireQueued()获取锁
- 线程将从await方法返回
Condition的signalAll()方法相当于等待队列中的每一个节点执行一次signal方法,效果就是将等待队列中所有节点全部移动到同步队列,并唤醒每个节点的线程。