【问题标题】:How do I "cancel" a CountDownLatch?如何“取消”CountDownLatch?
【发布时间】:2012-05-04 18:01:59
【问题描述】:

我有多个消费者线程使用await() 在大小为 1 的 CountDownLatch 上等待。我有一个生产者线程,它在成功完成时调用countDown()

在没有错误的情况下效果很好。

但是,如果生产者检测到错误,我希望它能够向消费者线程发出错误信号。理想情况下,我可以让生产者调用 abortCountDown() 之类的东西,并让所有消费者收到 InterruptedException 或其他一些异常。我不想调用countDown(),因为这需要我所有的消费者线程在调用await() 后进行额外的手动检查是否成功。我宁愿他们只收到一个他们已经知道如何处理的异常。

我知道CountDownLatch 中没有中止功能。是否有另一个同步原语可以轻松适应以有效地创建支持中止倒计时的CountDownLatch

【问题讨论】:

    标签: java concurrency java.util.concurrent


    【解决方案1】:

    JB Nizet 给出了一个很好的答案。我拿了他的,稍微打磨了一下。结果是一个名为 AbortableCountDownLatch 的 CountDownLatch 的子类,它向该类添加了一个“abort()”方法,这将导致所有等待锁存器的线程接收 AbortException(InterruptedException 的子类)。

    此外,与 JB 的类不同,AbortableCountDownLatch 将在中止时立即中止所有阻塞线程,而不是等待倒计时达到零(对于使用计数>1 的情况)。

    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.TimeUnit;
    
    public class AbortableCountDownLatch extends CountDownLatch {
        protected boolean aborted = false;
    
        public AbortableCountDownLatch(int count) {
            super(count);
        }
    
    
       /**
         * Unblocks all threads waiting on this latch and cause them to receive an
         * AbortedException.  If the latch has already counted all the way down,
         * this method does nothing.
         */
        public void abort() {
            if( getCount()==0 )
                return;
    
            this.aborted = true;
            while(getCount()>0)
                countDown();
        }
    
    
        @Override
        public boolean await(long timeout, TimeUnit unit) throws InterruptedException {
            final boolean rtrn = super.await(timeout,unit);
            if (aborted)
                throw new AbortedException();
            return rtrn;
        }
    
        @Override
        public void await() throws InterruptedException {
            super.await();
            if (aborted)
                throw new AbortedException();
        }
    
    
        public static class AbortedException extends InterruptedException {
            public AbortedException() {
            }
    
            public AbortedException(String detailMessage) {
                super(detailMessage);
            }
        }
    }
    

    【讨论】:

    • 如何使用这个类,我的情况是我有一个列表并且列表是实时动态更新的。列表中有自动事件,我必须等待 2 分钟,但如果在等待时间之间用户手册事件进入列表,我必须中断等待并立即继续。
    • 我不相信这个类是线程安全的。例如,如果一个或多个线程同时调用countDown()abort(),则abort() 的while 循环中存在潜在问题。
    【解决方案2】:

    在内部使用 CountDownLatch 将此行为封装在特定的更高级别的类中:

    public class MyLatch {
        private CountDownLatch latch;
        private boolean aborted;
        ...
    
        // called by consumers
        public void await() throws AbortedException {
            latch.await();
            if (aborted) {
                throw new AbortedException();
            }
        }
    
        // called by producer
        public void abort() {
            this.aborted = true;
            latch.countDown();
        }
    
        // called by producer
        public void succeed() {
            latch.countDown();
        }
    }
    

    【讨论】:

    • 否,因为线程安全由 CountDownLatch 确保:countDown 方法确保线程之间存在内存屏障。 javadoc 说:“在调用 countDown() 之前线程中的操作发生在从相应的 await() 成功返回之后的操作”
    • @JBNizet 我也在使用 CDL,但不是像您的示例中那样使用布尔值,而是使用一个列表,该列表仅在启动任何工作线程和工作线程之前由主线程编写线程也写在列表中。主线程调用 cdl.await(),工作线程调用 cdl.countDown()。在这里不使用同步列表是否安全?
    • @Timmos 是的,这很安全:CountDownLatch 保证在 countDown() 之前所做的每个更改都会被等待它的线程看到。
    • @JBNizet 在我的情况下,我猜它仍然不安全:工作线程正在并行写入列表,并且工作线程之间没有发生之前的关系,但只有主线程。但是,您对happens-before 的评论是一个很好的补充。到目前为止,我完成了 Java 并发实践,涵盖该主题并确实证实了您在这里所说的内容的部分是第 16 章,或者更具体地说是第 16.1.4 小节。 “捎带同步”。在此示例中,布尔值搭载 CountDownLatch 的同步。
    • 我错过了说工作线程正在并行写入列表的部分。那么确实,你需要同步。但这与 CountDownLatch 没有太大关系。不安全的部分发生在 await() 调用返回之后。
    【解决方案3】:

    您可以围绕CountDownLatch 创建一个包装器,以提供取消服务员的功能。它需要跟踪等待的线程并在它们超时时释放它们,并记住闩锁已被取消,因此将来对await 的调用将立即中断。

    public class CancellableCountDownLatch
    {
        final CountDownLatch latch;
        final List<Thread> waiters;
        boolean cancelled = false;
    
        public CancellableCountDownLatch(int count) {
            latch = new CountDownLatch(count);
            waiters = new ArrayList<Thread>();
        }
    
        public void await() throws InterruptedException {
            try {
                addWaiter();
                latch.await();
            }
            finally {
                removeWaiter();
            }
        }
    
        public boolean await(long timeout, TimeUnit unit) throws InterruptedException {
            try {
                addWaiter();
                return latch.await(timeout, unit);
            }
            finally {
                removeWaiter();
            }
        }
    
        private synchronized void addWaiter() throws InterruptedException {
            if (cancelled) {
                Thread.currentThread().interrupt();
                throw new InterruptedException("Latch has already been cancelled");
            }
            waiters.add(Thread.currentThread());
        }
    
        private synchronized void removeWaiter() {
            waiters.remove(Thread.currentThread());
        }
    
        public void countDown() {
            latch.countDown();
        }
    
        public synchronized void cancel() {
            if (!cancelled) {
                cancelled = true;
                for (Thread waiter : waiters) {
                    waiter.interrupt();
                }
                waiters.clear();
            }
        }
    
        public long getCount() {
            return latch.getCount();
        }
    
        @Override
        public String toString() {
            return latch.toString();
        }
    }
    

    【讨论】:

    • 您能否举例说明如何使用它。我的情况是我有一个列表,并且列表是实时动态更新的。列表中有自动事件,我必须等待 2 分钟,但如果在等待时间之间用户手册事件进入列表,我必须中断等待并立即继续。
    【解决方案4】:

    您可以使用允许访问其受保护的getWaitingThreads 方法的ReentrantLock 推出自己的CountDownLatch

    例子:

    public class FailableCountDownLatch {
        private static class ConditionReentrantLock extends ReentrantLock {
            private static final long serialVersionUID = 2974195457854549498L;
    
            @Override
            public Collection<Thread> getWaitingThreads(Condition c) {
                return super.getWaitingThreads(c);
            }
        }
    
        private final ConditionReentrantLock lock = new ConditionReentrantLock();
        private final Condition countIsZero = lock.newCondition();
        private long count;
    
        public FailableCountDownLatch(long count) {
            this.count = count;
        }
    
        public void await() throws InterruptedException {
            lock.lock();
            try {
                if (getCount() > 0) {
                    countIsZero.await();
                }
            } finally {
                lock.unlock();
            }
        }
    
        public boolean await(long time, TimeUnit unit) throws InterruptedException {
            lock.lock();
            try {
                if (getCount() > 0) {
                    return countIsZero.await(time, unit);
                }
            } finally {
                lock.unlock();
            }
            return true;
        }
    
        public long getCount() {
            lock.lock();
            try {
                return count;
            } finally {
                lock.unlock();
            }
        }
    
        public void countDown() {
            lock.lock();
            try {
                if (count > 0) {
                    count--;
    
                    if (count == 0) {
                        countIsZero.signalAll();
                    }
                }
            } finally {
                lock.unlock();
            }
        }
    
        public void abortCountDown() {
            lock.lock();
            try {
                for (Thread t : lock.getWaitingThreads(countIsZero)) {
                    t.interrupt();
                }
            } finally {
                lock.unlock();
            }
        }
    }
    

    您可能希望更改此类以在取消对 await 的新调用时抛出 InterruptedException。如果您需要该功能,您甚至可以让此类扩展 CountDownLatch

    【讨论】:

      【解决方案5】:

      从 Java 8 开始,您可以为此使用 CompletableFuture。一个或多个线程可以调用阻塞的 get() 方法:

      CompletableFuture<Void> cf = new CompletableFuture<>();
      try {
        cf.get();
      } catch (ExecutionException e) {
        //act on error
      }
      

      另一个线程可以使用cf.complete(null) 成功完成它,或者使用cf.completeExceptionally(new MyException()) 异常完成它

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2018-01-17
        • 2012-05-06
        • 1970-01-01
        • 1970-01-01
        • 2023-03-21
        • 1970-01-01
        • 2010-09-16
        相关资源
        最近更新 更多