Синхронизация модели памяти Java: как вызвать ошибку видимости данных?

"параллелизм 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 ответов


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

когда поток в системе 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;
                }
            }
        }
    }
}

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

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 вы видите, что он бесконечно переворачивает значение (или пока мне не стало скучно)

EDIT: что делает этот тест; когда он обнаруживает, что значение не является целевым значением потоков, он устанавливает значение. то есть. нить 0 наборы для true и поток 1 устанавливает в false когда два потока совместно используют поле должным образом, они видят, что изменения друг друга, и значение постоянно переключается между true и false.

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


модель памяти java определяет, что требуется для работы, а что нет. "красота" небезопасного многопоточного кода заключается в том, что в большинстве ситуаций (особенно в contolled dev-средах) он обычно работает. только когда вы добираетесь до производства с лучшим компьютером и нагрузка увеличивается, и JIT действительно пинает, что ошибки начинают кусаться.


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

Что подразумевается под дозаказа?

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

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

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


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


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

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


см. ниже код, он вводит ошибку видимости данных на 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