Могу ли я в Java полагаться на то, что присвоение ссылки является атомарным, для реализации копирования при записи?

Если у меня есть несинхронизированная коллекция java в многопоточной среде, и я не хочу заставлять читателей коллекции синхронизировать [1], это решение, в котором я синхронизирую авторов и использую атомарность справочное присвоение возможно? Что-то типа:

private Collection global = new HashSet(); // start threading after this

void allUpdatesGoThroughHere(Object exampleOperand) {
  // My hypothesis is that this prevents operations in the block being re-ordered
  synchronized(global) {
    Collection copy = new HashSet(global);
    copy.remove(exampleOperand);
    // Given my hypothesis, we should have a fully constructed object here. So a 
    // reader will either get the old or the new Collection, but never an 
    // inconsistent one.
    global = copy;    
  }
}

// Do multithreaded reads here. All reads are done through a reference copy like:
// Collection copy = global;
// for (Object elm: copy) {...
// so the global reference being updated half way through should have no impact 

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


[1] Причина в том, что большая часть времени тратится на чтение по сравнению с записью, в сочетании с риском возникновения тупиковых ситуаций.


Изменить: много хорошей информации в нескольких ответах и ​​комментариях, некоторые важные моменты:

  1. В опубликованном мной коде была ошибка. Синхронизация с глобальной (плохо названной переменной) может не защитить синхронизированный блок после подкачки.
  2. Вы можете исправить это, синхронизируя класс (переместив ключевое слово synchronized в метод), но могут быть и другие ошибки. Более безопасное и удобное в обслуживании решение - использовать что-нибудь из java.util.concurrent.
  3. В опубликованном мною коде нет «гарантии возможной согласованности». Один из способов убедиться, что читатели действительно видят обновления, сделанные авторами, - это использовать ключевое слово volatile.
  4. Поразмыслив, общая проблема, которая мотивировала этот вопрос, заключалась в попытке реализовать чтение без блокировки с заблокированной записью в java, однако моя (решенная) проблема была с коллекцией, которая может излишне сбивать с толку будущих читателей. Так что, если не очевидно, что опубликованный мною код работает, позволяя одному писателю за раз вносить изменения в «некоторый объект», который читается без защиты несколькими потоками чтения. Фиксация редактирования осуществляется посредством атомарной операции, поэтому читатели могут получить только «объект» до или после редактирования. Когда / если поток чтения получает обновление, это не может произойти в середине чтения, поскольку чтение происходит на старой копии «объекта». Простое решение, которое, вероятно, было обнаружено и доказано, что оно каким-то образом сломалось до того, как появилась улучшенная поддержка параллелизма в java.

person MilesHampson    schedule 15.08.2012    source источник
comment
Это похоже на коллекции в java .util.concurrent подойдет.   -  person DaoWen    schedule 15.08.2012
comment
Я посмотрел на эти структуры данных, но был обеспокоен производительностью их получения и итератора по сравнению с моим решением выше. Возможно, мне стоит проверить это.   -  person MilesHampson    schedule 15.08.2012
comment
и интересно видеть, что они также могут иногда ужасно ошибаться stackoverflow.com/questions/3292577/. Не то чтобы я говорю, что вы не должны его использовать, просто иногда вам нужно сделать домашнее задание и понять, как выполняется ваш код.   -  person MilesHampson    schedule 16.08.2012
comment
Майлз Хэмпсон - Ситуация, описанная в этом вопросе, будет происходить в основном независимо от того, какие типы конструкций параллелизма они использовали. Внезапное завершение потока без предоставления ему возможности очиститься после себя (т. Е. Снять блокировки, которые он удерживает) будет иметь катастрофические последствия в любой ситуации. Я думаю, что тот факт, что пакет, участвующий в уничтожении потоков, называется Unsafe, должен быть достаточно большим красным флагом, чтобы знать, что при его использовании могут произойти неприятности!   -  person DaoWen    schedule 16.08.2012


Ответы (5)


Вместо того, чтобы пытаться развернуть собственное решение, почему бы не использовать ConcurrentHashMap в качестве вашего набора и просто установить все значения на какое-то стандартное значение? (Подойдет константа типа Boolean.TRUE.)

Я думаю, что эта реализация хорошо работает со сценарием «много читателей - мало писателей». Есть даже конструктор, позволяющий установить ожидаемый" уровень параллелизма ".

Обновление. Вир предложил использовать Collections.newSetFromMap, чтобы превратить ConcurrentHashMap в Set. Поскольку метод принимает Map<E,Boolean>, я предполагаю, что он делает то же самое с установкой всех значений Boolean.TRUE за кулисами.


Обновление: обращение к примеру с плакатом

Вероятно, именно этим я и займусь, но мне все еще любопытно, как мое минималистское решение может потерпеть неудачу. - Майлз Хэмпсон

Ваше минималистичное решение отлично подойдет, если немного подправить его. Меня беспокоит, что, хотя сейчас он минимален, в будущем он может стать более сложным. Трудно вспомнить все условия, которые вы принимаете при создании чего-то поточно-ориентированного, особенно если вы возвращаетесь к коду через несколько недель / месяцев / лет, чтобы внести, казалось бы, незначительную настройку. Если ConcurrentHashMap делает все, что вам нужно, с достаточной производительностью, почему бы вместо этого не использовать его? Все неприятные детали параллелизма скрыты, и даже через 6 месяцев вам будет сложно все испортить!

Вам понадобится хотя бы одна настройка, прежде чем ваше текущее решение заработает. Как уже указывалось, вам, вероятно, следует добавить модификатор volatile к объявлению global. Не знаю, есть ли у вас опыт работы с C / C ++, но я был очень удивлен, когда узнал, что семантика volatile в Java на самом деле намного сложнее, чем в C < / а>. Если вы планируете много заниматься параллельным программированием на Java, было бы неплохо ознакомиться с основами модель памяти Java. Если вы не сделаете ссылку на global ссылкой на volatile, тогда возможно, что ни один поток никогда не увидит никаких изменений значения global, пока они не попытаются обновить его, после чего ввод блока synchronized очистит локальный кеш и получит обновленное эталонное значение.

Однако даже после добавления volatile остается огромная проблема. Вот сценарий проблемы с двумя потоками:

  1. Начнем с пустого набора, или global={}. Потоки A и B имеют это значение в своей локальной кэшированной памяти потока.
  2. Поток A получает блокировку synchronized на global и начинает обновление, создавая копию global и добавляя новый ключ в набор.
  3. Пока поток A все еще находится внутри блока synchronized, поток B считывает свое локальное значение global в стек и пытается войти в блок synchronized. Поскольку поток A в настоящее время находится внутри блоков монитора, поток B.
  4. Поток A завершает обновление, устанавливая ссылку и выходя из монитора, в результате чего получается global={1}.
  5. Поток B теперь может войти в монитор и сделать копию набора global={1}.
  6. Поток A решает сделать другое обновление, читает свою локальную global ссылку и пытается войти в блок synchronized. Поскольку поток B в настоящее время удерживает блокировку {}, блокировка {1} отсутствует и поток A успешно входит в монитор!
  7. Тема A также делает копию {1} для обновления.

Теперь потоки A и B находятся внутри блока synchronized, и у них есть идентичные копии набора global={1}. Это означает, что одно из их обновлений будет потеряно! Эта ситуация вызвана тем фактом, что вы синхронизируете объект, хранящийся в ссылке, которую вы обновляете в своем блоке synchronized. Вы всегда должны быть очень осторожны с тем, какие объекты вы используете для синхронизации. Вы можете решить эту проблему, добавив новую переменную, которая будет действовать как блокировка:

private volatile Collection global = new HashSet(); // start threading after this
private final Object globalLock = new Object(); // final reference used for synchronization

void allUpdatesGoThroughHere(Object exampleOperand) {
  // My hypothesis is that this prevents operations in the block being re-ordered
  synchronized(globalLock) {
    Collection copy = new HashSet(global);
    copy.remove(exampleOperand);
    // Given my hypothesis, we should have a fully constructed object here. So a 
    // reader will either get the old or the new Collection, but never an 
    // inconsistent one.
    global = copy;    
  }
}

Эта ошибка была настолько коварной, что ни один из других ответов еще не исправил ее. Именно такие сумасшедшие детали параллелизма заставляют меня рекомендовать использовать что-то из уже отлаженной библиотеки java.util.concurrent вместо того, чтобы пытаться собрать что-то самостоятельно. Я думаю, что приведенное выше решение сработает, но как легко было бы снова облажаться? Это было бы намного проще:

private final Set<Object> global = Collections.newSetFromMap(new ConcurrentHashMap<Object,Boolean>());

Поскольку ссылка - final, вам не нужно беспокоиться о потоках, использующих устаревшие ссылки, а поскольку ConcurrentHashMap обрабатывает все неприятные проблемы с моделью памяти внутренне, вам не нужно беспокоиться обо всех неприятных деталях мониторов и барьеров памяти!

person DaoWen    schedule 15.08.2012
comment
Цитирую ... Подобно Hashtable, но в отличие от HashMap, этот класс не позволяет использовать null в качестве ключа или значения. ;) - person obataku; 15.08.2012
comment
@veer - Спасибо! Я изменил его, чтобы вместо этого рекомендовать Boolean.TRUE. - person DaoWen; 15.08.2012
comment
Кстати, вам может быть выгодно использовать _ 1_. - person obataku; 15.08.2012
comment
@veer - Это действительно интересно! Приятно знать, что это есть. Я уже проголосовал за ваш ответ, поскольку он фактически отвечает на все вопросы автора. Я начал писать свой пост до того, как заметил внизу ваши правки об использовании java.util.concurrent, иначе я бы даже не стал отвечать. Вы, очевидно, знаете свой материал! - person DaoWen; 15.08.2012
comment
Благодарность! Вы внесли хорошие предложения по альтернативным вариантам, что, вероятно, более ценно, чем ответы на его прямые вопросы о том, как катать свой собственный, в любом случае ...;) - person obataku; 15.08.2012
comment
Вероятно, именно этим я и займусь, но мне все еще любопытно, как мое минималистское решение может потерпеть неудачу. - person MilesHampson; 15.08.2012
comment
Майлз Хэмпсон - я обновил свой ответ красивым длинным эссе о проблемах в решении, которое вы разместили в своем вопросе. Наслаждаться! - person DaoWen; 15.08.2012
comment
Спасибо, это редактирование было именно тем, что я искал (кстати, вы могли решить эту проблему, просто переместив ключевое слово synchronized в метод, а не добавляя новый монитор). Я согласен, так сложно рассуждать о поведении параллельного кода, что у вас должна быть действительно веская причина не использовать java-утилиты. И спасибо за ссылку на модель памяти, imho, еще одно обязательное чтение - это книга Дуга Ли «Параллельное программирование на Java: принципы и шаблоны проектирования». - person MilesHampson; 15.08.2012
comment
Майлз Хэмпсон - Хороший момент о возможности просто перейти synchronized на уровень метода. Честно говоря, я даже не думал об этом, потому что всегда избегал этого. Это определенно сработает здесь, если на this ничего не синхронизируется. Дуг Ли - умный парень, я уверен, что его книгу стоит прочитать. - person DaoWen; 15.08.2012

Согласно соответствующему руководству по Java,

Мы уже видели, что выражение приращения, такое как c++, не описывает атомарного действия. Даже очень простые выражения могут определять сложные действия, которые можно разложить на другие действия. Однако есть действия, которые вы можете указать, которые являются атомарными:

  • Чтение и запись являются атомарными для ссылочных переменных и для большинства примитивных переменных (всех типов, кроме long и double).
  • Чтение и запись являются атомарными для всех переменных, объявленных volatile (включая long и double переменные).

Это подтверждается Разделом §17.7 Закона Спецификация языка Java

Запись и чтение ссылок всегда атомарны, независимо от того, реализованы ли они как 32-битные или 64-битные значения.

Похоже, что вы действительно можете рассчитывать на атомарный доступ по ссылке; однако учтите, что это не гарантирует, что все считыватели будут читать обновленное значение для global после этой записи, т.е. здесь нет гарантии упорядочения памяти.

Если вы используете неявную блокировку через synchronized при любом доступе к global, тогда вы можете добиться здесь некоторой согласованности памяти ... но, возможно, лучше использовать альтернативный подход.

Вы также, похоже, хотите, чтобы коллекция в global оставалась неизменной ... к счастью, существует _ 11_, который можно использовать для обеспечения этого. Например, вам, вероятно, следует сделать что-то вроде следующего ...

private volatile Collection global = Collections.unmodifiableSet(new HashSet());

... это или с помощью AtomicReference,

private AtomicReference<Collection> global = new AtomicReference<>(Collections.unmodifiableSet(new HashSet()));

Затем вы также можете использовать Collections.unmodifiableSet для ваших измененных копий.


// ... All reads are done through a reference copy like:
// Collection copy = global;
// for (Object elm: copy) {...
// so the global reference being updated half way through should have no impact

Вы должны знать, что создание копии здесь излишне, поскольку внутри for (Object elm : global) создается Iterator следующим образом ...

final Iterator it = global.iterator();
while (it.hasNext()) {
  Object elm = it.next();
}

Следовательно, невозможно переключиться на совершенно другое значение для global во время чтения.


Помимо всего этого, я согласен с мнение, выраженное DaoWen ... есть ли причина, по которой вы размещаете здесь свою собственную структуру данных, когда может быть альтернатива, доступная в java.util.concurrent? Я подумал, что, возможно, вы имеете дело со старой Java, поскольку вы используете необработанные типы, но спросить не повредит.

Семантику коллекции копирования при записи можно найти в CopyOnWriteArrayList или его двоюродный брат CopyOnWriteArraySet (который реализует Set с использованием первого).


Также предложенный DaoWen, рассматривали ли вы возможность использования _ 25_? Они гарантируют, что использование цикла for, как вы это сделали в своем примере, будет согласованным.

Точно так же итераторы и перечисления возвращают элементы, отражающие состояние хэш-таблицы в какой-то момент во время или после создания итератора / перечисления.

Внутри Iterator используется для улучшения for по сравнению с Iterable.

Вы можете создать Set из этого, используя _ 31_ следующим образом:

final Set<E> safeSet = Collections.newSetFromMap(new ConcurrentHashMap<E, Boolean>());
...
/* guaranteed to reflect the state of the set at read-time */
for (final E elem : safeSet) {
  ...
}
person obataku    schedule 15.08.2012
comment
Спасибо за ответ, но есть пара моментов, которые мне непонятны. Почему вы говорите, что упорядочение памяти - это проблема? Согласно предоставленному определению, читатели получают либо старую структуру данных, либо новую структуру данных, верно? Также зачем мне делать глобальную изменчивую, учитывая, что мой читатель может сделать копию ссылки в атомарной операции? У меня такое чувство, что я что-то здесь упускаю ... - person MilesHampson; 15.08.2012
comment
Я предлагал вам использовать volatile, чтобы гарантировать, что другие потоки получат последнее записанное значение при чтении поля ... в противном случае нет никаких гарантий, когда ваши читатели получат обновленную ссылку. - person obataku; 15.08.2012
comment
@MilesHampson С тех пор я обновил свой пост. Вы можете задать любые вопросы, которые могут у вас возникнуть. - person obataku; 15.08.2012
comment
На самом деле для меня не имеет значения, когда читатели получат обновления (известные последние слова ...), поэтому я все еще считаю, что мое решение, указанное выше, будет работать. Спасибо за подробные изменения. Следует отметить, что я не считаю правильным сказать, что мне нужна неизменяемая коллекция (например, в моем сообщении использовалось remove ()). - person MilesHampson; 15.08.2012
comment
Вы действительно хотите, чтобы неизменяемые коллекции отображались через global; все ваши изменения внесены в копию. Я имел в виду global = Collections.unmodifiableSet(copy); - person obataku; 15.08.2012
comment
... но да, ваш подход должен работать. Помните, что нет никакой гарантии вообще, что читатели когда-либо прочитают обновленные значения. - person obataku; 15.08.2012

Я думаю, что ваша первоначальная идея была правильной, и DaoWen хорошо поработала над устранением ошибок. Если вы не можете найти что-то, что делает все за вас, лучше понять эти вещи, чем надеяться, что какой-то магический класс сделает это за вас. Магические классы могут облегчить вашу жизнь и уменьшить количество ошибок, но вы действительно хотите понимать, что они делают.

ConcurrentSkipListSet может помочь вам здесь. Это может избавить вас от всех ваших проблем с многопоточностью.

Однако он медленнее, чем HashSet (обычно - HashSets и SkipLists / Trees трудно сравнивать). Если вы делаете много чтений для каждой записи, то, что у вас есть, будет быстрее. Что еще более важно, если вы обновляете более одной записи за раз, ваши чтения могут увидеть противоречивые результаты. Если вы ожидаете, что всякий раз, когда есть запись A, есть запись B, и наоборот, список пропусков может дать вам одну без другой.

С вашим текущим решением для читателей содержимое карты всегда внутренне согласовано. При чтении можно быть уверенным, что для каждого B есть A. Можно быть уверенным, что метод size() дает точное количество элементов, которые будут возвращены итератором. Две итерации вернут одни и те же элементы в том же порядке.

Другими словами, allUpdatesGoThroughHere и ConcurrentSkipListSet - два хороших решения двух разных проблем.

person RalphChapin    schedule 15.08.2012
comment
Я смутно знал о CSL, но не об их производительности, я прочитаю research.ibm.com/people/m/michael/spaa-2002.pdf, чтобы узнать, что я могу от них получить. Задача вопроса заключалась в том, чтобы получить лучшее «стандартное» решение для моего конкретного вопроса, но я думаю, что одна из забавных частей SO - давать и получать указания о том, «как», а не просто предлагать «что», а именно: почему я продолжал спрашивать ответы на вопросы о начальной магической структуре данных для получения более подробной информации. Рад за ваш ответ, даже если я не могу сделать его принятым. - person MilesHampson; 16.08.2012
comment
@MilesHampson Мой ответ - просто пустая болтовня по сравнению со всей другой тяжелой работой, проделанной здесь, но я подумал, что стоит добавить. Мое собственное тестирование показывает, что CSL на 20–30% медленнее, чем TreeMaps, возможно, потому, что они пропускают списки вместо деревьев, и в основном из-за их необычного доступа к памяти. TreeMaps и HashMaps трудно сравнивать, потому что HM часто должны выделять новые массивы и могут испортить GC, когда они становятся большими, но намного быстрее, чем TM, когда они не делают этого. - person RalphChapin; 16.08.2012

Можете ли вы использовать метод Collections.synchronizedSet? Из HashSet Javadoc http://docs.oracle.com/javase/6/docs/api/java/util/HashSet.html

Set s = Collections.synchronizedSet(new HashSet(...));
person km1    schedule 15.08.2012
comment
Это относится к доступу к самой коллекции, а не к ссылке, хранящейся в поле. - person obataku; 15.08.2012
comment
@veer, в java-документе указано public static <T> Set<T> synchronizedSet(Set<T> s) Returns a synchronized (thread-safe) set backed by the specified set. In order to guarantee serial access, it is critical that all access to the backing set is accomplished through the returned set. - person km1; 16.08.2012
comment
да, это не то, чего хочет ОП. Я, кстати, не голосовал против вас. - person obataku; 16.08.2012

Замените synchronized на global volatile, и все будет в порядке с копированием при записи.

Хотя присвоение является атомарным, в других потоках оно не упорядочено с записью в объект, на который имеется ссылка. Должна быть связь происходит до, которую вы получаете с volatile или синхронизацией обоих операций чтения и записи.

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

Если вы использовали synchronized как для чтения, так и для записи, это было бы правильно, но производительность может быть невысокой при чтении, требующем передачи. ReadWriteLock может быть подходящим, но у вас все равно будет чтение, блокирующее запись.

Другой подход к проблеме публикации - использовать семантику конечного поля для создания объекта, который (теоретически) безопасен для небезопасной публикации.

Конечно, существуют и параллельные коллекции.

person Tom Hawtin - tackline    schedule 15.08.2012
comment
Вы определенно правы в том, что вам нужно ключевое слово volatile, но я не думаю, что удалять synchronized безопасно. Без блокировки у вас будет состояние гонки между созданием copy и назначением global, что может привести к потере обновлений. - person DaoWen; 15.08.2012
comment
@DaoWen О, возможно, в зависимости от того, что вы делаете. Что касается обновления, чтобы убедиться, что видимое состояние хорошее, в этом нет необходимости. Если это действительно проблема производительности, как предлагается, ReadWriteLock было бы более подходящим. - person Tom Hawtin - tackline; 15.08.2012
comment
В приведенном примере определенно будет состояние гонки. Я бы согласился с блокировкой чтения-записи, но опять же, у них обычно также есть проблема возможного голодания писателей бесконечными потоками читателей. Итак, как вы сказали, все зависит от обстоятельств. - person DaoWen; 15.08.2012
comment
Синхронизированный объект не может быть удален, так как мне нужно, чтобы это произошло, чтобы убедиться, что мои операции по изменению структуры данных завершены, прежде чем я выполню назначение. В чем преимущество глобальной нестабильности? Операция фиксации глобального состояния и операция копирования ссылки на глобальное состояние перед чтением уже атомарны, верно? Мне не нужно устанавливать порядок между ними, и, поскольку есть две разные области памяти, я мог бы подумать, что все читатели могут продолжать работать со своей областью, не обращая внимания на то, что глобальная ссылка меняется? - person MilesHampson; 15.08.2012
comment
P.S Я не голосовал против, я думаю, что включение volatile и ReadWriteLock в обсуждение было ценным, даже если удаление synchronized не сработает. - person MilesHampson; 15.08.2012