【问题标题】:java concurrency: CopyOnWriteArrayList strategyjava并发:CopyOnWriteArrayList策略
【发布时间】:2018-12-11 12:13:23
【问题描述】:

我正在尝试将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 是一个单例类
  2. flush 方法可以被多个线程访问。

问题:

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

【问题讨论】:

    标签: java multithreading java.util.concurrent


    【解决方案1】:

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

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

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

    在这种情况下使用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 方法发生了变化,new 不执行 Audit 元素的副本。如果并行调用 flush 方法,我不确定是否可以保证这些元素是不同的。如果Audit 的数组对于flush 的所有调用都相同,则在对flush 的任何调用之前,应该只填充一次队列。

    【讨论】:

    • 我编辑了你的答案。请你看一下提交它吗?
    • 我刚刚意识到您删除了Audits... audits 参数。我也读过[*],但我不太明白你的意思。我已经根据我的建议编辑了您的帖子。请不要犹豫,提出您的想法。
    猜你喜欢
    • 2020-12-14
    • 2011-01-05
    • 1970-01-01
    • 2014-09-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-09-08
    • 2011-09-11
    相关资源
    最近更新 更多