玩转并发-深入AQS和CountDownLatch

概述

CountDownLatch中count down是倒数的意思,latch则是门闩的含义。整体含义可以理解为倒数的门栓,似乎有一点“三二一,芝麻开门”的感觉。CountDownLatch的作用也是如此,在构造CountDownLatch的时候需要传入一个整数n,在这个整数“倒数”到0之前,主线程需要等待在门口,而这个“倒数”过程则是由各个执行线程驱动的,每个线程执行完一个任务“倒数”一次。总结来说,CountDownLatch的作用就是等待其他的线程都执行完任务,必要时可以对各个任务的执行结果进行汇总,然后主线程才继续往下执行。

CountDownLatch使用

CountDownLatch主要有两个方法:countDown()和await()。countDown()方法用于使计数器减一,其一般是执行任务的线程调用,await()方法则使调用该方法的线程处于等待状态,其一般是主线程调用。这里需要注意的是,countDown()方法并没有规定一个线程只能调用一次,当同一个线程调用多次countDown()方法时,每次都会使计数器减一;另外,await()方法也并没有规定只能有一个线程执行该方法,如果多个线程同时执行await()方法,那么这几个线程都将处于等待状态,并且以共享模式享有同一个锁

public static void main(String[] args) {
		//设置CountDownLatch计数器为1
		final CountDownLatch latch = new CountDownLatch(1);
		
		Thread thread1 = new Thread() {
			@Override
			public void run() {
				try {
					latch.await();
					System.out.println("do other thing");
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
		};
		
		Thread thread2 = new Thread() {
			public void run() {
				try {
					Thread.sleep(1000);
					//计数器-1
					latch.countDown();
					System.out.println("释放Latch");
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
		};
		
		thread1.start();
		thread2.start();
	}

打印结果:

释放Latch
do other thing


CountDownLatch源码剖析

CountDownLatch的数据结构

从源码可知,其底层是由AQS提供支持,所以其数据结构可以参考AQS的数据结构,而AQS的数据结构核心就是两个虚拟队列:同步队列sync queue 和条件队列condition queue,不同的条件会有不同的条件队列。它可以用来实现可以依赖 int 状态的同步器,获取和释放参数以及一个内部FIFO等待队列深入理解AQS原理

CountDownLatch源码

CountDownLatch(int count)
构造一个用给定计数初始化的 CountDownLatch。

// 使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断。
void await()
// 使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断或超出了指定的等待时间。
boolean await(long timeout, TimeUnit unit)
// 递减锁存器的计数,如果计数到达零,则释放所有等待的线程。
void countDown()
// 返回当前计数。
long getCount()
// 返回标识此锁存器及其状态的字符串。
String toString()

构造函数:

 //count小于0,则抛出异常
 public CountDownLatch(int count) {
        if (count < 0) throw new IllegalArgumentException("count < 0");
        this.sync = new Sync(count);
    }

内部类Sync,Sync继承于AQS类

private static final class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 4982264981922014374L;

		//Sync构造函数
        Sync(int count) {
            setState(count);
        }
		
		//在AQS中,state是一个private volatile long类型的对象。
		//对于CountDownLatch而言,state表示的”锁计数器“。
		//CountDownLatch中的getCount()
		//最终是调用AQS中的getState(),返回的state对象,即”锁计数器“。
        int getCount() {
            return getState();
        }
		
		//尝试获取共享锁
        protected int tryAcquireShared(int acquires) {
            return (getState() == 0) ? 1 : -1;
        }
		
		//尝试释放共享锁
        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;
            }
        }
    }
//CountDownLatch持有AQS锁的成员变量
private final Sync sync;

Await()方法
调用 Await() 方法时,先去获取 state 的值,当计数器不为0的时候,说明还有需要等待的线程在运行,则调用 doAcquireSharedInterruptibly 方法,进来执行的第一个动作就是尝试加入等待队列 ,即调用 addWaiter()方法

public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }

 //AQS中方法
 public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        //尝试获取共享锁,该方法定义在Sync中
        if (tryAcquireShared(arg) < 0)
        	//若锁获取失败则调用此方法
            doAcquireSharedInterruptibly(arg);
    }

//Sync中尝试获取共享锁
protected int tryAcquireShared(int acquires) {
			//计数器的值为0则获取成功,否则失败返回-1
            return (getState() == 0) ? 1 : -1;
        }

假设此时共享锁获取失败,则开始调用doAcquireSharedInterruptibly()方法
将当前线程加入等待队列,并通过 parkAndCheckInterrupt()方法实现当前线程的阻塞

//AQS中模板方法
private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
        //将当前节点加入等待队列
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            for (;;) {
            	//获取队列中当前节点的前一个节点,如果为head,则继续尝试获取锁
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                    	//获取成功,设置当前节点为head,并唤醒该线程
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                }
                //判断当前节点是否需要阻塞,如果当前节点的前继节点不是head,则需要
                if (shouldParkAfterFailedAcquire(p, node) &&
                	//阻塞线程
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

//唤醒节点
private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below
        //将此节点设置为头节点
        setHead(node);
      	
      	//propagate也就是state的更新值大于0,代表可以继续acquire
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            //判断后继节点是否存在,如果存在是否是共享模式的节点
            //进行共享模式的释放
            
            //在这里h头节点进行了两次判定,第一次是判定旧头节点存在且状态已经被设置过,
            //第二次是判定设置后的头节点是否存在并且状态已经被设置过。
            //只有满足上述的一个条件,就会对其后继节点做判断。
            //后继节点不存在或者后继节点是共享模式,那就可以对整个队列进行释放操作。
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }

 //释放线程,实际就是设置waitStatus为0
 private void doReleaseShared() {
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                //共享模式和独占模式都是满足SIGNAL信号才能唤醒后继节点
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    //CAS操作成功则解除阻塞
                    unparkSuccessor(h);
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            
            //在循环过程中,为了防止在上述操作中添加了新节点的情况
            //通过检测头节点是否改变,如果改变了就继续循环
            if (h == head)                   // loop if head changed
                break;
        }
    }
 
 //解除阻塞
 private void unparkSuccessor(Node node) {
        /*
         * If status is negative (i.e., possibly needing signal) try
         * to clear in anticipation of signalling.  It is OK if this
         * fails or if status is changed by waiting thread.
         */
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         */
        Node s = node.next;
        //如果s为null且当前状态为CANCELLED,则从尾部开始迭代寻找一个不为null且waitStatus设置过,即小于0的数作为要操作的节点
        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;
        }
        //调用Unsafe类的unpark,解除阻塞
        //这是CPU级别的操作
        if (s != null)
            LockSupport.unpark(s.thread);
    }

调用await()方法的线程想要获取锁,有两个条件:

  • 所有持有该锁的线程释放此锁-计数器为0
  • 当前线程被唤醒后,CLH队列中当前线程的前一个线程处于队列的head处。或者队列为空。*

countdonw()释放锁
当计数器为 0 后,会唤醒等待队列里的所有线程,所有调用了 await() 方法的线程都被唤醒,并发执行

public void countDown() {
        sync.releaseShared(1);
    }

//Sync中方法
public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
        	//释放CLH队列中head节点的后驱节点
            doReleaseShared();
            return true;
        }
        return false;
  }
//Sync中方法
protected boolean tryReleaseShared(int releases) {
            // Decrement count; signal when transition to zero
            for (;;) {
            	//不断自旋获取锁的状态,如果计数器为0,则跳出自旋
            	//计数器为0说明锁根本不被占用
                int c = getState();
                if (c == 0)
                    return false;
                //计数-1
                int nextc = c-1;
                //更新锁的状态
                if (compareAndSetState(c, nextc))
                	//当计数器值为0,返回true,可以释放await的线程
                    return nextc == 0;
            }
        }



使用countdown,每次使用计数器值-1,当计数器值为0,释放线程。

总结

  • AQS分为共享模式和独占模式,CountDownLacth使用了它的共享模式
  • AQS 当第一个等待线程(被包装为 Node)要入队的时候,要保证存在一个 head 节点,这个 head 节点不关联线程,也就是一个虚节点。
  • 当队列中的等待节点(关联线程的,非 head 节点)抢到锁,将这个节点设置为 head 节点
  • 第一次自旋抢锁失败后,waitStatus 会被设置为 -1(SIGNAL),第二次再失败,就会被 LockSupport 阻塞挂起
  • 如果一个节点的前置节点为 SIGNAL 状态,则这个节点可以尝试抢占锁
  • 当要释放某个节点时,先将当前节点的waitStatus置为0

猜你喜欢

转载自blog.csdn.net/weixin_40288381/article/details/87693470