【问题标题】:Check size and then perform operation - is it safe for ConcurrentLinkedDeque?检查大小然后执行操作 - ConcurrentLinkedDeque 是否安全?
【发布时间】:2018-12-23 08:42:29
【问题描述】:

我需要将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 的替代方案。这是实现我想要的唯一方法吗?我对尝试使用并发集合特别感兴趣。

【问题讨论】:

  • 不,非Lock 示例在多线程环境中不安全。没有外部同步,检查然后执行不是原子的。此外,除非您在运行时不允许添加Operations,否则我不会在仍持有Lock 的同时调用perform()。
  • 此外,ConcurrentLinkedDeque#size() 需要全面扫描,并且可能因并发添加/删除而有所不同。使用ArrayDeque 和您的锁定代码可能会是最好的选择。

标签: java multithreading concurrency deque


【解决方案1】:

当涉及到内部状态时,并发集合是线程安全的。换句话说,他们

  • 允许多个线程同时读取/写入,而不必担心内部状态会损坏
  • 在其他线程修改集合时允许迭代和删除
    • 但并非全部。我相信CopyOnWriteArrayList的Iterator不支持remove()操作
  • 保证诸如 happens-before 之类的事情
    • 意味着一个线程的写入将发生在后续线程的读取

但是,它们不是线程安全的跨外部方法调用。当您调用一种方法时,它将获取任何必要的锁,但这些锁在方法返回时被释放。如果您不小心,这可能会导致 check-then-act 竞态条件。查看您的代码

if (this.operations.size() == this.buffer) {
    this.operations.removeFirst();
}
this.operations.addLast(operation);

可能会发生以下情况:

  1. Thread-A 检查大小条件,结果是 false
  2. Thread-A 移动以添加新的Operation
  3. 在Thread-A 之前可以添加Operation,Thread-B 检查大小条件,这也会导致false
  4. Thread-B 去添加新的Operation
  5. Thread-A 确实添加新的Operation
    • 哦,不! Thread-A添加的Operation导致达到大小阈值
  6. Thread-B,已经超过 if 语句,添加它的 Operation 使双端队列有太多 Operations

这就是check-then-act 需要外部同步的原因,您在第二个示例中使用Lock 执行此操作。请注意,您还可以在 Deque 上使用 synchronized 块。

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

Operation op;

lock.lock();
try {
    op = deque.pollLast(); // poll won't throw exception if there is no element
} finally {
    lock.unlock();
}

if (op != null) {
    op.perform();
}

【讨论】:

    【解决方案2】:

    来自 size() 的文档

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

    虽然@Slaw 是正确的,但还要补充一点,在遍历过程中可能会发生加法/减法。

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

    【讨论】:

      猜你喜欢
      • 2010-11-10
      • 2012-10-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-11-13
      相关资源
      最近更新 更多