java 并发:CopyOnWriteArrayList 策略

java concurrency: CopyOnWriteArrayList strategy

我正在尝试将 CopyOnWriteArrayList 理解为我的代码:

我的代码是:

public class AuditService {
    private CopyOnWriteArrayList<Audit> copyWrite;

    public void flush(Audit... audits) {
        Collection<Audit> auditCollection = Arrays.asList(audits);
        this.copyWrite.addAll(auditCollection);

        this.copyWrite.forEach(audit -> {
            try {
              // save audit object on database
              this.copyWrite.remove(audit);
            } catch (DataAccessException e) {
              // log it
            }

        });
    }
}

这段代码的作用是:

  1. 首先将审核存储到缓冲区中,CopyOnWriteArrayList
  2. 尝试将审核保存到数据库
  3. 存储后,将从缓冲区中删除 CopyOnWriteArrayList

其他:

  1. AuditService是单例class
  2. flush方法可以被多个线程访问。

问题:

  1. 我猜 this.copyWrite.forEach(audit -> {... 可以被多个线程同时访问:这是否意味着可以尝试将同一个审计对象保存到数据库中两次?
  2. 每次在CopyOnWriteArrayList上进行修改操作时,都会在其他线程上填充一个新副本?它是如何填充的?

每次调用 remove 时,都会生成支持 CopyOnWriteArrayList 的内部数组的新副本。将来使用访问器和修改器方法对该列表的访问将对更新可见。

但是,方法CopyOnWriteArrayList#foreach 方法迭代调用时可用的数组。这意味着在 列表上的任何更新之前进入 foreach 的方法 flush 的所有执行都将迭代数组的陈旧版本。

因此,在并行执行方法 flush 期间,相同的 Audit 元素将被持久化不止一次,如果 d 则最多 d 次是方法的最大并发执行数 flush.

在这种情况下使用 CopyOnWriteArrayList 的另一个问题是每次调用 remove 都会创建一个新副本,代码的复杂性是 d.n^2 其中 n 是列表的长度,d 是上面定义的。

CopyOnWriteArrayList 不是此处使用的正确实现。存在多种可能的合适设计。其中之一是使用 LinkedBlockingQueue 如下 [*]:

public void flush(Audit... audits) {
    Collection<Audit> auditCollection = Arrays.asList(audits);
    this.queue.addAll(auditCollection);

    Collection<Audit> poissonedAudits = new ArrayList<Audit>();
    Audit audit = null;
    while ((audit = this.queue.poll()) != null) {
       try {
          // save audit object on database
          queue.remove(audit);
        } catch (DataAccessException e) {
          // log it
          poissonedAudits.add(audit);
        }
    }
    this.queue.addAll(poissonedAudits);
}

LinkedBlockingQueue#poll() 的调用是线程安全的和原子的。同一个元素永远不会被多次轮询(只要它没有被多次添加到队列中,参见 [*])。复杂度在 n.

中是线性的

需要考虑两点:

  • 您的审核集合不得包含 null 元素,因为 LinkedBlockingQueue 禁止这样做,因为 null 用于指示列表为空。
  • 你应该注意不要使用takepoll(timeout, unit)等阻塞方法来轮询队列。

[*] flush 方法发生了变化,新方法不执行 Audit 元素的复制。如果并行调用 flush 方法,我不确定是否可以保证这些元素是不同的。如果 Audit 的数组对于 flush 的所有调用都是相同的,则在对 flush.

的任何调用之前,队列应该只填充一次