Использование летучих
Это тот случай, когда один поток заботится о том, что делает другой? Тогда JMM FAQ имеет ответ:
В большинстве случаев одна нить не
все равно, что делает другой. Но когда
это делает, это то, что синхронизация
для.
В ответ на вопрос тех, кто говорит, что код OP безопасен как есть, рассмотрим следующее: в модели памяти Java нет ничего, что гарантировало бы, что это поле будет сброшено в основную память при запуске нового потока. Кроме того, JVM может свободно переупорядочивать операции, если изменения не обнаруживаются в потоке.
Теоретически, потокам читателей не гарантируется "запись" в validProgramCodes. На практике они в конечном итоге будут, но вы не можете быть уверены, когда.
Я рекомендую объявить элемент validProgramCodes как volatile. Разница в скорости будет незначительной, и она гарантирует безопасность вашего кода сейчас и в будущем, независимо от того, какие оптимизации JVM могут быть введены.
Вот конкретная рекомендация:
import java.util.Collections;
class Metadata {
private volatile Map validProgramCodes = Collections.emptyMap();
public Map getValidProgramCodes() {
return validProgramCodes;
}
public void setValidProgramCodes(Map h) {
if (h == null)
throw new NullPointerException("validProgramCodes == null");
validProgramCodes = Collections.unmodifiableMap(new HashMap(h));
}
}
Неизменность
В дополнение к обертке с unmodifiableMap
, я копирую карту (new HashMap(h)
). Это делает моментальный снимок, который не изменится, даже если вызывающая программа установщика продолжит обновлять карту "h". Например, они могут очистить карту и добавить новые записи.
Зависит от интерфейсов
На стилистической ноте часто лучше объявлять API с абстрактными типами, такими как List
и Map
, а не с конкретными типами, такими как ArrayList
и HashMap.
. Это дает гибкость в будущем, если конкретные типы должны изменить (как я сделал здесь).
Кэширование
Результатом присвоения «h» «validProgramCodes» может быть просто запись в кэш процессора. Даже когда начинается новый поток, «h» не будет виден новому потоку, если он не был сброшен в общую память. Хорошее время выполнения позволит избежать очистки, если в этом нет необходимости, и использование volatile
является одним из способов указать, что это необходимо.
1039 * Изменение порядка *
Примите следующий код:
HashMap codes = new HashMap();
codes.putAll(source);
meta.setValidProgramCodes(codes);
Если setValidCodes
- это просто OP validProgramCodes = h;
, компилятор может изменить порядок кода следующим образом:
1: meta.validProgramCodes = codes = new HashMap();
2: codes.putAll(source);
Предположим, что после выполнения строки 1 для записи поток чтения начинает выполнять этот код:
1: Map codes = meta.getValidProgramCodes();
2: Iterator i = codes.entrySet().iterator();
3: while (i.hasNext()) {
4: Map.Entry e = (Map.Entry) i.next();
5: // Do something with e.
6: }
Теперь предположим, что поток писателя вызывает «putAll» на карте между строкой 2 и 3 читателя. Карта, лежащая в основе Итератора, претерпела одновременное изменение и выдает исключение во время выполнения - дьявольски прерывистое, казалось бы необъяснимое исключение во время выполнения это никогда не было произведено во время тестирования.
Параллельное программирование
Каждый раз, когда у вас есть один поток, которому небезразлично, что делает другой поток, вы должны иметь некоторый барьер памяти, чтобы гарантировать, что действия одного потока видны другому. Если событие в одном потоке должно произойти до события в другом потоке, вы должны указать это явно. Там нет никаких гарантий в противном случае. На практике это означает volatile
или synchronized
.
Не экономьте. Неважно, как быстро неправильная программа не справляется со своей задачей. Приведенные здесь примеры являются простыми и надуманными, но будьте уверены, они иллюстрируют реальные ошибки параллелизма, которые невероятно сложно выявить и устранить из-за их непредсказуемости и чувствительности платформы.
Дополнительные ресурсы