• CountDownLatch的实现原理


    CountDownLatch是java并发包中辅助并发的工具类,目的是让并发运行的代码在某一个执行点阻塞,直到所有条件都满足,这里的条件就是调用countDown()方法,有点类似计数器的功能。

    用法如

        public static void main(String[] args) throws InterruptedException {
            CountDownLatch countDownLatch = new CountDownLatch(2);
            //如果没有countDown()操作,直接调用await方法则永远不会打印最后一行
            countDownLatch.countDown();
            countDownLatch.countDown();
            countDownLatch.await();
            System.out.println("运行结束");
        }
    

    构造函数中传入的数字2,表示需要2次countDown()方法调用,否则代码会一直阻塞在await()方法调用处  

     比较常见的用法如,主线程声明一个CountDownLatch,然后多线程countDown,主线程在等待

     1     public static void testMultiThread() throws InterruptedException {
     2         int threadNum = 2;
     3         final CountDownLatch countDownLatch = new CountDownLatch(threadNum);
     4         ExecutorService executorService = Executors.newFixedThreadPool(threadNum);
     5         for(int i=0;i<threadNum;i++){
     6             executorService.execute(new Runnable() {
     7                 public void run() {
     8                     System.out.println("我的任务,打印出一句话");
     9                     countDownLatch.countDown();
    10                 }
    11             });
    12         }
    13         countDownLatch.await();
    14         System.out.println("全部任务都结束了,欧耶");
    15         executorService.shutdown();
    16     }

    运行结果

    我的任务,打印出一句话
    我的任务,打印出一句话
    全部任务都结束了,欧耶
    
    Process finished with exit code 0
    

    如果把第三行代码修改成

     final CountDownLatch countDownLatch = new CountDownLatch(threadNum+1);
    

      

    那么程序将永远无法打印出
    全部任务都结束了,欧耶

    以上是这个类的表象行为,那么它是如何在多线程做到这样的功能呢

    先来看看它的类结构

    CountDownLacth中,有6个public的方法,一个内部私有类Sync,及一个Sync实例的变量sync

    Sync是这个类的关键,它保证了countDown(),await()方法在多线程场景下可以保证countDownLatch的可见性(正常的同步)

    我们先来自己实现一个CountDownLacth类,使用synchronized关键字实现

    MyCountDownLatch模拟了countDown和await方法,通过synchronized和私有变量state来达到这个目的。synchronized的劣势在于锁机制完全互斥,并发量高时性能下降比较明显,无法维持常态化的性能(JDK 5)。因此CountDownLatch以及并发包中的类都采用了取巧的方式,通过线程自旋来追求线程响应时间,而不是让线程只能一直等待锁被释放再竞争。1.6之后,synchronized的性能和ReentrantLock的性能其实已经相当,偏向锁也改进了一个线程重复获取锁时不需要cpu切换上下文。

    private static final class Sync extends AbstractQueuedSynchronizer {
            private static final long serialVersionUID = 4982264981922014374L;
    
         //初始化总的countdown次数值,这个操作涉及内存语义 volatile写 Sync(int count) { setState(count); } 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; } } }

      

    CountDownLatchd countDown方法会调用

    Sync的tryReleaseShared去将计数器-1

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

     

    AbstractQueuedSynchronizer类方法
    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }
    

     

    public void await() throws InterruptedException {
            sync.acquireSharedInterruptibly(1);
        }
    
    AbstractQueuedSynchronizer类方法
    public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }
    
    AbstractQueuedSynchronizer类方法
    /**
     * Acquires in shared interruptible mode.
     * @param arg the acquire argument
     */
    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) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
    

      

     

     

    await()方法Sync的tryAcquireShared方法不断判断计数器值为0,方法返回正数,await方法直接返回,亦即是获取了锁

     

      

    AbstractQueuedSynchronizer类是concurrent包的一个基础类,基于它,我们可以实现并发安全的诸多功能,例如CountDownLacth

        /**
         * The synchronization state.
         */
        private volatile int state;
    

      

    AbstractQueuedSynchronizer类有一个volatile的变量,CountDownLacth中的Sync类,getState()方法其实就是获取它的值
    由于volatile关键字的特殊内存语义,当它被修改时,将本地高速缓存中的值写到主存中去,当它的值被读取时,会把本地缓存中state置为无效,到主存获取值,因此每个线程都能获取到他最新的值

       countDownLatch.countDown(); 
       -> sync.releaseShared(1);
       -> 
       
        public final boolean releaseShared(int arg) {
    	//countdown时,尝试判断当前状态,如果状态已经可以完全释放锁时,进行释放锁操作
            if (tryReleaseShared(arg)) {
                doReleaseShared(); //释放共享锁
                return true;
            }
            return false;
        }
    	
    	
    	/**
         * Release action for shared mode -- signals successor and ensures
         * propagation. (Note: For exclusive mode, release just amounts
         * to calling unparkSuccessor of head if it needs signal.)
         */
        private void doReleaseShared() {
            /* 判断是否已经有线程(node)在等待,如果在等待,则从head开始,寻找后继节点,并唤醒他们
    		 * 直到最后head 为null或head == tail,才退出,退出时,已然唤醒了所有需要唤醒的线程
             * Ensure that a release propagates, even if there are other
             * in-progress acquires/releases.  This proceeds in the usual
             * way of trying to unparkSuccessor of head if it needs
             * signal. But if it does not, status is set to PROPAGATE to
             * ensure that upon release, propagation continues.
             * Additionally, we must loop in case a new node is added
             * while we are doing this. Also, unlike other uses of
             * unparkSuccessor, we need to know if CAS to reset status
             * fails, if so rechecking. 
             */
            for (;;) {
                Node h = head;  //等待获取锁的队列
                if (h != null && h != tail) {    //head == tail 时,是一个特殊情况,是第一个node入队时的临时状态,此时head节点上没有等待的线程
                    int ws = h.waitStatus;
                    if (ws == Node.SIGNAL) {
                        if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                            continue;            // loop to recheck cases
                        unparkSuccessor(h);  //head唤醒后继线程
                    }
                    else if (ws == 0 &&
                             !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))  //尝试设置waitStatus状态为PROPAGATE
                        continue;                // loop on failed CAS
                }
                if (h == head)                   // loop if head changed 
                    break;
            }
        }
    

      

    	countDownLatch.await();
    	->  sync.acquireSharedInterruptibly(1);
    	->     
    	/**
         * Acquires in shared mode, aborting if interrupted.  Implemented
         * by first checking interrupt status, then invoking at least once
         * {@link #tryAcquireShared}, returning on success.  Otherwise the
         * thread is queued, possibly repeatedly blocking and unblocking,
         * invoking {@link #tryAcquireShared} until success or the thread
         * is interrupted.
         * @param arg the acquire argument.
         * This value is conveyed to {@link #tryAcquireShared} but is
         * otherwise uninterpreted and can represent anything
         * you like.
         * @throws InterruptedException if the current thread is interrupted
         */
        public final void acquireSharedInterruptibly(int arg)
                throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            if (tryAcquireShared(arg) < 0)  //尝试判断共享锁状态,即判断state是否为0了,如果已经为0,表示期待的状态已经达到了,锁的状态已经标识不需等待了,直接返回
                doAcquireSharedInterruptibly(arg); //为0后
        }
    	
    	/**
         * Acquires in shared interruptible mode.
         * @param arg the acquire argument
         */
        private void doAcquireSharedInterruptibly(int arg)
            throws InterruptedException {
            final Node node = addWaiter(Node.SHARED); //当前线程装入node,此时等待队列不为空了
            boolean failed = true;
            try {
                for (;;) {
                    final Node p = node.predecessor();  //当前线程的前继线程
                    if (p == head) { //前继为head
                        int r = tryAcquireShared(arg); //判断state状态,是否已经ok
                        if (r >= 0) {  //state值为0时,r == 1
                            setHeadAndPropagate(node, r); //当前node设置为head,尝试获取下一个node
                            p.next = null; // help GC
                            failed = false;
                            return;
                        }
                    }
                    if (shouldParkAfterFailedAcquire(p, node) &&
                        parkAndCheckInterrupt())  //尝试park(阻塞)当前线程,有点像进入wait状态,处理器可能不会分配时间
                        throw new InterruptedException();
                }
            } finally {
                if (failed)
                    cancelAcquire(node);
            }
        }
    

     这里需要说明一下,node的waitStatus是在何时被设置为Node.SIGNAL状态的

    shouldParkAfterFailedAcquire方法中,pred参数为当前node的前继node,方法一进来就判断了pred.waitStatus是否为Node.SIGNAL,如果已经是这个状态了就直接返回
    ws>0表示CANCELLED状态,因为其他状态都是<=0的,此时遍历的将这种无效的node去掉;如果ws <= 0,就将pred的状态变成Node.SIGNAL,并返回false;这里可以看到,如果前缀node已经是SIGNAL状态
    就会park当前节点线程,如果,前缀node是未cancel状态,就设置为SIGNAL状态。这样,在调用await时,如果无法立即返回,就会将当前线程阻塞,并设置前置node状态为SIGNAL。这也对应了releaseShared
    中的Node.SIGAL状态的判断。await()是从tail -> head方向做的;countdown(releaseShared)的方向是从head -> tail的,这样,等待锁的线程不断判断前缀node,释放锁的线程不断更新head -> tail
    状态,随着head -> tail状态更新完毕,await等待锁也等到了p == head,返回true

      /**
         * Checks and updates status for a node that failed to acquire.
         * Returns true if thread should block. This is the main signal
         * control in all acquire loops.  Requires that pred == node.prev.
         *
         * @param pred node's predecessor holding status
         * @param node the node
         * @return {@code true} if thread should block
         */
        private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
            int ws = pred.waitStatus;
            if (ws == Node.SIGNAL)
                /*
                 * This node has already set status asking a release
                 * to signal it, so it can safely park.
                 */
                return true;
            if (ws > 0) {
                /*
                 * Predecessor was cancelled. Skip over predecessors and
                 * indicate retry.
                 */
                do {
                    node.prev = pred = pred.prev;
                } while (pred.waitStatus > 0);
                pred.next = node;
            } else {
                /*
                 * waitStatus must be 0 or PROPAGATE.  Indicate that we
                 * need a signal, but don't park yet.  Caller will need to
                 * retry to make sure it cannot acquire before parking.
                 */
                compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
            }
            return false;
        }
    

      

    Java的LockSupport.park()实现分析 参见博客: https://blog.csdn.net/hengyunabc/article/details/28126139

    附waitStatus的各种状态
    	
    	/**
             * Status field, taking on only the values:
             *   SIGNAL:     The successor of this node is (or will soon be)
             *               blocked (via park), so the current node must
             *               unpark its successor when it releases or
             *               cancels. To avoid races, acquire methods must
             *               first indicate they need a signal,
             *               then retry the atomic acquire, and then,
             *               on failure, block. 线程的后继线程正/已被阻塞,当该线程release或cancel时,要唤醒这个后继线程(unpark)
             *   CANCELLED:  This node is cancelled due to timeout or interrupt.
             *               Nodes never leave this state. In particular,
             *               a thread with cancelled node never again blocks. 由于timeout或线程被中断时的状态
             *   CONDITION:  This node is currently on a condition queue.
             *               It will not be used as a sync queue node
             *               until transferred, at which time the status
             *               will be set to 0. (Use of this value here has
             *               nothing to do with the other uses of the
             *               field, but simplifies mechanics.) 表明该线程被处于条件队列,就是因为调用了Condition.await而被阻塞
             *   PROPAGATE:  A releaseShared should be propagated to other
             *               nodes. This is set (for head node only) in
             *               doReleaseShared to ensure propagation
             *               continues, even if other operations have
             *               since intervened.   传播共享锁,只能在head上设置这个状态
             *   0:          None of the above
             *
             * The values are arranged numerically to simplify use.
             * Non-negative values mean that a node doesn't need to
             * signal. So, most code doesn't need to check for particular
             * values, just for sign.
             *
             * The field is initialized to 0 for normal sync nodes, and
             * CONDITION for condition nodes.  It is modified using CAS
             * (or when possible, unconditional volatile writes).
             */
            volatile int waitStatus;
    

      




  • 相关阅读:
    MongoDB学习:(一)MongoDB安装
    事件轮询 Event Loop
    常见的HTML5语义化标签
    前端动画性能优化方案
    前端动画的实现
    《SVN的操作流程及规范》
    css、js文件后的后缀作用是什么?
    实现单行文字溢出显示...,以及多行文字溢出显示...
    从输入URL到页面返回的过程详解
    jQuery实现点击复制效果
  • 原文地址:https://www.cnblogs.com/windliu/p/5809759.html
Copyright © 2020-2023  润新知