Не существует «лучшего способа выполнить безопасную публикацию», поскольку решение о способе публикации зависит от фактического варианта использования, связанного с публикацией.
Так что неверно говорить, что Collections.unmodifiableMap(…)
не является безопасная публикация. Этот метод вообще не является публикацией.
Когда поток потенциально изменяет данные после публикации объекта, т.е. когда другой поток уже может обрабатывать данные, безопасная публикация вообще не существует.
A ConcurrentHashMap
может решить эту проблему не потому, что это делает публикацию карты безопасной, а потому, что каждая модификация является безопасной публикацией сама по себе. Это по-прежнему делает его использование потокобезопасным только в том случае, если использующие потоки могут справиться с тем фактом, что непротиворечивого общего состояния карты не существует, когда существуют изменения во время обработки карты, но согласованы только отдельные отображения. И каждый ключ или значение не должны изменяться после публикации.
Если вы соблюдаете правило, согласно которому вы не должны изменять объект (ы) после публикации, существует множество возможностей для правильного издание. Рассмотрим:
HashMap<String, List<Integer>> map = new HashMap<>();
List<Integer> l = new ArrayList<>();
l.add(42);
map.put("foo", l);
Thread t = new Thread() {
@Override
public void run() {
System.out.println(map);
}
};
t.start();
System.out.println(map);
t.join();
Между вызовом t.start()
и любым действием, выполняемым потоком, существует отношение , которое происходит до . Этого достаточно для безопасной публикации HashMap
и содержимого ArrayList
без каких-либо дополнительных усилий - при условии соблюдения правила «без изменений после публикации». Тот факт, что оба потока могут читать карту одновременно, не имеет значения.
Поэтому, когда у вас есть код, такой как
volatile Object[] array = new Object[size];
array[index] = value;
, вы правы, это небезопасно, потому что нарушает «нет правило «модификация после публикации».
Но CopyOnWriteArrayList
отличается, если внимательно посмотреть опубликованный код:
public E set(int index, E element) {
synchronized (lock) {
Object[] es = getArray();
E oldValue = elementAt(es, index);
if (oldValue != element) {
es = es.clone(); // <- create a local copy
es[index] = element; // <- modifies the copy that has not been published
}
// Ensure volatile write semantics even when oldvalue == element
setArray(es); // <- publishes the copy
return oldValue;
}
}
Таким образом, нет нарушения «нет изменений после публикации» Как правило, модификации всегда выполняются локальными копиями перед их публикацией, а запись volatile
совершенно новой ссылки на массив устанавливает отношение случай-до с последующими чтениями volatile
этой ссылки. synchronized
существует только для обеспечения согласованности нескольких модификаций.
Обратите внимание, что опубликованный вами код в некоторых отношениях отличается от этой реализации OpenJDK :
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;
}
}
Хотя оба работают без проблем при правильном использовании, разница
// Ensure volatile write semantics even when oldvalue == element
setArray(es); // invoked even when oldvalue == element
показывает неправильное мышление, точно соответствующее обсуждаемому правилу. Когда oldvalue == element
, заданный элемент уже был в списке, другими словами, уже опубликован. Поэтому, если элемент был изменен перед этим избыточным вызовом set
, он нарушил бы правило «без изменений после публикации», и выполнение еще одной записи volatile
не исправило бы это. С другой стороны, если не было сделано никаких изменений, запись volatile
была бы устаревшей. Таким образом, нет никакой причины выполнять запись volatile
, когда oldvalue == element
.
Это может помочь вам оценить ваш подход ThreadLocal
. Нет смысла создавать новую копию для каждого потока. Поскольку эти копии никогда не изменяются, они могут быть безопасно прочитаны произвольным числом потоков. Но так как эти снимки никогда не изменяются, ни один поток не заметит изменений, которые были внесены в исходную карту после создания ее снимка. Если изменения происходят редко и у вас много читателей, схожий подход к CopyOnWriteArrayList
сработает.