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
}
});
}
}
这段代码的作用是:
- 首先将审核存储到缓冲区中,
CopyOnWriteArrayList
。
- 尝试将审核保存到数据库
- 存储后,将从缓冲区中删除
CopyOnWriteArrayList
。
其他:
AuditService
是单例class
flush
方法可以被多个线程访问。
问题:
- 我猜
this.copyWrite.forEach(audit -> {...
可以被多个线程同时访问:这是否意味着可以尝试将同一个审计对象保存到数据库中两次?
- 每次在
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
用于指示列表为空。
- 你应该注意不要使用
take
或poll(timeout, unit)
等阻塞方法来轮询队列。
[*] flush
方法发生了变化,新方法不执行 Audit
元素的复制。如果并行调用 flush
方法,我不确定是否可以保证这些元素是不同的。如果 Audit
的数组对于 flush
的所有调用都是相同的,则在对 flush
.
的任何调用之前,队列应该只填充一次
我正在尝试将 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
}
});
}
}
这段代码的作用是:
- 首先将审核存储到缓冲区中,
CopyOnWriteArrayList
。 - 尝试将审核保存到数据库
- 存储后,将从缓冲区中删除
CopyOnWriteArrayList
。
其他:
AuditService
是单例classflush
方法可以被多个线程访问。
问题:
- 我猜
this.copyWrite.forEach(audit -> {...
可以被多个线程同时访问:这是否意味着可以尝试将同一个审计对象保存到数据库中两次? - 每次在
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
用于指示列表为空。 - 你应该注意不要使用
take
或poll(timeout, unit)
等阻塞方法来轮询队列。
[*] flush
方法发生了变化,新方法不执行 Audit
元素的复制。如果并行调用 flush
方法,我不确定是否可以保证这些元素是不同的。如果 Audit
的数组对于 flush
的所有调用都是相同的,则在对 flush
.