Java并发------AbstractQueuedSynchronizer之Condition(二)

一、Condition简介

1、Condition 的使用场景:

public static void main(String[] args) {
    BoundedBuffer boundedBuffer = new BoundedBuffer();
    new Thread(() -> {
        System.out.println("线程1开始执行");
        for (int i = 0; i < 500; i++) {
            try {
                boundedBuffer.put(i);
                System.out.println("线程1放入:" + i);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }).start();
    new Thread(() -> {
        System.out.println("线程2开始执行");
        for (int i = 0; i < 500; i++) {
            try {
                Object take = boundedBuffer.take();
                System.out.println("线程2取出:" + take);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }).start();
}
static class BoundedBuffer {
    final Lock lock = new ReentrantLock();
    // condition 依赖于 lock 来产生
    final Condition notFull = lock.newCondition();
    final Condition notEmpty = lock.newCondition();
    final Object[] items = new Object[10];
    int putptr, takeptr, count;
    // 生产
    public void put(Object x) throws InterruptedException {
        lock.lock();
        try {
            while (count == items.length){
                System.out.println("队列已满:" + x);
                notFull.await();
            }  // 队列已满,等待,直到 not full 才能继续生产
            items[putptr] = x;
            if (++putptr == items.length) putptr = 0;
            ++count;
            notEmpty.signal(); // 生产成功,队列已经 not empty 了,发个通知出去
        } finally {
            lock.unlock();
        }
    }
    // 消费
    public Object take() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0) {
                System.out.println("队列为空");
                notEmpty.await(); // 队列为空,等待,直到队列 not empty,才能继续消费
            }
            Object x = items[takeptr];
            if (++takeptr == items.length) takeptr = 0;
            --count;
            notFull.signal(); // 被我消费掉一个,队列 not full 了,发个通知出去
            return x;
        } finally {
            lock.unlock();
        }
    }
}

Condition 经常可以用在生产者-消费者的场景中,Condition 是依赖于 ReentrantLock 的,不管是调用 await 进入等待还是 signal 唤醒,都必须获取到锁才能进行操作。

每个 ReentrantLock 实例可以通过调用多次 newCondition 产生多个 ConditionObject 的实例:

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

public class ConditionObject implements Condition, java.io.Serializable {
    // 条件队列的第一个节点
    private transient Node firstWaiter;
    // 条件队列的最后一个节点
    private transient Node lastWaiter;
}

static final class Node {
	//补充上一篇Node中没有介绍的一个成员属性
    Node nextWaiter;
}     

上一篇介绍 AQS 中的阻塞队列 tail,是由一个个 Node 组成,每个 Node 节点都包括前驱和后续节点,所以说阻塞队列是双向链表。

而Condition 可以理解为条件队列,也是由一个个 Node 组成。每次 await 后,当前线程都加入到条件队列中。条件队列是一个单向链表,上面 ConditionObject 这类中可以看出,它只包含两个元素,头节点和尾节点。每个节点 Node 都只指向它的下一个节点 nextWaiter。所以 singal 后,条件队列中的 Node 会转移到阻塞队列中,等到拿到锁后继续执行。
在这里插入图片描述

二、Condition 的实现原理

1、await

线程处于 await 状态后就会将线程挂起,等待其他线程来唤醒自己。

public final void await() throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    //将当前线程加入到条件队列中,返会当前线程对象
    Node node = addConditionWaiter();
    //调用的就是tryRelease,释放锁,返回的是当前node节点的state值
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    //检查是否在阻塞队列中
    while (!isOnSyncQueue(node)) {
        LockSupport.park(this);
        //从名字上理解。如果处在wait状态时,线程被中断,那么退出循环,代表被唤醒
        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);
}

再分步骤看:

1)addConditionWaiter

private Node addConditionWaiter() {
    Node t = lastWaiter;
    if (t != null && t.waitStatus != Node.CONDITION) {
        //这个方法很简单,就是遍历条件队列,把状态不为Node.CONDITION的节点从条件队列中清除
        unlinkCancelledWaiters();
        t = lastWaiter;
    }
    //记住这个Node.CONDITION的状态,代表加入了条件队列中
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    //如果条件队列本身就没有节点的话,头节点设置为当前线程节点即可
    if (t == null)
        firstWaiter = node;
    else
        t.nextWaiter = node;
    lastWaiter = node;
    return node;
}

将当前线程包装为一个 Node 节点,这个 Node 节点是在条件队列中,前面说过条件队列只有 firstWaiter 和 nextWaiter 属性,所以新加入条件队列的节点在队尾,还没有向后的指针,更没有阻塞队列中节点的 next 和 prev 属性。waitStatus 状态设置为 -2,返回当前的线程的 Node 节点。

2)fullyRelease

final int fullyRelease(Node node) {
    boolean failed = true;
    try {
        int savedState = getState();
        if (release(savedState)) {
            failed = false;
            return savedState;
        } else {
            throw new IllegalMonitorStateException();
        }
    } finally {
        if (failed)
            node.waitStatus = Node.CANCELLED;
    }
}

调用的就是 release 方法,上一篇讲过,完全释放独占锁,成功的话返回当前节点的 state 状态值。

3)isOnSyncQueue

final boolean isOnSyncQueue(Node node) {
    if (node.waitStatus == Node.CONDITION || node.prev == null)
        return false;
    if (node.next != null) // If has successor, it must be on queue
        return true;
    //检查阻塞队列中是否有此node节点,有返回true,没有返回false
    return findNodeFromTail(node);
}

这个方法从名字就能看出,检查是否在同步队列中,同步队列其实就是我们所说的阻塞队列。前面 addConditionWaiter 方法介绍过,先加入条件队列中节点是没有 next 和 prev 属性的,所以如果是新加入条件队列的节点一定不在阻塞队列中,返回 false,进入 while 循环;但是如果此节点已经有next ,或者已经在阻塞队列 tail 中,则返回 True,不再进入 while 循环。

记住这里,当进入 while 循环后就会把当前线程挂起;继续向下执行需要其他线程来唤醒当前线程,再继续将线程加入到阻塞队列中,等待获取锁后执行。

下面的方法是唤醒之后执行的。

4)checkInterruptWhileWaiting

private int checkInterruptWhileWaiting(Node node) {
    return Thread.interrupted() ?
        (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
        0;
}
//这里线程处于中断状态才会进入此方法
final boolean transferAfterCancelledWait(Node node) {
    //如果这步 CAS 成功,说明是 signal 方法之前发生的中断,因为如果 signal 先发生的话,signal 中会将 waitStatus 设置为 0
    if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
        // 将节点放入阻塞队列
        // 这里我们看到,即使中断了,依然会转移到阻塞队列
        enq(node);
        return true;
    }
    // 到这里是因为 CAS 失败,肯定是因为 signal 方法已经将 waitStatus 设置为了 0
    // signal 方法会将节点转移到阻塞队列,但是可能还没完成,这边自旋等待其完成
    while (!isOnSyncQueue(node))
        Thread.yield();
    return false;
}

这个方法返回重新赋值 interruptMode 有三种情况:

  • signal 之前已经中断,返回 THROW_IE;
  • signal 之后中断,返回 REINTERRUPT;
  • 没有发生中断,返回 0。

这几个方法是 await 中,检查线程中断状态后准备进入的几个方法,单独拿出来做一下说明:

if (acquireQueued(node, savedState) && interruptMode != THROW_IE)//获得锁后返回线程的中断状态
    interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // 这个再次检查无效节点没什么好说的
    unlinkCancelledWaiters();
if (interruptMode != 0)
    reportInterruptAfterWait(interruptMode);

这段代码我们简化来看,如果没有发生过中断,则不会进入最后一个方法;如果发生中断,进入最后一个方法就只有 THROW_IE、REINTERRUPT两种可能。

5)reportInterruptAfterWait

private void reportInterruptAfterWait(int interruptMode)
    throws InterruptedException {
    if (interruptMode == THROW_IE)
        throw new InterruptedException();
    else if (interruptMode == REINTERRUPT)
        selfInterrupt();
}

最后就是处理中断状态,THROW_IE 会直接报出异常;REINTERRUPT 会重新把线程设置为中断状态。

到这里调用此方法的线程先后经历了线程挂起、线程唤醒、获取到锁,最后返回主线程,并在线程中做了一个是否中断的标记。

2、signal

public final void signal() {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    //唤醒第一个等待的线程
    Node first = firstWaiter;
    if (first != null)
        doSignal(first);
}

分步来看:

1)doSignal

private void doSignal(Node first) {
    do {
        //将条件队列向前移位,如果下一个节点没有,直接把lastWaiter设置为null就可以了,就是没有在等待的节点了
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        //将此节点的相关属性置为空
        first.nextWaiter = null;
    } while (!transferForSignal(first) &&
            (first = firstWaiter) != null);
}

整理条件队列,这个很好理解。

2)transferForSignal

final boolean transferForSignal(Node node) {
    //如果失败则说明节点不在条件队列中
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;
    //自旋入队,返回的入队前阻塞队列尾节点tail,也就是当前node的前驱节点
    Node p = enq(node);
    int ws = p.waitStatus;
    //前驱节点waitStatus大于0,说明取消了排队,则直接唤醒就可以
    //否则就向前驱节点waitStatus设置为-1,等待前驱节点来唤醒
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    return true;
}

这个方法只有在转移失败时候才会返回 false,转移失败的话重新去找下一个可以进行转移的节点即可;转移成功,就退出上面的 while 循环了。

唤醒成功,唤醒的线程重新回到刚才挂起的地方。

三、其他的 await 方法

下面这是 关于 await 带超时机制的几个方法:

public final long awaitNanos(long nanosTimeout) 
                  throws InterruptedException
public final boolean awaitUntil(Date deadline)
                throws InterruptedException
public final boolean await(long time, TimeUnit unit)
                throws InterruptedException

超时的思路很好理解,正常的 await 方法调用的是 park,直接将线程挂起;而带超时机制的 await 调用的 parkNanos,就是指定挂起的时间,当挂起结束后,会查看挂起的过程中有没有调用 signal,也就是查看到底是其他线程调用了 unpark,还是自己醒来,调用了就是没超时,自己醒来就是超时。超时会自己转移到阻塞队列中,等待获取锁,最后返回一个是否超时的布尔值。

最后还有一个不抛异常的 await 方法:

public final void awaitUninterruptibly() {}

这个方法就是最后依然检查线程是否中断,但是不会抛出中断异常。

四、线程状态

下面是线程中断的几个方法:

Thread.currentThread().interrupt();//线程标记中断状态True
Thread.currentThread().isInterrupted();//线程是否中断
boolean interrupted = Thread.interrupted();//返回线程中断状态,并将线程中断状态重新置为False

下面需要知道中断是可以打断线程挂起的:

final Thread[] thread = {null};
new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("挂起前");
        thread[0] = Thread.currentThread();
        //LockSupport.parkNanos(this, 100000000000L);
        LockSupport.park(this);
        System.out.println("挂起后");
    }
}).start();
new Thread(new Runnable() {
    @Override
    public void run() {
        try {
            Thread.sleep(3000);
            System.out.println("其他线程进行中断处理");
            thread[0].interrupt();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}).start();

再来个详细点的例子来看:

ReentrantLock reentrantLock = new ReentrantLock();
Condition condition = reentrantLock.newCondition();
final Thread[] thread = {null};
new Thread(new Runnable() {
    @Override
    public void run() {
        thread[0] = Thread.currentThread();
        System.out.println("线程1添加到等待队列");
        reentrantLock.lock();
        System.out.println("线程1获取到独占锁");
        try {
            Thread.sleep(3000);
            System.out.println("睡3秒后释放锁给其他线程并将线程1转移到条件队列中");
            condition.await();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println("线程1重新获得独占锁");
            condition.signal();
            reentrantLock.unlock();
        }
    }
}).start();
new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("线程2添加到等待队列");
        reentrantLock.lock();
        try {
            System.out.println("线程2获取到独占锁");
            Thread.sleep(3000);
            System.out.println("睡3秒后帮助唤醒线程1,将线程1重新添加到等待队列中");
            thread[0].interrupt();
            System.out.println("将线程2转移到等待队列中");
            condition.await();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println("线程2重新获得独占锁");
            condition.signal();
            reentrantLock.unlock();
        }
    }
}).start();
new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("线程3添加到等待队列");
        reentrantLock.lock();
        try {
            System.out.println("线程3获取到锁");
            Thread.sleep(3000);
            System.out.println("线程3加入到等待队列后睡3秒再释放锁");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            reentrantLock.unlock();
        }
    }
}).start();

这个方法的运行结果是:

线程1添加到等待队列
线程1获取到独占锁
线程2添加到等待队列
线程3添加到等待队列
睡3秒后释放锁给其他线程并将线程1转移到条件队列中
线程2获取到独占锁
睡3秒后帮助唤醒线程1,将线程1重新添加到等待队列中
将线程2转移到等待队列中
线程3获取到独占锁
线程3加入到等待队列后睡3秒再释放锁
java.lang.InterruptedException
	at com.config.test$1.run(test.java:25)
	at java.lang.Thread.run(Thread.java:748)
线程1重新获得独占锁
线程2重新获得独占锁

这个结果可以看出,打断一个线程的挂起状态后,线程会重新回到等待队列中,等待获取锁,但是获取到锁后,线程状态如果是中断状态会抛出一个中断异常出来,这个中断异常我们可以进行处理也可以选择不处理。

所以这个中断异常需求场景而来,如果是在做一些 I/O 操作时,我们是需要知道整个流程是否发生过中断的。另外,lockInterruptibly() 这个方法提供了一旦发生中断,直接取消排队返回,不需要排入等待队列中,可以花很短的时间就检测出线程是否发生过中断。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值