这是一个合适的定制同步器吗?

时间:2014-08-25 14:47:30

标签: java multithreading

我非常需要类似于CountDownLatch的同步器,但倒计时的起始编号是未知的。要添加上下文,如果我正在浏览缓冲的记录集(例如来自文本文件或查询)并为每条记录启动runnable,但我不知道会有多少条记录。我需要一个同步器,当迭代完成并且所有可运行的完成时发出信号。

这是我想出的同步器......一个BufferedLatch。在迭代循环中为每个递增recordSetSize的记录调用一个方法。每个runnable结束时为每个记录启动,processedRecordSetSize递增。当完成所有记录的迭代(但是runnables可能仍在队列中)时,调用setDownloadComplete()方法,让BufferedLatch知道现在修复了recordSetSize。 await()方法等待iterationComplete变量为true(recordsetSize现在已修复)和recordsetSize == processedRecordSetSize;

这是同步器的最佳实现吗?是否存在同步阻碍的并发机会?虽然测试似乎工作得很好,但是我有什么可以忽略的吗?

import java.util.concurrent.atomic.AtomicInteger;

public final class BufferedLatch {
/** A customized synchronizer built for concurrent iteration processes where the number of objects to be iterated is unknown
 *  and a runnable will be kicked off for each object, and the await() method will wait for all runnables to be complete
 */
    private final AtomicInteger recordsetSize = new AtomicInteger(0);
    private final AtomicInteger processedRecordsetSize = new AtomicInteger(0);
    private volatile boolean iterationComplete = false;

    public int incrementRecordsetSize() throws Exception { 
        if (iterationComplete) {
            throw new Exception("Cannot increase recordsize after download is flagged complete!");
        }
        else {
            return recordsetSize.incrementAndGet();
        }

    }
    public void incrementProcessedRecordSize() { 
        synchronized(this) { 
            processedRecordsetSize.incrementAndGet();
            if (iterationComplete) {
                 if (processedRecordsetSize.get() == recordsetSize.get()) { 
                     this.notifyAll();
                 }
            }
        }
    }
    public void setDownloadComplete() { 
        synchronized(this) { 
            iterationComplete = true;
        }
    }

    public void await() throws InterruptedException { 
        while (! (iterationComplete && (processedRecordsetSize.get() == recordsetSize.get()))) {
            synchronized(this) { 
                while (! (iterationComplete && (processedRecordsetSize.get() == recordsetSize.get()))) {
                    this.wait();
                }
            }
        }
    }

}

更新 - 新代码

public final class BufferedLatch {
/** A customized synchronizer built for concurrent iteration processes where the number of objects to be iterated is unknown
 *  and a runnable will be kicked off for each object, and the await() method will wait for all runnables to be complete
 */
private int recordCount = 0;
private int processedRecordCount = 0;
private boolean iterationComplete = false;

public synchronized void incrementRecordCount() throws Exception { 
    if (iterationComplete) {
        throw new Exception("Cannot increase recordCount after download is flagged complete!");
    }
    else {
        recordCount++;
    }
}
public synchronized void incrementProcessedRecordCount() { 
    processedRecordCount++;
    if (iterationComplete && recordCount == processedRecordCount) { 
        this.notifyAll();
    }
}
public synchronized void setIterationComplete() { 
    iterationComplete = true;
    if (iterationComplete && recordCount == processedRecordCount) { 
        this.notifyAll();
    }
}

public synchronized void await() throws InterruptedException { 
    while (! (iterationComplete && (recordCount == processedRecordCount))) {
        this.wait();
    }
}

}

1 个答案:

答案 0 :(得分:2)

可能不是。从概念上讲,我认为你在这里做了些什么,因为看起来你的应用程序需要的东西不仅仅是CountDownLatch。但是,实施似乎有几个问题。

首先,我注意到混合atomics / volatiles和普通对象监视器锁(synchronized)看起来很奇怪。虽然可能有适当的用途混合这些不同的结构,但在这种情况下混合我认为会导致错误。

考虑首先检查incrementRecordsetSize()的{​​{1}},并且只有在它为{0}}时才会增加iterationCompleterecordsetSize变量是易失性的,因此可以看到来自其他线程的更新。但是,这里没有锁定的事实允许TOCTOU竞争条件(检查时间与使用时间)。规则似乎是,如果iterationComplete为真,则recordsetSize不得递增。假设线程T1出现并且发现iterationComplete为假,因此它决定增加iterationComplete。在此之前,另一个线程T2出现并将recordsetSize设置为true。这将允许T1不正确地进行增量。更糟糕的是,在它出现之前,假设另一个线程T3出现并调用iterationComplete。它将增加incrementProcessedRecordSize(),然后找到processedRecordsetSize为真。它还可能发现iterationComplete等于processedRecordsetSize,然后通知所有服务员,然后服务员就像处理完成一样继续进行。但事实并非如此,因为T1会继续增加recordsetSize,并且可能继续进行处理。

这里的问题是这个对象的状态由三个独立的状态块组成 - 两个int计数器和一个布尔值 - 所有这三个都必须以原子方式读写。如果某些逻辑位试图利用单个的易失性或原子属性,它会引入竞争条件的可能性,例如我所描述的那种。

我建议将其重写为具有两个普通整数和一个布尔值(不是原子,不易变)的普通对象,只是锁定所有内容。这当然应该清理逻辑并使事情更容易理解。

recordsetSize我注意到该条件基本上复制了incrementProcessedRecordSize方法中的条件。简化约定是所有更新都要通知,并且只有服务员评估条件。这可能会导致一些不必要的唤醒。如果这是一个问题,您可以考虑尽量减少通知的数量,但您需要考虑可维护性。如果您不小心,等待/通知条件将在代码中传播,并且很难推理。或者,您可以将条件重构为方法,并从等待和通知的不同位置调用它。

看起来await执行复杂形式的双重检查锁定。它不是在锁外部测试一个易失性布尔值,而是在锁外部和内部测试几个单独的信息。这似乎很容易受到TOCTOU问题的影响(如上所述),但如果你能证明状态确实是锁存的话,可能是安全的,也就是说,一旦它变为真,它永远不会返回到假。在我能够说服自己这是正确的之前,我必须长时间盯着代码。

另一方面,这会给你带来什么?它似乎只是优化了锁定。如果你在处理完成后有很多线程可能会有所值,但它看起来并不像。我只是删除外部while循环并检查await()块中的变量。

最后,拥有一个代表计数器和布尔值的对象可能对你正在做的事情是明智的,但你所说的其他事情(在问题和评论中)是有些线程是生成工作负载(例如,从文件读取行)和其他线程正在退休该工作负载。这意味着有一些其他数据结构,如包含此工作负载的队列,并且您在此处存在生产者 - 消费者问题。当然,其他结构必须是线程安全的,因为多个线程在它上面进行交互。但是 this 结构中的计数器和布尔值需要与工作负载结构的更新保持同步更新,否则在检查和更新这些单独的对象之间可能存在竞争条件。

在我看来,你可以用队列替换这个对象中的计数器,只需在所有东西周围放置简单的锁。生产者会在他们完成之前附加到队列中,此时他们将synchronized设置为true,这会阻止添加更多工作。消费者从队列中拉出,直到iterationComplete为真并且队列为空,此时他们已完成。如果他们发现队列为空但iterationComplete为假,则他们知道在等待进一步工作时会阻塞。

我说要坚持简单的锁定并避免使用挥发物/原子,直到你得到正确的基础知识。如果该代码存在瓶颈,则选择性地应用优化,同时保留相同的不变量。