Синхронизация модели памяти 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;
}
}
}
}
}
печатает следующее, показывая его изменение значения, но застревает.
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
для того же поля.
Модель памяти Java определяет, что требуется для работы, а что нет. "Красота" небезопасного многопоточного кода заключается в том, что в большинстве ситуаций (особенно в контролируемых средах разработки) он обычно работает. только когда вы приступаете к работе с более совершенным компьютером и увеличиваете нагрузку, и JIT действительно замечает, что ошибки начинают кусаться.
Я думаю, что главное в этом то, что не гарантируется, что все jvms будут переупорядочивать инструкции одинаково. Он используется в качестве примера того, что существуют различные возможные переупорядочения, и поэтому для некоторых реализаций jvm вы можете получить разные результаты. Просто так получилось, что ваш jvm каждый раз переупорядочивается одинаково, но это может быть не так для другого. Единственный способ гарантировать порядок - использовать правильную синхронизацию.
Не уверен на 100% в этом, но это может быть связано:
Что подразумевается под переупорядочением?
Существует ряд случаев, когда доступ к переменным программы (полям экземпляра объекта, статическим полям класса и элементам массива) может казаться выполненным в порядке, отличном от заданного программой. Компилятор может свободно пользоваться порядком инструкций во имя оптимизации. Процессоры могут выполнять инструкции не по порядку при определенных обстоятельствах. Данные могут перемещаться между регистрами, кэшами процессора и основной памятью в порядке, отличном от указанного в программе.
Например, если поток записывает данные в поле a, а затем в поле b, и значение b не зависит от значения a, то компилятор может переупорядочивать эти операции, а кэш свободно сбрасывать b в main память до а. Существует ряд потенциальных источников переупорядочения, таких как компилятор, JIT и кэш.
Предполагается, что компилятор, среда выполнения и аппаратное обеспечение создают иллюзию семантики "как если бы", что означает, что в однопоточной программе программа не должна иметь возможность наблюдать последствия переупорядочений. Однако переупорядочения могут вступать в действие в неправильно синхронизированных многопоточных программах, где один поток может наблюдать влияние других потоков и может обнаруживать, что доступ к переменным становится видимым для других потоков в порядке, отличном от выполняемого или указанного в программа
В зависимости от вашей ОС, Thread.yield() может работать или не работать. Thread.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.
Почему обычная переменная не работает и вышеприведенная программа зависает?
- sharedVariable не был объявлен как volatile.
- Теперь, поскольку sharedVariable не был объявлен как volatile, компилятор оптимизирует код. Он видит, что sharedVariable не будет изменен, поэтому я должен читать из памяти каждый раз в цикле. Это выведет sharedVariable из цикла. Нечто похожее на ниже.
е
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