Несинхронизированное чтение целочисленного потока безопасно в Java? - PullRequest
11 голосов
/ 05 августа 2011

Я вижу этот код довольно часто в некоторых модульных тестах 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 ]

17 голосов
/ 05 августа 2011

Нет, это не потокобезопасно. invoc должен быть объявлен как volatile, или доступен при синхронизации с той же блокировкой, или изменен для использования AtomicInteger. Просто использовать синхронизированный метод для увеличения invoc, но не синхронизировать его для чтения, недостаточно.

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

В этой цитате из Java-параллелизма на практике (раздел 3.1.3) обсуждается, как синхронизация записи и чтения должна быть синхронизирована:

Внутренняя блокировка может использоваться, чтобы гарантировать, что один поток видит эффекты другого в предсказуемой манере, как показано на рисунке 3.1. Когда поток A выполняет синхронизированный блок, а затем поток B входит в синхронизированный блок, защищенный той же самой блокировкой, значения переменных, которые были видны A до снятия блокировки, гарантированно будут видны B после получения блокировки. Другими словами, все, что A делал в или до синхронизированного блока, видимо B, когда он выполняет синхронизированный блок, защищенный той же блокировкой. Без синхронизации такой гарантии нет.

Следующий раздел (3.1.4) описывает использование volatile:

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

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

2 голосов
/ 05 августа 2011

Хорошо!

  private volatile int invoc = 0;

сделает свое дело.

И посмотрите Являются ли java примитивные целые атомарными по своему замыслу или случайно? в которых содержатся некоторые соответствующие определения java. Очевидно, что int - это хорошо, но double & long может и не быть.


редактировать, дополнение. Вопрос спрашивает: «видите правильное значение invoc?». Что такое «правильное значение»? Как и в пространственно-временном континууме, в действительности потоков не существует между потоками. В одном из приведенных выше сообщений отмечается, что значение в конечном итоге будет сброшено, а другой поток получит его. Код "потокобезопасен"? Я бы сказал «да», потому что в этом случае он не будет «плохо себя вести» из-за капризов последовательности.

1 голос
/ 05 августа 2011

Теоретически, возможно, что чтение кэшируется. Ничто в модели памяти 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) требуются миллионы циклов ЦП для возврата, и нет смысла оптимизировать его, чтобы сэкономить несколько операций чтения.

-

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

0 голосов
/ 05 августа 2011

Если вам не нужно использовать «int», я бы предложил AtomicInteger в качестве многопоточной альтернативы.

0 голосов
/ 05 августа 2011

Насколько я понимаю, код должен быть безопасным. Да, байт-код можно изменить. Но в конечном итоге invoc должен снова синхронизироваться с основным потоком. Synchronize гарантирует, что invoc правильно увеличивается, поэтому в некоторых регистрах существует единообразное представление invoc. Через некоторое время это значение будет сброшено, и небольшой тест завершится успешно.

Это, конечно, нехорошо, и я согласился бы с ответом, за который проголосовал , а исправит код, подобный этому, потому что он пахнет. Но подумав об этом, я бы посчитал это безопасным.

...