Java JUC 并发包组件之LockSupport与Condition的使用

本篇我们只介绍LockSupport与Condition的部分API和一些概念,不涉及示例,如果想要示例,可以查看JDK源码的阻塞队列部分,我也写了一篇阻塞队列的文章也可以查看。

Java线程状态与生命周期一篇我们介绍Java的声明周期时介绍过在调用LockSupport的方式时会让线程处于WAITiNG状态和返回RUNNABLE状态。LockSupport定义了一组的公共静态方法,这些方法提供了最基本的线程阻塞和唤醒功能,它是构建同步组件的基础工具。LockSupport定义了一组以park开头的方法用来阻塞当前线程,以及unpark(Thread thread) 方法来唤醒一个被阻塞的线程。如下表为LockSupport提供的API方法:

API方法

描述

void park()

阻塞当前线程,调用void unPack(Thread thread)或者线程中断才返回

void parkNanos(long nanos)

在void park()的基础上添加了阻塞时间,超时也会返回,阻塞最长不超过nanos秒

void parkUntil(long deadline)

在void park()的基础上添加了阻塞时间,超时也会返回,阻塞最长不超过deadline这个时间点。

void park(Object blocker)

扫描二维码关注公众号,回复: 12069396 查看本文章

这是三个方法以上面三个一样,唯一不同的时添加了blocker是用来标识当前线程在等待的对象(阻塞对象)

void parkNanos(Object blocker,long nanos)

void parkUntil(Object blocker,long deadline)

void unPack(Thread thread)

唤醒处于等待状态的线程

 LockSupport的的使用较为简单,相对于LockSupport的使用,我们用的更多的则是Condition接口,在介绍synchronized时,我们已经知道synchronized结合wait()、 wait(long timeout)、notify()以及notifyAll()方法可以实现等待通知机制,它的实现主要是由对象的监视器实现。同样Condition也实现了等待通知机制,不过它使用的是等待队列的方式实现的,这里又用到了AQS的内容。如下为synchronized与Condition接口实现的对比:

使用Condition接口时,必须通过Lock的newCondition方法获取,后面我们会讲解Lock的newCondition方法,这里我们先介绍Condition接口提供的方法,如下表为Condition接口提供的方法:

API方法

描述

void await()

进入等待状态直到被通知,或者中断。

void awaitUninterruptibly()

进入等待状态直到被通知,对中断不敏感

long awaitNanos(long nanosTimeout)

当前线程进入等待状态直到被通知。中断、或者到超时,如果返回0或者负数表示超时,否则返回消耗的时间。

boolean await(long time, TimeUnit unit)

与awaitNanos(long nanosTimeout)一样

boolean awaitUntil(Date deadline)

当前线程进入等待状态直到被通知。中断、或者到某个时间点,如果没有指定时间点就被通知返回true 否则返回false

void signal()

唤醒一个等待在Condition上的线程,该线程从等待方法返回前必须获得与Condition相关的锁

void signalAll()

唤醒所有等待在Condition上的线程,该线程从等待方法返回前必须获得与Condition相关的锁

我们说在获取Condition时,必须获取到锁,我们通过ReentrantLock的newCondition方法查看Lock是如何获取到Condition实例的,代码如下:

final ConditionObject newCondition() {
    return new ConditionObject();
}

上面的代码中返回了一个ConditionObject实例,ConditionObject是Condition的一个实现,它是AQS的一个内部类。每Condition对象都包含着一个队 列(以下称为等待队列),该队列是Condition对象实现等待/通知功能的关键。下面结合源码分析Condition的实现,主要包括三个方向分别为:等待队列、等待和通知。

首先我们看Condition的等待队列,了解过AQS的或许对于等待队列已经有一定的了解,虽然Condition是AQS的子类,但是Condition的等待队列与AQS的等待队列有着一些区别。如下为AQS中对ConditionObject的定义:

public class ConditionObject implements Condition, java.io.Serializable {
    private static final long serialVersionUID = 1173984872572414699L;
    //队列中的第一个等待节点
    private transient Node firstWaiter;
    //队列中最后一个等待节点
    private transient Node lastWaiter;
    //构造方法
    public ConditionObject() { }
    ......
}

一个Condition包含一个等待队列,Condition拥有首节点(firstWaiter)和尾节点 (lastWaiter)。对于Node的定义可以参考AQS的一篇博客:队列同步器 AQS原理解析—独占式获取同步状态。当前线程调用Condition.await()方法,将会以当前线程构造节点,并将节点从尾部 加入等待队列,如下为Condition的等待队列结构图:

如图所示,Condition拥有首尾节点的引用,而新增节点只需要将原有的尾节点nextWaiter 指向它,并且更新尾节点即可。与同步队列不同的是一个Lock实例也就是AQS实例,可以有多个Condition等待队列,如下图为AQS的同步队列与Condition队列结构图:

我们实在调用Condition以await开头的方法时,会使当前线程进入等待队列并释放锁,同时线程状态变为等待状态。我们从源码的角度以await()为例分析Condition的等待过程,代码如下所示:

public final void await() throws InterruptedException {
    //从线程中断抛出异常
    if (Thread.interrupted())
        throw new InterruptedException();
    //将节点添加到等待队列
    Node node = addConditionWaiter();
    //释放锁
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    //如果不是同步队列类型的节点,将线程改为WAITING状态,
    //下面添加的节点的状态为Condition类型,不是同步类型
    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);
}
//将节点添加到等待队列
private Node addConditionWaiter() {
    Node t = lastWaiter;
    // If lastWaiter is cancelled, clean out.
    if (t != null && t.waitStatus != Node.CONDITION) {
        unlinkCancelledWaiters();
        t = lastWaiter;
    }
    //创建节点实例,类型为Condition
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    if (t == null)
        firstWaiter = node;
    else
        t.nextWaiter = node;
        lastWaiter = node;
        return node;
}

调用该方法的线程成功获取了锁的线程,也就是同步队列中的首节点,该方法会将当前线程构造成节点并加入等待队列中,然后释放同步状态,唤醒同步队列中的后继节点,然后当前线程会进入等待状态。同步队列的首节点并不会直接加入等待队列,而是通过addConditionWaiter()方法把当前线程构造成一个新的节点并将其加入等待队列中。其加入队列的结构图如下:

调用Condition的signal()方法,将会唤醒在等待队列中等待时间最长的节点(首节点),在唤醒节点之前,会将节点移到同步队列中。我们以signal()为例如下为源代码:

public final void signal() {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)
    doSignal(first);
}

private void doSignal(Node first) {
    do {
       if ( (firstWaiter = first.nextWaiter) == null)
          lastWaiter = null;
          first.nextWaiter = null;
        }while (!transferForSignal(first) &&(first = firstWaiter) != null);
}
final boolean transferForSignal(Node node) {
    //如果不能改变节点状态,取消
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;

    //家人到同步队列
    Node p = enq(node);
    int ws = p.waitStatus;
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        //唤醒线程
        LockSupport.unpark(node.thread);
        return true;
}

signal()方法进行了isHeldExclusively()检查,检查当前线程是否是获取了锁的线程。接着获取等待队列的首节点,将其移动到同步队列并使用LockSupport唤醒节点中的线程。通过调用同步器的enq(Node node)方法,等待队列中的头节点线程安全地移动到同步队列。当节点移动到同步队列后,当前线程再使用LockSupport唤醒该节点的线程。如下为唤醒的结构图:

LockSupport与Condition的等待通知机制就介绍到此,如果想要使用示例,建议阅读阻塞队列部分源码,因为阻塞队列源码部分大量使用了Condition等待通知机制和LockSupport。这里就不在列举任何例子了。 

猜你喜欢

转载自blog.csdn.net/wk19920726/article/details/108508677