Несинхронизированное чтение целочисленного потока безопасно в Java?
Я вижу этот код довольно часто в некоторых модульных тестах OSS, но безопасен ли он для потоков? Гарантирован ли цикл while правильное значение invoc?
Если нет; ботаник указывает на того, кто знает, на какой архитектуре ЦП это может произойти.
private int invoc = 0;
private synchronized void increment() {
invoc++;
}
public void isItThreadSafe() throws InterruptedException {
for (int i = 0; i < TOTAL_THREADS; i++) {
new Thread(new Runnable() {
public void run() {
// do some stuff
increment();
}
}).start();
}
while (invoc != TOTAL_THREADS) {
Thread.sleep(250);
}
}
5 ответов
Нет, это не потокобезопасно. invoc должен быть объявлен как volatile, или доступен при синхронизации с той же блокировкой, или изменен для использования AtomicInteger. Просто использовать синхронизированный метод для увеличения invoc, но не синхронизировать его для чтения, недостаточно.
JVM выполняет много оптимизаций, включая кэширование для конкретного процессора и переупорядочение команд. Он использует ключевое слово volatile и блокировку, чтобы решить, когда он может свободно оптимизировать и когда ему нужно иметь актуальное значение, доступное для чтения другими потоками. Поэтому, когда читатель не использует блокировку, JVM не может знать, не присвоить ли ей устаревшее значение.
Эта цитата из Java Concurrency in Practice (раздел 3.1.3) обсуждает, как нужно синхронизировать как записи, так и чтения:
Внутренняя блокировка может быть использована, чтобы гарантировать, что один поток видит эффекты другого в предсказуемой манере, как показано на рисунке 3.1. Когда поток A выполняет синхронизированный блок, а затем поток B входит в синхронизированный блок, защищенный той же самой блокировкой, значения переменных, которые были видны A до снятия блокировки, гарантированно будут видны B после получения блокировки. Другими словами, все, что A делал в синхронизированном блоке или перед ним, является видимым для B, когда он выполняет синхронизированный блок, защищенный той же блокировкой. Без синхронизации такой гарантии нет.
Следующий раздел (3.1.4) охватывает использование volatile:
Язык Java также предоставляет альтернативную, более слабую форму синхронизации, изменчивых переменных, чтобы гарантировать, что обновления переменной передаются предсказуемо другим потокам. Когда поле объявляется как volatile, компилятор и среда выполнения уведомляются о том, что эта переменная является общей и что операции с ней не должны переупорядочиваться с другими операциями с памятью. Энергозависимые переменные не кэшируются в регистрах или в кэшах, где они скрыты от других процессоров, поэтому чтение изменчивой переменной всегда возвращает самую последнюю запись любым потоком.
Когда у всех наших компьютеров были однопроцессорные компьютеры, мы писали код и никогда не сталкивались с проблемами, пока он не работал на многопроцессорном компьютере, обычно в производстве. Некоторые факторы, которые вызывают проблемы с видимостью, такие как локальные кэши ЦП и переупорядочение команд, - это то, что вы ожидаете от любой многопроцессорной машины. Однако устранение явно ненужных инструкций может произойти для любой машины. Ничто не заставляет JVM когда-либо заставлять читателя видеть актуальное значение переменной, вы зависите от разработчиков JVM. Так что, мне кажется, этот код не будет хорошим выбором для любой архитектуры ЦП.
Что ж!
private volatile int invoc = 0;
Сделаем трюк.
И посмотрите, являются ли примитивные java атомными по своему замыслу или случайно? какие сайты некоторые из соответствующих определений Java. Очевидно, что int - это хорошо, но double & long может и не быть.
редактировать, дополнения. Вопрос спрашивает: "видите правильное значение invoc?". Что такое "правильное значение"? Как и в пространственно-временном континууме, в действительности потоков не существует между потоками. В одном из приведенных выше сообщений отмечается, что значение в конечном итоге будет сброшено, а другой поток получит его. Код "потокобезопасен"? Я бы сказал "да", потому что в этом случае он не будет "плохо себя вести" из-за капризов последовательности.
Теоретически, возможно, что чтение кэшируется. Ничто в модели памяти Java не мешает этому.
На практике это крайне маловероятно (в вашем конкретном примере). Вопрос в том, может ли JVM оптимизировать вызов метода.
read #1
method();
read #2
Чтобы JVM могла предположить, что чтение #2 может повторно использовать результат чтения #1 (который может быть сохранен в регистре процессора), он должен знать наверняка, что method()
не содержит синхронизирующих действий. Это вообще невозможно - если только method()
является встроенным, и JVM может видеть из плоского кода, что между read#1 и read#2 нет никакой синхронизации / volatile или других действий синхронизации; тогда это может безопасно устранить чтение #2.
Теперь в вашем примере метод Thread.sleep()
, Один из способов реализовать это - это занятый цикл в течение определенного времени, в зависимости от частоты процессора. Затем JVM может включить его, а затем исключить чтение #2.
Но конечно такая реализация sleep()
нереально. Обычно он реализован как собственный метод, который вызывает ядро ОС. Вопрос в том, может ли JVM оптимизировать этот собственный метод.
Даже если JVM знает о внутренней работе некоторых собственных методов, поэтому может оптимизировать их, маловероятно, что sleep()
так трактуется. sleep(1ms)
требуются миллионы циклов ЦП для возврата, и нет смысла оптимизировать его, чтобы сэкономить несколько операций чтения.
-
Эта дискуссия раскрывает самую большую проблему гонок данных - для ее решения требуется слишком много усилий. Программа не обязательно ошибается, если она не "правильно синхронизирована", однако доказать, что она не неправильная, задача не из легких. Жизнь намного проще, если программа правильно синхронизирована и не содержит данных гонки.
Насколько я понимаю, код должен быть безопасным. Да, байт-код можно изменить. Но в конечном итоге invoc должен снова синхронизироваться с основным потоком. Synchronize гарантирует, что invoc правильно увеличивается, поэтому в некоторых регистрах есть единообразное представление invoc. Через некоторое время это значение будет сброшено, и небольшой тест завершится успешно.
Это, конечно, нехорошо, и я бы пошел с ответом, за который проголосовал, и исправил бы код, как этот, потому что он пахнет. Но подумав об этом, я бы посчитал это безопасным.
Если вам не нужно использовать "int", я бы предложил AtomicInteger в качестве многопоточной альтернативы.