检查大小然后执行操作-ConcurrentLinkedDeque是否安全?

时间:2018-07-15 18:24:54

标签: java multithreading concurrency deque

我只需要用新值替换Deque中的第一个值 如果大小将超过限制。我写了这段代码来解决它:

final class Some {
    final int buffer;
    final Deque<Operation> operations = new ConcurrentLinkedDeque<>();
    // constructors ommited;

    @Override
    public void register(final Operation operation) {
        if (this.operations.size() == this.buffer) {
            // remove the oldest operation
            this.operations.removeFirst();
        }
        // add new operation to the tail
        this.operations.addLast(operation);
    }

    @Override
    public void apply() {
        // take the fresh operation from tail and perform it
        this.operations.removeLast().perform();
    }
}

如您所见,我有两种方法可以修改Deque。我对此代码是否可以在多线程环境中正常运行感到怀疑。问题是:检查size()然后执行可以修改ConcurrentLinkedDeque之后的操作是否安全?我希望锁最少。因此,如果此代码行不通,那么我必须引入锁定,然后使用ConcurrentLinkedDeque()就没有意义了。

final class Some {
    final int buffer;
    final Deque<Operation> operations = new LinkedList<>();
    final Lock lock = new ReentrantLock();
    // constructors ommited;

    @Override
    public void register(final Operation operation) {
        this.lock.lock();
        try {
            if (this.operations.size() == this.buffer) {
                // remove the oldest operation
                this.operations.removeFirst();
            }
            // add new operation to the tail
            this.operations.addLast(operation);
        } finally {
            lock.unlock();
        }
    }

    @Override
    public void apply() {
        this.lock.lock();
        try {
            // take the fresh operation from tail and perform it
            this.operations.removeLast().perform();
        } finally {
            this.lock.unlock();
        }
    }
}

这是Lock的替代方法。那是实现我想要的唯一途径吗?我对尝试使用并发集合特别感兴趣。

2 个答案:

答案 0 :(得分:4)

并发集合在进入内部状态时是线程安全的。换句话说,他们

  • 允许多个线程同时读取/写入,而不必担心内部状态将被破坏
  • 在其他线程修改集合时允许迭代和删除
    • 但是,并非全部。我相信CopyOnWriteArrayList的{​​{1}}不支持Iterator操作
  • 保证诸如 happens-before 之类的东西
    • 通过一个线程进行写操作将先于由后续线程进行读操作

但是,它们不是跨线程安全的 跨线程的 外部方法调用。当您调用一个方法时,它将获取所需的任何锁,但这些锁将在该方法返回时释放。如果您不小心,可能会导致 check-then-act 竞争状况。查看您的代码

remove()

可能会发生以下情况:

  1. if (this.operations.size() == this.buffer) { this.operations.removeFirst(); } this.operations.addLast(operation); 检查大小条件,结果为Thread-A
  2. false开始添加新的Thread-A
  3. Operation可以添加Thread-A之前,Operation检查大小条件,这也会导致Thread-B
  4. false去添加新的Thread-B
  5. Operation 确实添加新的Thread-A
    • 哦,不! Operation添加的Operation导致达到大小阈值
  6. Thread-A,已经过Thread-B语句,添加了if,使得双端队列有太多Operation s

这就是为什么 check-then-act 需要外部同步的原因,您在第二个示例中使用Operation进行了同步。请注意,您也可以在Lock上使用synchronized块。

与您的问题无关:在第二个示例中,您在按住Deque的同时调用Operation.perform()。这意味着在执行Lock时,没有其他线程可以尝试向Operation中添加另一个Deque。如果不希望这样,您可以像这样更改代码:

perform()

答案 1 :(得分:1)

来自size()的文档

  

Blockquote请注意,与大多数集合不同,此方法不是恒定时间操作。由于这些双端队列的异步性质,确定当前元素数量需要遍历所有元素以进行计数。此外,在执行此方法期间大小可能会更改,在这种情况下,返回的结果将不准确。因此,该方法在并发应用程序中通常不是很有用。

虽然@Slaw是正确的,但还要补充一点,在遍历期间可以进行加/减。

我不在软件中使用size()。我使用AtomicInteger自己计算集合中的内容。如果count.get()