lock锁实现条件队列

一、条件队列的意义

条件(也称为条件队列或条件变量)为一个线程暂停执行(“等待”),直到满足了条件,另一线程通知因为条件不满足而阻塞了的线程。

由于对该共享状态信息的访问发生在不同的线程中,因此必须对其进行保护,因此某种形式的锁与该条件相关联。 等待条件提供的关键属性是它自动释放关联的锁并挂起当前线程,就像Object.wait一样。

二、创建条件队列

使用lock锁来创建条件队列

    static ReentrantLock lock = new ReentrantLock();
	// 假期等待集
    static Condition vocationCondition = lock.newCondition();
	// 价钱等待集
    static Condition moneyCondition = lock.newCondition();

看一下添加底层创建条件队列的源码:

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

在下面会来说条件队列对象ConditionObject。

Condition将Object监控器方法( wait , notify和notifyAll )分解为不同的对象,从而通过与任意Lock实现结合使用,从而使每个对象具有多个等待集。

Lock替换了synchronized方法和语句的使用,而Condition替换了Object监视器方法的使用。

Condition实例从本质上说就是通过同步队列住转移到条件队列,然后再从条件队列转移到同步队列。

二、条件队列结构

ConditionObject对象中只有两个属性:firstWaiter、lastWaiter。队头和队尾。

因为条件队列中队头和队尾是Node类型,而同步队列中的节点类型也是Node类型。

所以二者之间就建立起来了对应的联系。

队列的信息包含以下几个部分:

  1. private transient Node firstWaiter;// 队列的头部结点;
  2. private transient Node lastWaiter;// 队列的尾部节点;
  3. 而Node节点中的nextWaiter表示条件队列中的下一个结点;

条件队列是一个单向链表,在该链表中我们使用nextWaiter属性来串联链表。但是,就像在同步队列中不会使用nextWaiter属性来串联链表一样,在条件队列是中,也并不会用到prev, next属性,它们的值都为null。

队列中节点的信息包含以下几个部分:

  1. 当前节点的线程 thread
  2. 当前节点的状态 waitStatus
  3. 当前节点的下一个节点指针 nextWaiter

所以在分析条件队列中的Node结点的时候,重点关注结点中的属性即可。

三、条件队列和同步队列的区别

在条件队列和同步队列中的节点都是Node类型,所以属性是完全相同的。

在同步队列中,只使用Node结点中的prev、next来串联链表,且是双向链表。

在条件队列中,只是用到了Node结点中的nextWaiter来串联链表,且是单向链表;

但是对于节点Node来说,先从同步队列中转移到条件队列中,然后在从条件队列转移到同步队列。

四、同步队列转移到条件队列和条件队列转移到同步队列

下面来解释下上面说的:先从同步队列中转移到条件队列中,然后在从条件队列转移到同步队列。

(1)同步队列转移到条件队列

直接看代码:

lock.lock();
moneyCondition.await();
lock.unlock();

既然当前线程能够获取得到锁,那么就意味着,当前线程是在同步队列中的。

画个简单的图表示:lock.lock()

而在条件队列调用:moneyCondition.await(); ,这个时候,看下底层做了什么?

看下源代码中的实现:

注意:在条件队列中添加线程的时候,是持有锁的。不存在线程安全问题。

既然在条件队列中添加了当前线程,那么就意味着应该将在同步线程中的线程移除掉。

所以先看下条件队列的初始化过程

4.1、条件队列初始化

每一个线程调用了Condtion对象的await方法的线程都会被包装成新的Node结点扔进一个条件队列中

条件队列对于新来的结点采用的是尾插法

下面来看下源代码中的演示:

对于第一个新来的结点来说,肯定是

当以后的线程过来的时候,如下所示:

形成一个单向链表结构,新来的线程插在尾结点中来。

注意:在条件队列中,我们只需要关注一个值即可那就是Node.CONDITION。它表示线程处于正常的等待状态,条件队列中的waitStatus都应该是CONDITION

而只要waitStatus不是CONDITION,我们就认为线程不再等待了,此时就要从条件队列中出队

4.2、同步队列移除掉当前节点

首先对于当前节点来说,是因为被唤醒了才能够这样子操作。(忽略是因为线程中断引起的)

那么就意味着当前节点是头结点了。

因为条件不满足,当前线程进入到了条件队列中。所以当前同步队列中的线程应该移除掉。

因为在条件队列中和同步队列中的Node已经不是一个对象了,所以这里直接获取得到修改state来释放锁。

看看具体的代码:

因为是当前线程持有锁,所以可以持有锁。但是需要注意的是:现在的head就是当前节点

当前操作中仅仅只是将node节点中的独占锁exclusiveOwnerThread置为null,还有一些属性需要重置。

那么只需要看一下unparkSuccessor方法

此时,node节点中的节点状态pre为空,但是next还有值。需要下一个节点获取得到锁的时候,将next置为null。方便GC

4.3、条件队列转移到同步队列中

此时此刻,条件队列中的node只有两个属性:currentthread和waitStatus=Node.CONDITION

当前线程被唤醒(忽略线程中断),所以满足条件了,再次来尝试得到锁。

这里太过于熟悉了。无非是获取得到了锁,直接执行;又或者是再次进入到阻塞队列中来,等待被唤醒。

如果是没有获取得到锁,那么会将waitStatus状态从Node.CONDITION修改到SIGNAL;

如果是直接获取得到了锁,会在锁释放的时候,将Node.CONDITION改为0;

这里直接分析能够持有锁的情况下,就开始执行临界区代码。

执行完成需要释放锁,那么怎么释放?

是否还记得一行代码?

lock.lock();
moneyCondition.await();
lock.unlock();

lock.unlock();这个是在这里使用到的。

所以上面的三行代码看起来是一起执行的,但是事实上并不是一起执行的。

执行顺序是:lock.lock(); moneyCondition.await();-------->在同步队列中获取得到锁之后,转移到条件队列中阻塞,释放锁;

然后是:moneyCondition.await();lock.unlock();-------->获取得到锁之后,执行临界区代码,然后释放锁;

但是这里也同样的理解了,为什么条件队列为什么要结合着lock锁来执行了。

2.2、同步队列和条件队列

一般情况下,等待锁的同步队列和条件队列条件队列是相互独立的,彼此之间并没有任何关系。

1、条件队列中的线程首先要在同步队列中

但是如果想要使用到条件队列,那么必须得首先持有锁。那么添加到条件队列中的线程,首先第一步需要的进入到同步队列中来,然后从同步队列中转移到条件队列中去。

lock.lock();
moneyCondition.await();
lock.unlock();

而只有调用了await方法,才会进入到条件队列中去。

2、条件队列调用signal方法尝试转移到同步队列中来

但是,当我们调用某个条件队列的signal方法时,会将某个或所有等待在这个条件队列中的线程唤醒,被唤醒的线程和普通线程一样需要去争锁,如果没有抢到,则同样要被加到等待锁的同步队列中去,此时节点就从条件队列中被转移到同步队列中

从网上找来一个图来进行描述:

但是,这里尤其要注意的是,node是被 一个一个转移过去的,哪怕我们调用的是signalAll()方法也是一个一个转移过去的,而不是将整个条件队列接在同步队列的末尾。

同时要注意的是,我们在同步队列中只使用prev、next来串联链表,而不使用nextWaiter;我们在条件队列中只使用nextWaiter来串联链表,而不使用prev、next.事实上,它们就是两个使用了同样的Node数据结构的完全独立的两种链表。 因此,将节点从条件队列中转移到同步队列中时,我们需要断开原来的链接(nextWaiter),建立新的链接(prev, next),这某种程度上也是需要将节点一个一个地转移过去的原因之一。

3、条件队列和同步队列的区别

同步队列是等待锁的队列,当一个线程被包装成Node加到该队列中时,必然是没有获取到锁;当处于该队列中的节点获取到了锁,它将从该队列中移除(事实上移除操作是将获取到锁的节点设为新的对头元素,并将thread属性置为null)。

条件队列是等待在特定条件下的队列,因为调用await方法时,必然是已经获得了lock锁,所以在进入条件队列前线程必然是已经获取了锁;在被包装成Node扔进条件队列中后,线程将释放锁,然后挂起;当处于该队列中的线程被signal方法唤醒后,由于队列中的节点在之前挂起的时候已经释放了锁,所以必须先去再次的竞争锁,因此,该节点会被添加到同步队列中。因此,条件队列在出队时,线程并不持有锁。

而且需要注意的是,当线程调用await方法之后,是先让线程进入到条件队列中,形成单链表之后,才释放锁的。

从而避免线程安全问题。

这句话总结的非常好。

三、源码分析

上面只是对源码中的信息做了分析和总结,而下面才会来看真正的源码。

1、await方法

拿到锁的线程因为不满足某种条件而进入到条件队列中来。想要使用这种效果,需要调用condition.await()方法实现。

public final void await() throws InterruptedException {
  if (Thread.interrupted())
    throw new InterruptedException();
  // 添加节点到条件队列中
  Node node = addConditionWaiter();
  // 释放当前线程所占用的锁,保存当前的锁状态
  int savedState = fullyRelease(node);
  int interruptMode = 0;
  // 如果当前队列不在同步队列中,说明刚刚被await, 还没有人调用signal方法,
  // 则直接将当前线程挂起
  while (!isOnSyncQueue(node)) {
    // 线程挂起的地方
    LockSupport.park(this); 
    // 线程将在这里被挂起,停止运行
    // 能执行到这里说明要么是signal方法被调用了,要么是线程被中断了
    // 所以检查下线程被唤醒的原因。重点代码
    if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
      break;
  }
  // 线程将在同步队列中利用进行acquireQueued方法进行“阻塞式”争锁,
  // 抢到锁就返回,抢不到锁就继续被挂起。因此,当await()方法返回时,
  // 必然是保证了当前线程已经持有了lock锁
  if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
    interruptMode = REINTERRUPT;
  if (node.nextWaiter != null) // clean up if cancelled
    unlinkCancelledWaiters();
  // 因为中断唤醒的
  if (interruptMode != 0)
    reportInterruptAfterWait(interruptMode);
}

线程醒来的原因

线程如果正在阻塞中,醒来的原因无非有两个:

  • 1、被条件队列中的线程调用singal唤醒;
  • 2、阻塞线程因为中断被唤醒

因为支持可中断的方式,所以这里必须得记录一下到底是因为哪种原因导致线程被唤醒的。

        /**
         * Checks for interrupt, returning THROW_IE if interrupted
         * before signalled, REINTERRUPT if after signalled, or
         * 0 if not interrupted.
         */
        private int checkInterruptWhileWaiting(Node node) {
            return Thread.interrupted() ?
                (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
                0;
        }

从源码注释中看:检查是否中断

  • 1、如果在signalled之前,线程已经中断,那么返回THROW_IE(-1)
  • 2、如果在signalled之后,线程已经中断,那么返回THROW_IE(1)
  • 3、如果没有被中断过,那么线程返回0,表示正常返回。正常返回的时候表示的是,是被同步队列中的线程唤醒的;

三种状态都会跳出while循环。但是循环方式不同:

  • 1、如果出现了异常而中断,这里是直接break;
  • 2、如果是正常的被唤醒的,那么返回0。然后再次判断在同步队列中,当前肯定在同步队列中;因为这个而退出来的

然后来尝试获取得到锁来执行。

unlinkCancelledWaiters

这个方法在await方法中体现出来了两次。

1、入队的时候需要判断一下尾结点是否取消了;为了提高效率,先检查一下

2、唤醒醒来的时候,判断下一个结点;

那么来看一下这里的算法是如何实现的:

        private void unlinkCancelledWaiters() {
            Node t = firstWaiter;
            Node trail = null;
            while (t != null) {
                Node next = t.nextWaiter;
                if (t.waitStatus != Node.CONDITION) {
                    t.nextWaiter = null;
                    if (trail == null)
                        firstWaiter = next;
                    else
                        trail.nextWaiter = next;
                    if (next == null)
                        lastWaiter = trail;
                }
                else
                    trail = t;
                t = next;
            }
        }

做个图来表示

经过第一轮循环之后

经过第二轮循环之后

经过第三轮循环之后

然后经过最后一轮循环之后

所以unlinkCancelledWaiters的目的就是为了排除掉waitStatus不为Node.CONDITION(-2)的结点。

transferAfterCancelledWait

        private int checkInterruptWhileWaiting(Node node) {
            return Thread.interrupted() ?
                (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
                0;
        }

如果是因为线程中断而被取消的,那么将会执行第一个逻辑判断。

而线程中断又分为了在signalled之前中断的和在signalled之后中断的。

所谓的中断无非是外部触发了某个条件,需要给当前线程一个中断标记,让线程停止工作而已。

在唤醒之前中断

transferAfterCancelledWait方法

    final boolean transferAfterCancelledWait(Node node) {
      	// 一般说来,这种会终止掉
        if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
            enq(node);
            return true;
        }
        /*
         * If we lost out to a signal(), then we can't proceed
         * until it finishes its enq().  Cancelling during an
         * incomplete transfer is both rare and transient, so just
         * spin.
         */
        while (!isOnSyncQueue(node))
            Thread.yield();
        return false;
    }

中断唤醒的线程,此时可能是没有锁的。CAS交换waitStatus之后,将waitStatus置为0,然后队列。

            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);

在总结这里的时候,发现了一个问题。在从条件队列中转移到同步队列的队列中,node的nextWaiter还是存在着连接的。整个条件队列中还是有指向的。

所以才会有了下面判断:

if (interruptMode != 0) 
  // 因为中断而导致的传播
  reportInterruptAfterWait(interruptMode);
        /**
         * Throws InterruptedException, reinterrupts current thread, or
         * does nothing, depending on mode.
         */
        private void reportInterruptAfterWait(int interruptMode)
            throws InterruptedException {
            if (interruptMode == THROW_IE)
                throw new InterruptedException();
            else if (interruptMode == REINTERRUPT)
                selfInterrupt();
        }

正常情况下是直接抛出异常!而标记成自我中断的,需要在外部循环进行判断。

在唤醒之后中断

用到的时候再来表示。

条件队列将节点添加进同步队列中,并要么立即唤醒线程,要么等待前驱节点释放锁后将自己唤醒,无论怎样,被唤醒的线程要从哪里恢复执行呢?调用了await方法的地方

四、条件队列总结

条件队列是基于lock锁的基础之上建立起来的,进入到条件队列之前,一定是在同步队列中的。

而在条件队列中的线程,只会在唤醒之后,才会进入到同步队列中。这里分为两种情况:1、排队;2、直接可以获取得到锁;

获取得到锁的线程如果满足了条件,那么直接执行临界区代码;如果不满足条件,那么进入到条件队列中进行等待。

典型的生产者/消费者模式。

posted @ 2022-10-21 21:51  写的代码很烂  阅读(32)  评论(0编辑  收藏  举报