Подтвердить что ты не робот

Нужно ли нам синхронизировать доступ к массиву, когда переменная массива является изменчивой?

У меня есть класс, содержащий изменчивую ссылку на массив:

private volatile Object[] objects = new Object[100];

Теперь я могу гарантировать, что в массив может записываться только один поток (назовем его writer). Например,

objects[10] = new Object();

Все остальные потоки будут считывать только значения, записанные потоком writer.

Вопрос: Нужно ли мне синхронизировать такие чтения и записи, чтобы обеспечить согласованность памяти?

Я полагаю, да, я должен. Потому что это не было бы полезно с точки зрения производительности, если JVM обеспечивает некоторую гарантию целостности памяти при записи в массив. Но я не уверен в этом. Не нашел ничего полезного в документации.

4b9b3361

Ответ 1

private volatile Object[] objects = new Object[100];

Вы делаете только objects ссылкой volatile таким образом. Не связано с содержимым экземпляра массива.

Вопрос: Нужно ли мне синхронизировать такие чтения и записи, чтобы обеспечить согласованность памяти?

Да.

было бы не полезно с точки зрения производительности, если JVM обеспечивает некоторую гарантию целостности памяти при записи в массив

рассмотрим использование таких коллекций, как CopyOnWriteArrayList (или ваша собственная оболочка массива с некоторой реализацией Lock внутри мутаторов и методов чтения).

Java-платформа также имеет Vector (устаревшая с неправильным дизайном) и synchronized List (медленная для многих сценариев), но я не рекомендую их использовать.

PS: Еще одна хорошая идея от @SashaSalauyou

Ответ 2

Вы можете использовать AtomicReferenceArray:

final AtomicReferenceArray<Object> objects = new AtomicReferenceArray<>(100);

// writer
objects.set(10, new Object());

// reader
Object obj = objects.get(10);

Это обеспечит атомные обновления и произойдет - до согласованности операций чтения/записи, так же, как если бы каждый элемент массива был volatile.

Ответ 3

Да, вам нужно синхронизировать обращения к элементам энергозависимого массива.

Другие люди уже рассмотрели, как вы, вероятно, можете использовать CopyOnWriteArrayList или AtomicReferenceArray вместо этого, поэтому я собираюсь отклониться в несколько другом направлении. Я бы также рекомендовал прочитать Volatile Arrays в Java одним из крупных участников JMM Джереми Мэнсоном.

Теперь я могу гарантировать, что единственный поток (назовем его автором) может записывать в массив, например. следующим образом:

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

Все остальные потоки будут считывать только значения, записанные потоком записи.

Да, но, как ваша интуиция правильно ведет вас, это относится только к значению ссылки на массив. Это означает, что, если вы не пишете ссылки на массив с переменной volatile, вы не получите часть записи контракта volatile для записи-записи.

Это означает, что либо вы хотите сделать что-то вроде

objects[i] = newObj;
objects = objects;

который является уродливым и ужасным по-разному. Или вы хотите публиковать новый массив каждый раз, когда ваш писатель делает обновление, например.

Object[] newObjects = new Object[100];

// populate values in newObjects, make sure that newObjects IS NOT published yet

// publish newObjects through the volatile variable
objects = newObjects;

который не является очень распространенным прецедентом.

Обратите внимание, что в отличие от настроек элементов массива, которые не предоставляют семантику volatile -write, получение элементов массива (с помощью newObj = objects[i];) действительно обеспечивает семантику volatile -read, потому что вы разыгрываете массив:)

Потому что с точки зрения производительности было бы не полезно, если JVM обеспечивает некоторую гарантию согласованности памяти при записи в массив. Но я не уверен в этом.

Как вы говорите, обеспечение фехтования памяти, необходимого для семантики volatile, будет очень дорогостоящим, и если вы добавите ложный обмен в микс, это станет еще хуже.

Не нашел ничего полезного в документации.

Можно с уверенностью предположить, что семантика volatile для ссылок на массивы точно такая же, как семантика volatile для ссылок без массива, что совсем не удивительно, учитывая, как массивы (даже примитивные) все еще остаются объекты.

Ответ 4

Per JLS § 17.4.5 - Выполняется до заказа:

Два действия могут быть упорядочены с помощью отношения "происходить-до". Если одно действие происходит - перед другим, то первое видно и упорядочивается до второго.

[...]

Записывается в поле volatile - перед каждым последующим чтением этого поля.

Происходит-до отношения довольно сильно. Это означает, что если поток A записывает в переменную volatile, и любой поток B позже считывает переменную, то поток B, как ожидается, увидит изменение самой переменной volatile, а также любой другой поток изменений A, сделанный до устанавливая переменную volatile, включая любые другие объекты, независимо от того, были ли они иначе volatile.

Однако этого недостаточно!

Назначение элемента objects[10] = new Object(); не является записью переменной objects. Это только чтение переменной, чтобы определить массив, на который он указывает, за которым следует запись в другую переменную, содержащуюся внутри объекта массива, расположенного где-то еще в памяти. Не происходит - до того, как отношение устанавливается простым чтением в переменные volatile, поэтому код небезопасен.

Как указывает @DimitarDimitrov, вы можете клониться вокруг этого, выполняя фиктивную запись в переменную objects. Каждая пара операций - переназначение objects = objects; по потоку записи, связанное с поиском foo = objects[x]; потоком читателя - определяет обновленную связь "бывшее-до" и, таким образом, "публикует" все последние изменения, сделанные потоком записи к нити читателя. Это может работать, но это требует дисциплины, и это не изящно.

Но есть более тонкая проблема: даже если поток читателя видит обновленное значение элемента массива, которое все еще не гарантирует, что он правильно видит поля объекта, на который ссылается этот элемент, потому что следующее заказ возможен:

  • Writer создает некоторый объект foo.
  • Создатели Writer objects[x] = foo;
  • Reader проверяет objects[x] и видит ссылку на новый объект foo (который он может делать, хотя это не гарантируется, так как не происходит - до отношения еще).
  • Writer делает objects = objects;

К сожалению, это не определяет формальное происхождение-до отношения, потому что переменная volatile variable (3) появилась перед изменением переменной volatile (4). Хотя читатель может видеть, что objects[x] является объектом foo случайно, это не означает, что поля foo безопасно опубликованы, поэтому читатель может теоретически увидеть новый объект, но с неправильными значениями! Чтобы решить эту проблему, объекты, которыми вы делитесь между потоками, используя эту технику, должны иметь все поля final или volatile или иначе синхронизироваться. Если все объекты String, например, все будет в порядке, но в противном случае слишком легко совершить ошибки с этим. (Спасибо @Holger за указание этого.)


Вот несколько менее хрупких альтернатив:

  • Существуют параллельные классы массивов, такие как AtomicReferenceArray, чтобы обеспечить массивы, в которых каждый элемент ведет себя как volatile. Это намного проще в использовании, потому что он гарантирует, что если читатель увидит обновленное значение элемента массива, он также правильно увидит объект, на который ссылается этот элемент.

  • Вы можете обернуть все обращения к массиву в блоках synchronized, синхронизируя на каком-то общем объекте:

    // writer
    synchronized (aSharedObject) {
        objects[x] = foo;
    }
    
    // reader
    synchronized (aSharedObject) {
        bar = objects[x];
    }
    

    Как и volatile, использование synchronized создает связь между событиями. (Все, что делает поток, прежде чем освободить блокировку синхронизации объекта, - до того, как какой-либо другой поток получит блокировку синхронизации одного и того же объекта.) Если вы сделаете это, ваш массив не должен быть volatile.

  • Подумайте, нужен ли массив именно вам. Вы не сказали, для чего предназначены эти записи для писателя и читателя, но если вы хотите какую-то очередь производителей-потребителей, тогда вам действительно нужен класс BlockingQueue или Executor. Вы должны осмотреть классы Java concurrency, чтобы убедиться, что один из них уже делает то, что вам нужно, потому что, если это произойдет, это будет, безусловно, проще использовать правильно, чем volatile.