Неинициализированный объект просочился в другой поток, несмотря на отсутствие явного утечки кода?
Давайте посмотрим на эту простую Java-программу:
import java.util.*;
class A {
static B b;
static class B {
int x;
B(int x) {
this.x = x;
}
}
public static void main(String[] args) {
new Thread() {
void f(B q) {
int x = q.x;
if (x != 1) {
System.out.println(x);
System.exit(1);
}
}
@Override
public void run() {
while (b == null);
while (true) f(b);
}
}.start();
for (int x = 0;;x++)
b = new B(Math.max(x%2,1));
}
}
Основная тема
Основной поток создает экземпляр B
с x
установить в 1, затем записывает этот экземпляр в статическое поле A.b
, Это повторяет это действие навсегда.
Опросная нить
Порожденная нить опрашивает, пока не обнаружит, что A.b.x
это не 1.
?!?
Половину времени он проходит в бесконечном цикле, как и ожидалось, но половину времени я получаю этот вывод:
$ java A
0
Почему поток опроса может видеть B
который имеет x
не установлен в 1?
x%2
вместо просто x
здесь просто потому, что проблема воспроизводима с ним.
Я использую openjdk 6 на Linux x64.
3 ответа
Вот что я думаю: поскольку b не является окончательным, компилятор может переупорядочивать операции так, как ему нравится, верно? Так что это, по сути, проблема переупорядочения и, как следствие, небезопасная проблема публикации. Обозначение переменной как окончательной решит проблему.
Более или менее, это тот же пример, который приведен здесь в документации по модели памяти Java.
Реальный вопрос в том, как это возможно. Я также могу рассуждать здесь (поскольку я понятия не имею, как компилятор будет переупорядочивать), но, возможно, ссылка на B записывается в основную память (где она видна другому потоку) ДО того, как произойдет запись в x. Между этими двумя операциями происходит чтение, таким образом, нулевое значение
Часто соображения, связанные с параллелизмом, фокусируются на ошибочных изменениях состояния или на тупиках. Но видимость состояния из разных потоков одинаково важна. В современном компьютере есть много мест, где можно кэшировать состояние. В регистрах кэш-память L1 на процессоре, кэш-память L2 между процессором и памятью и т. Д. JIT-компиляторы и модель памяти Java предназначены для использования преимуществ кэширования всякий раз, когда это возможно или допустимо, поскольку это может ускорить процесс.
Это также может дать неожиданные и нелогичные результаты. Я считаю, что происходит в этом случае.
Когда создается экземпляр B, переменная экземпляра x кратко устанавливается на 0, а затем устанавливается любое значение, которое было передано в конструктор. В этом случае: 1. Если другой поток пытается прочитать значение x, он может увидеть значение 0, даже если x уже был установлен в 1. Он может видеть устаревшее кэшированное значение.
Чтобы убедиться, что актуальное значение x видно, есть несколько вещей, которые вы можете сделать. Вы можете сделать x volatile или защитить чтение x с помощью синхронизации на экземпляре B (например, добавив synchronized getX()
метод). Вы могли бы даже изменить х с int на java.util.concurrent.atomic.AtomicInteger
,
Но, безусловно, самый простой способ исправить проблему - сделать x final. В любом случае, это никогда не изменится в течение жизни B. Java предоставляет специальные гарантии для конечных полей, и одна из них заключается в том, что после завершения работы конструктора конечный набор полей в конструкторе будет виден любому другому потоку. То есть никакой другой поток не увидит устаревшее значение для этого поля.
Создание неизменяемых полей также имеет много других преимуществ, но это отличная возможность.
См. Также " Атомность, видимость и порядок " Джереми Мэнсона. Особенно часть, где он говорит:
(Примечание: когда я говорю синхронизация в этом посте, я на самом деле не имею в виду блокировку. Я имею в виду все, что гарантирует видимость или упорядочение в Java. Это может включать в себя конечные и изменяемые поля, а также инициализацию класса, запуск и присоединение потока и все виды других хороших вещей.)
Мне кажется, что на Bx может быть условие гонки, такое, что может существовать доля секунды, в которой Bx был создан и Bx=0 до this.x = x в конструкторе B. Серия событий будет что-то вроде:
B is created (x defaults to 0) -> Constructor is ran -> this.x = x
Ваш поток обращается к Bx через некоторое время после его создания, но до запуска конструктора. Однако я не смог воссоздать проблему локально.