Почему маркировка переменной Java volatile делает вещи менее синхронизированными? - PullRequest
7 голосов
/ 09 ноября 2010

Итак, я только что узнал о ключевом слове volatile, когда писал несколько примеров для раздела, который я собираюсь обучить завтра. Я написал быструю программу, чтобы продемонстрировать, что операции ++ и - не являются атомарными.

public class Q3 {

    private static int count = 0;

    private static class Worker1 implements Runnable{

        public void run(){
            for(int i = 0; i < 10000; i++)
                count++; //Inner class maintains an implicit reference to its parent
        }
    }

    private static class Worker2 implements Runnable{

        public void run(){
            for(int i = 0; i < 10000; i++)
                count--; //Inner class maintains an implicit reference to its parent
        }
    }


    public static void main(String[] args) throws InterruptedException {
        while(true){
            Thread T1 = new Thread(new Worker1());
            Thread T2 = new Thread(new Worker2());
            T1.start();
            T2.start();

            T1.join();
            T2.join();
            System.out.println(count);
            count = 0;
            Thread.sleep(500);

        }
    }
}

Как и ожидалось, результат этой программы, как правило, примерно такой:

-1521
  -39
    0
    0
    0
    0
    0
    0

Однако, когда я меняюсь:

private static int count = 0;

до

private static volatile int count = 0;

мой вывод изменится на:

    0
 3077
    1
-3365
   -1
   -2
 2144
    3
    0
   -1
    1
   -2
    6
    1
    1

Я прочитал Когда именно вы используете ключевое слово volatile в Java? поэтому я чувствую, что у меня есть общее представление о том, что делает ключевое слово (поддерживать синхронизацию между кэшированными копиями переменной разные темы, но не безопасны для чтения-обновления-записи). Я понимаю, что этот код, конечно, не является потокобезопасным. В частности, это не небезопасно, чтобы служить примером для моих учеников. Однако мне любопытно, почему добавление ключевого слова volatile делает вывод не таким «стабильным», как при отсутствии ключевого слова.

Ответы [ 7 ]

6 голосов
/ 09 ноября 2010

Почему маркировка переменной Java энергозависимой делает вещи менее синхронизированными?

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

Кроме того, этоВажно помнить, что хотя x++ кажется атомарным, на самом деле это операция чтения / изменения / записи.Если вы запустите свою тестовую программу на нескольких разных архитектурах, вы найдете разные результаты, потому что реализация JVM volatile очень зависит от аппаратного обеспечения.Доступ к полям volatile также может быть значительно медленнее, чем доступ к кэшированным полям - иногда на 1 или 2 порядка, что изменит время вашей программы.

Использование ключевого слова volatile действительно возбуждает памятьбарьер для конкретного поля и (с Java 5) этот барьер памяти распространяется на все другие общие переменные.Это означает, что значение переменных будет скопировано в / из центрального хранилища при обращении к нему.Однако между Java * 1016 и ключевым словом synchronized есть небольшие различия.Например, с volatile блокировка не происходит, поэтому, если несколько потоков обновляют изменчивую переменную, условия гонки будут существовать вокруг неатомарных операций.Вот почему мы используем AtomicInteger и друзей, которые должным образом заботятся о функциях приращения без синхронизации.

Вот несколько хороших чтений по этому вопросу:

Надеюсь, это поможет.

5 голосов
/ 09 ноября 2010

Обоснованное предположение о том, что вы видите - когда JIT-компилятор не помечен как изменчивый, он использует операции x86 inc / dec, которые могут атомарно обновлять переменную.После пометки volatile эти операции больше не используются, а переменная вместо этого читается, увеличивается / уменьшается, а затем, наконец, записывается, вызывая больше «ошибок».

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

Одним из решений будет использование класса AtomicInteger, который допускает атомные приращения / убывания.

3 голосов
/ 09 ноября 2010

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

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

1 голос
/ 15 января 2014

Я наткнулся на этот вопрос и после игры с кодом немного нашел очень простой ответ.

После первоначального прогрева и оптимизации (первые 2 числа перед нулями), когда JVM работаетна полной скорости T1 просто запускается и заканчивается до того, как T2 даже запускается, поэтому count идет до 10000, а затем до 0. Когда я изменил количество итераций в рабочемпотоки от 10000 до 100000000 вывод очень нестабилен и каждый раз отличается.

Причина нестабильного вывода при добавлении volatile заключается в том, что он делает код намного медленнее и даже при 10000 итерациях T2 достаточновремя начинать и мешать T1.

0 голосов
/ 10 октября 2013

Причина всех этих нулей в , а не в том, что ++ и - уравновешивают друг друга. Причина в том, что здесь нечего вызывать count в циклах влияет на count в основном потоке. Вам нужны блоки синхронизации или энергозависимый count («барьер памяти»), чтобы заставить JVM заставить все видеть одно и то же значение. С вашим конкретным JVM / оборудованием, скорее всего, происходит то, что значение сохраняется в регистре вообще раз и никогда не попадать в кеш - не говоря уже о основной памяти - вообще.

Во втором случае вы делаете то, что намеревались: неатомарное увеличение и уменьшение одного и того же course и получение результатов, примерно таких, как вы ожидали.

Это древний вопрос, но что-то нужно было сказать о каждом потоке, который имеет собственную , независимую копию данных.

0 голосов
/ 19 октября 2011

Если вы видите значение count, которое не , кратное 10000, это просто показывает, что у вас плохой оптимизатор.

0 голосов
/ 09 ноября 2010

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...