Синхронизация модели памяти Java: как вызвать ошибку видимости данных? - PullRequest
9 голосов
/ 22 января 2012

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

Проблема, которую этот класс пытается продемонстрировать, заключается в том, что переменные здесь не являются «общими» для потоков. Таким образом, значение потока видит может отличаться от другого потока, поскольку они не являются изменчивыми или синхронизированными. Также из-за переупорядочения операторов, разрешенного JVM, ready = true может быть установлено до числа = 42.

Для меня этот класс всегда хорошо работает с использованием JVM 1.6. Любая идея о том, как заставить этот класс выполнять некорректное поведение (то есть вывести 0 или запустить навсегда)?

public class NoVisibility {
    private static boolean ready;
    private static int number;

    private static class ReaderThread extends Thread {
        public void run() {
            while (!ready)
                Thread.yield();
            System.out.println(number);
        }
    }

    public static void main(String[] args) {
        new ReaderThread().start();
        number = 42;
        ready = true;
    }
}

Ответы [ 6 ]

6 голосов
/ 22 января 2012

Проблема в том, что вы недостаточно долго ожидаете оптимизации кода и кэширования значения.

Когда поток в системе x86_64 впервые читает значение, он получает потокобезопасную копию. Его только более поздние изменения он может не увидеть. Это может быть не так на других процессорах.

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

public class RequiresVolatileMain {
    static volatile boolean value;

    public static void main(String... args) {
        new Thread(new MyRunnable(true), "Sets true").start();
        new Thread(new MyRunnable(false), "Sets false").start();
    }

    private static class MyRunnable implements Runnable {
        private final boolean target;

        private MyRunnable(boolean target) {
            this.target = target;
        }

        @Override
        public void run() {
            int count = 0;
            boolean logged = false;
            while (true) {
                if (value != target) {
                    value = target;
                    count = 0;
                    if (!logged)
                        System.out.println(Thread.currentThread().getName() + ": reset value=" + value);
                } else if (++count % 1000000000 == 0) {
                    System.out.println(Thread.currentThread().getName() + ": value=" + value + " target=" + target);
                    logged = true;
                }
            }
        }
    }
}

печатает следующее, показывая переворачивание значения, но застревает.

Sets true: reset value=true
Sets false: reset value=false
...
Sets true: reset value=true
Sets false: reset value=false
Sets true: value=false target=true
Sets false: value=true target=false
....
Sets true: value=false target=true
Sets false: value=true target=false

Если я добавлю -XX:+PrintCompilation, это переключение произойдет примерно в то время, когда вы видите

1705    1 % RequiresVolatileMain$MyRunnable::run @ -2 (129 bytes)   made not entrant
1705    2 % RequiresVolatileMain$MyRunnable::run @ 4 (129 bytes)

То, что код был скомпилирован в native, - это способ, который не является поточно-ориентированным.

если вы сделаете значение volatile, вы увидите, что оно бесконечно переворачивает значение (или пока мне не надоест)

РЕДАКТИРОВАТЬ: Что делает этот тест; когда это обнаруживает, что значение не является тем целевым значением потоков, это устанавливает значение. то есть. поток 0 устанавливается на true, а поток 1 устанавливается на false Когда два потока правильно разделяют поле, они видят изменения друг друга, и значение постоянно меняется между истиной и ложью.

Без volatile это завершается неудачно, и каждый поток видит только свое собственное значение, поэтому они оба изменяют значение и поток 0, см. true, а поток 1 видит false для одного и того же поля.

6 голосов
/ 22 января 2012

Модель памяти Java определяет, что требуется для работы, а что нет. «Красота» небезопасного многопоточного кода заключается в том, что в большинстве ситуаций (особенно в контролируемых средах разработки) он обычно работает. только когда вы приступаете к работе с более совершенным компьютером и увеличиваете нагрузку, и JIT действительно замечает, что ошибки начинают кусаться.

2 голосов
/ 22 января 2012

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

2 голосов
/ 22 января 2012

Не уверен на 100% в этом, но это может быть связано:

Что подразумевается под переупорядочением?

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

Например, если поток записывает данные в поле a, а затем в поле b, а значение b выполняетне зависит от значения a, то компилятор может свободно переупорядочивать эти операции, а кэш может свободно сбрасывать b в основную память перед a.Существует ряд потенциальных источников переупорядочения, таких как компилятор, JIT и кэш.

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

1 голос
/ 22 января 2012

В зависимости от вашей ОС, Thread.yield () может работать или не работать.Thread.yield () на самом деле нельзя считать независимым от платформы, и его не следует использовать, если вам нужно это предположение.

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

0 голосов
/ 29 января 2015

Пожалуйста, смотрите код ниже, он представляет ошибку видимости данных на x86. Пробовал с jdk8 и jdk7

package com.snippets;


public class SharedVariable {

    private static int  sharedVariable = 0;// declare as volatile to make it work
    public static void main(String[] args) throws InterruptedException {

        new Thread(new Runnable() {

            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                sharedVariable = 1;
            }
        }).start();

        for(int i=0;i<1000;i++) {
            for(;;) {
                if(sharedVariable == 1) {
                    break;
                }
            }
        }
        System.out.println("Value of SharedVariable : " + sharedVariable);
    }

}

Хитрость заключается не в том, чтобы процессор делал переупорядочение, а в том, чтобы сделать компилятор для оптимизации, которая вносит ошибку видимости.

Если вы запустите приведенный выше код, вы увидите, что он зависает бесконечно, потому что он никогда не видит обновленное значение sharedVariable.

Чтобы исправить код, объявите sharedVariable как volatile.

Почему обычная переменная не работает и вышеприведенная программа зависает?

  1. sharedVariable не был объявлен как volatile.
  2. Теперь, поскольку sharedVariable не был объявлен как volatile, компилятор оптимизирует код. Он видит, что sharedVariable не будет изменен, так почему я должен читать из памяти каждый раз в цикле. Это выведет sharedVariable из цикла. Нечто похожее на ниже.

F

for(int i=0;i<1000;i++)/**compiler reorders sharedVariable
as it is not declared as volatile
and takes out the if condition out of the loop
which is valid as compiler figures out that it not gonna  
change sharedVariable is not going change **/
    if(sharedVariable != 1) {  
     for(;;) {}  
    }      
}

Общий доступ на github: https://github.com/lazysun/concurrency/blob/master/Concurrency/src/com/snippets/SharedVariable.java

...