CountDownLatch原理

  1.源码

public CountDownLatch(int count) {

    if (count < 0) throw new IllegalArgumentException("count < 0");

    this.sync = new Sync(count);

}

   2.Sync对象

private static final class Sync extends AbstractQueuedSynchronizer {

  private static final long serialVersionUID = 4982264981922014374L;

 

  Sync(int count) {

    setState(count);

  }

 

  int getCount() {

    return getState();

  }

 

  protected int tryAcquireShared(int acquires) {

    return (getState() == 0) ? 1 : -1;

  }

 

  protected boolean tryReleaseShared(int releases) {

    for (;;) {

      int c = getState();   // 获取当前state属性的值

      if (c == 0)   // 如果state0,则说明当前计数器已经计数完成,直接返回

        return false;

      int nextc = c-1;

      if (compareAndSetState(c, nextc)) // 使用CAS算法对state进行设置

        return nextc == 0;  // 设置成功后返回当前是否为最后一个设置state的线程

    }

  }

}

 

  1. 假设我们是这样创建的:new CountDownLatch(5)。其实也就相当于new Sync(5),相当于setState(5)。setState我们可以暂时理解为设置一个计数器,当前计数器初始值为5。
  2. tryAcquireShared方法其实就是判断一下当前计数器的值,是否为0了,如果为0的话返回1(返回1的时候,就表明当前线程可以继续往下走了,不再停留在调用countDownLatch.await()这个方法的地方)。
  3. 这里tryReleaseShared(int)方法即对state属性进行减一操作的代码。可以看到,CAS也即compare and set的缩写,jvm会保证该方法的原子性,其会比较state是否为c,如果是则将其设置为nextc(自减1),如果state不为c,则说明有另外的线程在getState()方法和compareAndSetState()方法调用之间对state进行了设置,当前线程也就没有成功设置state属性的值,其会进入下一次循环中,如此往复,直至其成功设置state属性的值,即countDown()方法调用成功。

我们看到,CountDownLatch重写的方法 tryAcquireShared 实现如下:

protected int tryAcquireShared(int acquires) {

            return (getState() == 0) ? 1 : -1;

        }

判断 state 值是否为0,为0 返回1,否则返回 -1。state 值是 AbstractQueuedSynchronizer 类中的一个 volatile 变量。

private volatile int state;

在 CountDownLatch 中这个 state 值就是计数器,在调用 await 方法的时候,将值赋给 state 。

 

3.await()

await()会调用CountDownLatch 会调用内部类Sync 的 acquireSharedInterruptibly() 方法

acquireSharedInterruptibly(int arg)

public final void acquireSharedInterruptibly(int arg)

            throws InterruptedException {

        if (Thread.interrupted())

            throw new InterruptedException();

        if (tryAcquireShared(arg) < 0)

            doAcquireSharedInterruptibly(arg);

    }

(1) 判断当前线程是否中断

(2) 没中断, 调用tryAcquireShared来判断是不是需要继续阻塞

(3) 当tryAcquireShared返回-1, 证明需要继续阻塞, 进入下面的阻塞过程

doAcquireSharedInterruptibly(int arg)

private void doAcquireSharedInterruptibly(int arg) throws InterruptedException {

  final Node node = addWaiter(Node.SHARED); // 使用当前线程创建一个共享模式的节点

  boolean failed = true;

  try {

    for (;;) {

      final Node p = node.predecessor();    // 获取当前节点的前一个节点

      if (p == head) {  // 判断前一个节点是否为头结点

        int r = tryAcquireShared(arg);  // 查看当前线程是否获取到了执行权限

        if (r >= 0) {   // 大于0表示获取了执行权限

          setHeadAndPropagate(node, r); // 将当前节点设置为头结点,并且唤醒后面处于等待状态的节点

          p.next = null; // help GC

          failed = false;

          return;

        }

      }

     

      // 走到这一步说明没有获取到执行权限,就使当前线程进入搁置状态

      if (shouldParkAfterFailedAcquire(p, node) &&

          parkAndCheckInterrupt())

        throw new InterruptedException();

    }

  } finally {

    if (failed)

      cancelAcquire(node);

  }

}

在doAcquireSharedInterruptibly(int)方法中,首先使用当前线程创建一个共享模式的节点。然后在一个for循环中判断当前线程是否获取到执行权限,如果有(r >= 0判断)则将当前节点设置为头节点,并且唤醒后续处于共享模式的节点;如果没有,则对调用shouldParkAfterFailedAcquire(Node, Node)和parkAndCheckInterrupt()方法使当前线程处于搁置状态,该搁置状态是由操作系统进行的这样可以避免该线程无限循环而获取不到执行权限,造成资源浪费,这里也就是线程处于等待状态的位置,也就是说当线程被阻塞的时候就是阻塞在这个位置。当有多个线程调用await()方法而进入等待状态时,这几个线程都将等待在此处。这里回过头来看前面将的countDown()方法,其会唤醒处于等待队列中离头节点最近的一个处于等待状态的线程,也就是说该线程被唤醒之后会继续从这个位置开始往下执行,此时执行到tryAcquireShared(int)方法时,发现r大于0(因为state已经被置为0了),该线程就会调用setHeadAndPropagate(Node, int)方法,并且退出当前循环,也就开始执行wait()方法之后的代码。这里我们看看setHeadAndPropagate(Node, int)方法的具体实现:

setHeadAndPropagate

private void setHeadAndPropagate(Node node, int propagate) {

  Node h = head;

  setHead(node);    // 将当前节点设置为头节点

  // 检查唤醒过程是否需要往下传递,并且检查头结点的等待状态

  if (propagate > 0 || h == null || h.waitStatus < 0 ||

      (h = head) == null || h.waitStatus < 0) {

    Node s = node.next;

    if (s == null || s.isShared())  // 如果下一个节点是尝试以共享状态获取获取执行权限的节点,则将其唤醒

      doReleaseShared();

  }

}

setHeadAndPropagate(Node, int)方法主要作用是设置当前节点为头结点,并且将唤醒工作往下传递,在传递的过程中,其会判断被传递的节点是否是以共享模式尝试获取执行权限的,如果不是,则传递到该节点处为止(一般情况下,等待队列中都只会都是处于共享模式或者处于独占模式的节点)。也就是说,头结点会依次唤醒后续处于共享状态的节点,这也就是共享锁与独占锁的实现方式。这里doReleaseShared()方法也就是我们前面讲到的会将离头结点最近的一个处于等待状态的节点唤醒的方法。


也就是说这里干了两件事, 首先是把自己的node设置为头节点. 然后把唤醒工作往下传递. 当干完这些事之后. 当前线程就可以执行当初countdownlatch.await()后面自己的逻辑代码了, 所谓的阻塞, 就是shouldParkAfterFailedAcquire(Node, Node)和parkAndCheckInterrupt()引起的当前线程的临时搁置.
 

4.countDown

public void countDown() {

    sync.releaseShared(1);

}

   在countDown()方法中调用的sync.releaseShared(1)调用时实际还是调用的tryReleaseShared(int)方法,如下是releaseShared(int)方法的实现:

public final boolean releaseShared(int arg) {

    if (tryReleaseShared(arg)) {

        doReleaseShared();

        return true;

    }

    return false;

}

 

protected boolean tryReleaseShared(int releases) {

    // Decrement count; signal when transition to zero

    for (;;) {

        int c = getState();

        if (c == 0)

            return false;

        int nextc = c-1;

        if (compareAndSetState(c, nextc))

            return nextc == 0;

    }

}

可以看到,在执行sync.releaseShared(1)方法时,其在调用tryReleaseShared(int)方法时会在无限for循环中设置state属性的值,设置成功之后其会根据设置的返回值(此时state已经自减了一),即当前线程是否为将state属性设置为0的线程,来判断是否执行if块中的代码。doReleaseShared()方法主要作用是唤醒调用了await()方法的线程。需要注意的是,如果有多个线程调用了await()方法,这些线程都是以共享的方式等待在await()方法处的,试想,如果以独占的方式等待,那么当计数器减少至零时,就只有一个线程会被唤醒执行await()之后的代码,这显然不符合逻辑。如下是doReleaseShared()方法的实现代码:

 

private void doReleaseShared() {

  for (;;) {

    Node h = head;  // 记录等待队列中的头结点的线程

    if (h != null && h != tail) {   // 头结点不为空,且头结点不等于尾节点

      int ws = h.waitStatus;

      if (ws == Node.SIGNAL) {  // SIGNAL状态表示我这个线程对应的节点正在等待被唤醒

        if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))    // 清除当前节点的等待状态

          continue;

        unparkSuccessor(h); // 唤醒头节点的下一个节点,头结点是空的,里面没有线程,头结点的下一个才是有线程信息的节点

      } else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))

        continue;

    }

    if (h == head)  // 如果h还是指向头结点,说明前面这段代码执行过程中没有其他线程对头结点进行过处理

      break;

  }

}

在doReleaseShared()方法中(始终注意当前方法是最后一个执行countDown()方法的线程执行的),首先判断头结点不为空,且不为尾节点,说明等待队列中有等待唤醒的线程,这里需要说明的是,在等待队列中,头节点中并没有保存正在等待的线程,其只是一个空的Node对象,真正等待的线程是从头节点的下一个节点开始存放的,因而会有对头结点是否等于尾节点的判断。在判断等待队列中有正在等待的线程之后,其会清除头结点的状态信息,并且调用unparkSuccessor(Node)方法唤醒头结点的下一个节点,使其继续往下执行。如下是unparkSuccessor(Node)方法的具体实现:

 

private void unparkSuccessor(Node node) {

    int ws = node.waitStatus;

    if (ws < 0)

        compareAndSetWaitStatus(node, ws, 0);

 

    Node s = node.next;

    if (s == null || s.waitStatus > 0) {

        s = null;

        for (Node t = tail; t != null && t != node; t = t.prev)

            if (t.waitStatus <= 0)

                s = t;

    }

    if (s != null)

        LockSupport.unpark(s.thread);

}

可以看到,unparkSuccessor(Node)方法的作用是唤醒离传入节点最近的一个处于等待状态的线程,使其继续往下执行。前面我们讲到过,等待队列中的线程可能有多个,而调用countDown()方法的线程只唤醒了一个处于等待状态的线程,这里剩下的等待线程是如何被唤醒的呢?其实这些线程是被当前唤醒的线程唤醒的。

 

具体的我们可以看看await()方法的具体执行过程。离head节点最近的线程阻塞以后, 被unparkSuccessor唤醒. 满足了计数为0, 前置节点为head的条件. 得以继续往下执行-->setHeadAndPropagate

 

private void setHeadAndPropagate(Node node, int propagate) {

    Node h = head;

    setHead(node);// 我把自己的节点设置为head

   

//然后检查唤醒过程是不是要往下传递

    if (propagate > 0 || h == null || h.waitStatus < 0 ||

        (h = head) == null || h.waitStatus < 0) {

        Node s = node.next;

        if (s == null || s.isShared())

            doReleaseShared();

    }

}

因为我被唤醒了, 我执行完setHeadAndPropagate之后, 就可以跳出await, 进行下面自己的逻辑了. 所以我这个节点也就用没用了, 我把自己的节点设置为head. 然后检查唤醒过程是不是要往下传递. 然后进行唤醒操作. 唤醒下一个线程. 下一个线程被唤醒后, 又可以执行

因为我被唤醒了, 我执行完setHeadAndPropagate之后, 就可以跳出await, 进行下面自己的逻辑了. 所以我这个节点也就用没用了, 我把自己的节点设置为head. 然后检查唤醒过程是不是要往下传递. 然后进行唤醒操作

.....重复下去就完成了所有线程await的等待激活.

 

5.总过程:

await内部实现流程:

(1)判断state计数是否为0,是0,那么可以执行执行await后面的逻辑

(2)state大于0,则表示需要阻塞等待计数为0

(3)当前线程封装Node对象,进入阻塞队列

(4)不满足激活条件(前置节点是head, 计数为0), 会被操作系统给挂起, 等待激活

(5)被唤醒后, 执行后面的继续唤醒操作,重置头节点状态, 检查唤醒传递等待

(6)跳出await的循环, 开始自己的业务逻辑

countDown内部实现流程:

(1)尝试释放锁tryReleaseShared,实现计数-1

若计数已经小于0,则直接返回false

否则执行计数(AQS的state)减一

(2)若减完之后,state==0,然后就需要唤醒被阻塞的线程了doReleaseShared

如果队列为空,即表示没有线程被阻塞(也就是说没有线程调用了 CountDownLatch#wait()方法),直接退出

(3)头结点如果为SIGNAL, 则依次唤醒头结点下个节点上关联的线程,并出队

(4)唤醒的线程会从await的第5步后开始执行

 

你可能感兴趣的:(java基础)