Lock(五) — LockSupport 和 Condition接口

一、概述

任意一个Java对象,都拥有一组监视器方法(定义在java.lang.Object上)。
主要包括 wait()wait(long timeout)notify()notifyAll() 方法,这些方法与synchronized同步关键字配合,可以实现 等待/通知模式

Object的等待/通知模式如下:
在这里插入图片描述
Condition 接口也提供了类似 Object 的监视器方法,与 Lock 配合可以实现等待/通知模式,但这两者在使用方式以及功能特性上有差别

Object监视器方法与Condition接口对比如下图:
在这里插入图片描述


二、Condition 接口与示例

1. 使用示例

Condition 定义了等待/通知两种类型的方法,Condition对象由Lock.newCondition() 方法创建出来。

注意:
当前线程调用等待/通知两种类型方法时,需要提前获取到Condition对象关联的锁。

Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();

public void conditionWait() throws InterruptedException {
    lock.lock(); //1.先获取锁
    try {
        condition.await(); //2.让当前线程进入等待队列,同时会释放持有的锁。
    } finally {
        lock.unlock(); //3.释放锁
    }
}

public void conditionSignal() throws InterruptedException {
    lock.lock(); //1.先获取锁
    try {
        condition.signal(); //2.唤醒等待队列中头节点线程,使之进入AQS同步队列。
    } finally {
        lock.unlock(); //3.释放锁
    }
}

2. Condition 接口

Condition 接口内的方法主要分为两部分:

  1. await 开头的方法使当前线程进入等待队列;
  2. signal 开头的方法使当前线程从等待队列进入到AQS的同步队列中;

该接口的应用可以参考:ArrayBlockingQueue源码分析
在这里插入图片描述


三、Condition 实现原理

ConditionObject 是同步器 AbstractQueuedSynchronizer 的内部类。每个Condition对象都包含着一个队列(称为等待队列),该队列是Condition对象实现等待/通知功能的关键。

说明:
AbstractQueuedSynchronizer 内维护的队列称为: 同步队列
Condition 内维护的队列称为: 等待队列

下面分析Condition的实现,分为三部分:

  1. 等待队列
  2. 等待
  3. 通知

1. 等待队列

  1. 等待队列是一个FIFO的队列。
  2. 队列中的每个节点都包含了一个线程引用,该线程就是在Condition对象上等待的线程。
  3. 如果一个线程调用了Condition.await()方法,那么该线程将会释放锁、构造成节点加入等待队列并进入等待状态 (节点的定义复用了同步器中节点的定义)。
  4. 一个 Condition 包含一个等待队列,Condition拥有首节点(firstWaiter)和尾节点(lastWaiter)。

当前线程调用 Condition.await() 方法后,将会以当前线程构造节点,并将节点从尾部加入等待队列。加入尾部的过程并没有使用CAS保证,原因在于调用await()方法的线程必定是获取了锁的线程,也就是说该过程是由锁来保证线程安全的。

等待队列的基本结构如图所示:
在这里插入图片描述

Object 和 Lock 中同步队列、等待队列个数的差异:

  1. 在 Object 的监视器模型上,一个对象拥有 一个同步队列一个等待队列
  2. Lock(更确切地说是同步器)拥有 一个同步队列多个等待队列,其对应关系如图所示。
    在这里插入图片描述

2. 等待

调用以 await 开头的方法(如Condition.await()方法),会使当前线程进入等待队列并释放锁,同时线程状态变为等待状态。当从await()方法返回时,当前线程一定获取了Condition相关联的锁。

如果从队列(同步队列和等待队列)的角度看await()方法,当调用await()方法时,相当于同步队列的首节点(获取了锁的节点)移动到Condition的等待队列中。

// AbstractQueuedSynchronizer.ConditionObject.class
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);
}

调用该方法的线程成功获取了锁的线程,也就是同步队列中的首节点,该方法会将当前线程构造成节点并加入等待队列中,然后释放同步状态,唤醒同步队列中的后继节点,然后当前线程会进入等待状态。

当等待队列中的节点被唤醒,则唤醒节点的线程开始尝试获取同步状态。如果不是通过其他线程调用Condition.signal()方法唤醒,而是对等待线程进行中断,则会抛出InterruptedException。

如果从队列的角度去看,当前线程加入Condition 等待队列的过程 如图示。
在这里插入图片描述

3. 通知

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

public final void signal() {
    if (!isHeldExclusively()) //1.校验是否获取锁(在调用signal()方法的前提是当前线程必须持有锁)
        throw new IllegalMonitorStateException();
    Node first = firstWaiter; //2.获取等待队列的首节点
    if (first != null)
        doSignal(first); //3.将等待队列中的首节点移动到同步队列中,并唤醒该节点中的线程。
}

调用 signal() 方法有如下3步骤:

  1. 前置条件: 调用该方法前,当前线程必须获取了锁。因此进行了isHeldExclusively()检查。
  2. 接着获取等待队列的首节点。
  3. 将其移动到同步队列并使用LockSupport唤醒节点中的线程。

节点从等待队列移动到同步队列的过程如图所示。
在这里插入图片描述

通过调用同步器的enq(Node node)方法,等待队列中的头节点线程安全地移动到同步队列。当节点移动到同步队列后,当前线程再使用LockSupport唤醒该节点的线程。

被唤醒后的线程,将从await()方法中的while循环中退出(isOnSyncQueue(Node node) 方法返回true,节点已经在同步队列中),进而调用同步器的 acquireQueued() 方法加入到获取同步状态的竞争中。

成功获取同步状态(或者说锁)之后,被唤醒的线程将从先前调用的await()方法返回,此时该线程已经成功地获取了锁。

Condition的 signalAll() 方法,相当于对等待队列中的每个节点均执行一次signal()方法,效果就是将等待队列中所有节点全部移动到同步队列中,并唤醒每个节点的线程。

4. Condition 的应用

该接口的应用可以参考:ArrayBlockingQueue源码分析


四、LockSupport 类

LockSupport 定义了一组的公共静态方法,这些方法提供了最基本的线程阻塞和唤醒功能,而LockSupport也成为构建同步组件的基础工具。

  1. park 开头的方法用来阻塞当前线程。
  2. unpark(Thread thread) 方法来唤醒一个被阻塞的线程。

在这里插入图片描述


五、参考

  1. 《Java并发编程的艺术》
发布了158 篇原创文章 · 获赞 26 · 访问量 10万+

猜你喜欢

转载自blog.csdn.net/Love667767/article/details/104885348