Java-параллелизм: стратегия CopyOnWriteArrayList - PullRequest
0 голосов
/ 11 декабря 2018

Я пытаюсь понять 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 -> {... может быть достигнуто одновременно несколькими потоками: означает ли это, что один и тот же объект аудита можно дважды попытаться сохранить в базе данных?
  2. Каждый раз, когдаоперация модификации сделана на CopyOnWriteArrayList, новая копия заполняется в других потоках?Как это заселено?

1 Ответ

0 голосов
/ 23 декабря 2018

Каждый раз, когда вызывается remove, создается новая копия внутреннего массива, поддерживающего CopyOnWriteArrayList.Доступ к этому списку в будущем с помощью методов доступа и мутатора будет виден при обновлении.

Однако , метод метода CopyOnWriteArrayList#foreach выполняет итерацию в массиве, который был доступен во время его вызова,Это означает, что все выполнения метода flush, который вошел в foreach до , любое обновление в списке будет повторяться в устаревшей версии массива.

Таким образом, во время параллельноговыполнения метода 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() является потокобезопасным и атомарным.Один и тот же элемент никогда не может опрашиваться несколько раз (если он не добавлен в очередь более одного раза, cf [*]).Сложность линейна в n.

Следует учитывать два момента:

  • Ваша коллекция аудита не должна содержать null элементов, поскольку это запрещено для LinkedBlockingQueue,потому что null используется для указания того, что список пуст.
  • Вы должны позаботиться о том, чтобы не использовать методы блокировки, такие как take из poll(timeout, unit) для опроса очереди.

[*] Произошло изменение в методе flush, который new не выполняет копирование элементов Audit.Я не уверен, есть ли гарантия, что эти элементы различны, если метод flush вызывается параллельно.Если массив Audit одинаков для всех вызовов flush, очередь должна заполняться только один раз, перед любым вызовом flush.

...