Почему вызов метода setArray() требуется в CopyOnWriteArrayList

В CopyOnWriteArrayList.java, в наборе методов (int index, E element) ниже

public E set(int index, E element) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        Object oldValue = elements[index];

        if (oldValue != element) {
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len);
            newElements[index] = element;
            setArray(newElements);
        } else {
            // Not quite a no-op; ensures volatile write semantics
            setArray(elements);----? Why this call required?
        }
        return (E)oldValue;
    } finally {
        lock.unlock();
    }
}

Почему требуется вызов setArray()? Я не мог понять комментарий, написанный выше этого вызова метода. Это потому, что мы не используем синхронизированный блок, мы должны вручную очистить всю переменную, которую мы используем? В вышеуказанном методе они используют блокировки повторного входа. Если они использовали синхронизированный оператор, им все равно нужно вызвать метод setArray()?. Я думаю нет.

Question2: Если мы закончим иначе, это означает, что мы не модифицировали массив элементов, то почему нам нужно сбросить значение массива переменных?

Ответ 1

В этом коде используется глубокая модель памяти Java Memory voodoo, поскольку она смешивает блокировки и летучие элементы.

Использование блокировки в этом коде легко обойтись. Блокировка обеспечивает упорядочение памяти среди потоков, которые используют один и тот же замок. В частности, разблокировка в конце этого метода обеспечивает выполнение - перед семантикой с другими потоками, которые получают одну и ту же блокировку. Другие пути кода через этот класс, однако, не используют эту блокировку вообще. Следовательно, последствия для модели памяти для блокировки не имеют отношения к этим путям кода.

Эти другие коды кода используют изменчивые чтения и записи, особенно в поле array. Метод getArray выполняет волатильное чтение этого поля, а метод метода setArray выполняет волатильную запись этого поля.

Причина, по которой этот код вызывает setArray, даже когда он, по-видимому, не нужен, так что он устанавливает инвариант для этого метода, что он всегда выполняет волатильную запись в этот массив. Это устанавливается - перед семантикой с другими потоками, которые выполняют изменчивые чтения из этого массива. Это важно, потому что волатильная семантика чтения с чтением применяется к чтению и записи, отличным от данных самого изменчивого поля. В частности, записывается в другие (энергонезависимые) поля до того, как произойдет летучая запись - перед тем, как читать из этих других полей после волатильного чтения той же изменчивой переменной. См. JMM FAQ для объяснения.

Вот пример:

// initial conditions
int nonVolatileField = 0;
CopyOnWriteArrayList<String> list = /* a single String */

// Thread 1
nonVolatileField = 1;                 // (1)
list.set(0, "x");                     // (2)

// Thread 2
String s = list.get(0);               // (3)
if (s == "x") {
    int localVar = nonVolatileField;  // (4)
}

Предположим, что строка (3) получает значение, заданное строкой (2), интернированной строкой "x". (Для этого примера мы используем семантику интернированных интернированных строк.) Предполагая, что это так, модель памяти гарантирует, что значение, считанное в строке (4), будет равно 1, как задано линией (1). Это связано с тем, что волатильная запись в (2) и каждая более ранняя запись происходят до того, как волатильное чтение в строке (3) и каждое последующее чтение.

Теперь предположим, что начальное условие состояло в том, что в списке уже содержится один элемент, интернированная строка "x". И далее предположим, что предложение set() else не вызвало вызов setArray. Теперь, в зависимости от исходного содержимого списка, вызов list.set() в строке (2) может выполнять или не выполнять неустойчивую запись, поэтому чтение в строке (4) может иметь или не иметь никаких гарантий видимости!

Очевидно, что вы не хотите, чтобы эти возможности видимости памяти зависели от текущего содержимого списка. Чтобы установить гарантию во всех случаях, set() нужно выполнять волатильную запись во всех случаях, и поэтому она вызывает setArray(), даже если она сама не писала.

Ответ 2

TL;DR; Вызов setArray необходим для обеспечения гарантии, указанной в Javadoc CopyOnWriteArrayList (даже если содержимое списка не изменяется)


CopyOnWriteArrayList имеет гарантию целостности памяти, указанную в Javadoc:

Эффекты согласованности памяти: как и в других параллельных коллекциях, действий в потоке до помещения объекта в CopyOnWriteArrayListпроизойти - перед действиями после доступа или удаления этого элемента из CopyOnWriteArrayList в другом потоке.

Для обеспечения этой гарантии необходим вызов setArray.

Поскольку спецификация Java Memory Model в JLS гласит:

Записывается в неустойчивое поле (§8.3.1.4) - перед каждым последующим прочитайте это поле.

Таким образом, запись в метод array (с использованием метода setArray) необходима для обеспечения того, чтобы другие потоки, просматриваемые из списка, теперь имеют отношение "раньше" (или, скорее, произойдет-после) с потоком, который вызвал set, даже если элемент в методе set уже был идентичен (используя ==) с элементом, который уже был в списке в этой позиции.

Обновленное объяснение

Возвращение к гарантии в Javadoc. Этот порядок вещей (при условии доступа, а не удаления, как последнее действие - удаление уже позаботится об использовании lock, но доступ не использует lock):

  • Действие в потоке A перед размещением объекта в CopyOnWriteArrayList
  • Размещение и объект в CopyOnWriteArrayList (предположительно в потоке A, хотя Javadoc может быть более ясным об этом)
  • Доступ к [чтению] элемента из CopyOnWriteArrayList в потоке B

Предполагая, что шаг 2 помещает элемент в список, который уже был там, мы видим, что код переходит в эту ветку:

} else {
    // Not quite a no-op; ensures volatile write semantics
    setArray(elements);
}

Этот вызов setArray обеспечивает волатильное поле записи array из потока A. Так как поток B будет выполнять поле volatile read on array, между потоком A и потоком B создается связь между событиями и нитями, 't был создан, если else-branch не существует.

Ответ 3

Я считаю, что это связано с тем, что другие методы, которые читают массив, не получают блокировку, поэтому перед заказом нет никакой гарантии. Способ сохранения такого заказа - это обновление изменчивого поля, которое гарантирует такой порядок. (Это семантика записи, на которую она ссылается)

Ответ 4

Не требуется AFAICS. Это происходит по двум причинам.

  • написать семантику нужно только, если вы выполняете запись, это не так.
  • lock.unlock() выполняет семантику записи в блоке finally, неизбежно.

Метод

lock.unlock()

всегда обращается к

private volatile int state;

protected final void setState(int newState) {
    state = newState;
}

и это приводит к тому, что до семантики происходит как setArray(), что делает избыточный набор set. Вы можете утверждать, что не хотите зависеть от реализации ReentrantLock, но если вы обеспокоены тем, что будущая версия ReentrantLock не является потокобезопасной, у вас могут возникнуть большие проблемы, если это так.

Ответ 5

В JDK 11 эта бесполезная операция уже удалена из исходного кода. см. код ниже.

//code from JDK 11.0.1
public E set(int index, E element) {
    synchronized (lock) {
        Object[] es = getArray();
        E oldValue = elementAt(es, index);

        if (oldValue != element) {
            es = es.clone();
            es[index] = element;
            setArray(es);
        }
        return oldValue;
    }
}