Неинициализированный объект просочился в другой поток, несмотря на то, что код явно не протекал?

Посмотрите эту простую программу на 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.

Ответ 1

Вот что я думаю: потому что b не является окончательным, компилятор может свободно переупорядочивать операции по своему усмотрению, не так ли? Таким образом, это принципиально проблема переупорядочения и, как следствие, проблема небезопасной публикации. Обозначение переменной как окончательной будет устранять проблему.

Более или менее, это тот же пример, что и здесь, в Документах по модели памяти Java.

Реальный вопрос в том, как это возможно. Я также могу рассуждать здесь (поскольку я понятия не имею, как компилятор будет переупорядочивать), но, возможно, ссылка на B записывается в основную память (где она видна для другого потока) ПЕРЕД записью в x. Между этими двумя операциями происходит чтение, поэтому нулевое значение

Ответ 2

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

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

Когда создается экземпляр B, переменная экземпляра x кратковременно устанавливается в 0 перед тем, как будет установлено любое значение, переданное конструктору. В этом случае 1. Если другой поток пытается прочитать значение x, он может видеть значение 0, даже если x уже установлен в 1. Он может видеть устаревшее кешированное значение.

Чтобы убедиться, что отображается последнее значение x, вы можете сделать несколько вещей. Вы можете сделать x volatile, или вы можете защитить чтение x с синхронизацией в экземпляре B (например, добавив метод synchronized getX()). Вы даже можете изменить x от int до java.util.concurrent.atomic.AtomicInteger.

Но самый простой способ исправить проблему - сделать x final. В любом случае, он никогда не изменится в течение жизни B. Java делает специальные гарантии для конечных полей, и один из них заключается в том, что после завершения конструктора окончательное поле, заданное в конструкторе, будет видимым для любого другого потока. То есть ни один другой поток не увидит устаревшее значение для этого поля.

Создание неизменяемых полей имеет много других преимуществ, но это замечательно.

См. также Атомарность, видимость и порядок от Джереми Мэнсона. В частности, он говорит:

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

Ответ 3

Мне кажется, что на B.x может быть условие гонки, так что может существовать разделенная секунда, где Bx была создана, и B.x = 0 до этого. x = x в B-конструкторе. Серия событий будет выглядеть примерно так:

B is created (x defaults to 0) -> Constructor is ran -> this.x = x

Ваш поток обращается к B.x через некоторое время после его создания, но до запуска конструктора. Однако я не смог воссоздать проблему локально.