Несинхронизированное чтение целочисленных потоков в java?

Я вижу этот код довольно часто в некоторых модульных тестах OSS, но насколько безопасен поток? Является ли цикл while гарантированно видеть правильное значение invoc?

Если нет; nerd указывает на того, кто также знает, какая архитектура процессора может завершиться.

  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);
      }
  }

Ответ 1

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

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

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

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

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

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

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

Ответ 2

Ну!

  private volatile int invoc = 0;

Сделает трюк.

И посмотрите Явкие примитивные ints атомарно по дизайну или случайно?, в которых размещены некоторые из соответствующих определений java. По-видимому, int отлично, но double и long могут не быть.


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

Ответ 3

Теоретически возможно, что чтение кэшируется. Ничто в модели памяти Java не предотвращает это.

Практически, это крайне маловероятно (в вашем конкретном примере). Вопрос в том, может ли JVM оптимизироваться при вызове метода.

read #1
method();
read #2

Для JVM следует, что чтение №2 может повторно использовать результат чтения №1 (который может быть сохранен в регистре CPU), он должен точно знать, что method() не содержит никаких действий синхронизации. Это вообще невозможно - если только method() не встроен, и JVM может видеть из скользящего кода, что нет никаких синхронных/летучих или других действий синхронизации между чтением # 1 и чтением # 2; то он может безопасно устранить чтение # 2.

Теперь в вашем примере метод Thread.sleep(). Один из способов его реализации - цикл занятости в определенное время, в зависимости от частоты процессора. Затем JVM может встроить его, а затем устранить чтение # 2.

Но, конечно, такая реализация sleep() нереальна. Обычно он реализуется как собственный метод, который вызывает ядро ​​ОС. Вопрос в том, может ли JVM оптимизировать такой метод.

Даже если JVM обладает знаниями о внутренних функциях некоторых собственных методов, поэтому может оптимизировать их, что маловероятно, что sleep() обрабатывается таким образом. sleep(1ms) возвращает миллионы циклов ЦП, на самом деле нет смысла оптимизировать вокруг него, чтобы сохранить несколько чтений.

-

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

Ответ 4

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

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

Ответ 5

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