Распространяются ли статические переменные между потоками?

95

Мой учитель в классе Java верхнего уровня по многопоточности сказал то, в чем я не был уверен.

Он заявил, что следующий код не обязательно обновит readyпеременную. По его словам, два потока не обязательно совместно используют статическую переменную, особенно в том случае, когда каждый поток (основной поток по сравнению с ReaderThread) работает на своем собственном процессоре и, следовательно, не использует одни и те же регистры / кеш / и т. Д. И один процессор. не будет обновлять другой.

По сути, он сказал, что возможно, что readyобновляется в основном потоке, но НЕ в ReaderThread, так что ReaderThreadцикл будет бесконечным.

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

Я подумал, что, возможно, не гарантируется, что статическая переменная обновляется между потоками, но это кажется мне очень странным для Java. Устраняет ли readyэту проблему создание volatile?

Он показал этот код:

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;  
    }  
}
Dontocsata
источник
Видимость нелокальных переменных не зависит от того, являются ли они статическими переменными, полями объекта или элементами массива, все они имеют одни и те же соображения. (Проблема состоит в том, что элементы массива нельзя сделать нестабильными.)
Паоло Эберманн
1
спросите своего учителя, за какую архитектуру, по его мнению, можно было бы увидеть «0». Тем не менее, теоретически он прав.
bestsss
4
@bestsss Задав такой вопрос, учитель узнал бы, что он упустил весь смысл того, что он говорил. Дело в том, что компетентные программисты понимают, что гарантировано, а что нет, и не полагаются на то, что не гарантируется, по крайней мере, не без точного понимания того, что не гарантируется и почему.
Дэвид Шварц
Они распределяются между всем, что загружено одним и тем же загрузчиком классов. Включая темы.
Маркиз Лорн,
Ваш учитель (и принятый ответ) правы на 100%, но я отмечу, что это случается редко - это та проблема, которая будет скрываться годами и проявлять себя только тогда, когда она будет наиболее опасной. Даже короткие тесты, пытающиеся выявить проблему, как правило, ведут себя так, как будто все в порядке (вероятно, потому, что у JVM нет времени на большую оптимизацию), так что это действительно хорошая проблема, о которой следует знать.
Билл К.

Ответы:

75

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

Модель памяти JVM создает проблему с видимостью. В статье рассказывается о модели памяти и о том, как записи становятся видимыми для потоков . Вы не можете рассчитывать на изменения, которые один поток делает видимыми для других потоков своевременно (на самом деле JVM не обязана делать эти изменения видимыми для вас вообще, в любой период времени), если вы не установите связь «произошло раньше» .

Вот цитата из этой ссылки (из комментария Джеда Уэсли-Смита):

Глава 17 Спецификации языка Java определяет отношение «происходит раньше» для операций с памятью, таких как чтение и запись общих переменных. Результаты записи одним потоком гарантированно будут видны для чтения другим потоком только в том случае, если операция записи происходит до операции чтения. Синхронизированные и изменчивые конструкции, а также методы Thread.start () и Thread.join () могут формировать отношения «происходит раньше». В частности:

  • Каждое действие в потоке происходит перед каждым действием в этом потоке, которое происходит позже в порядке выполнения программы.

  • Разблокировка (синхронизированный выход блока или метода) монитора происходит перед каждой последующей блокировкой (синхронизированным блоком или входом метода) того же самого монитора. И поскольку отношение происходит до того, как оно является транзитивным, все действия потока до разблокировки происходят до всех действий, следующих за любой блокировкой потока, который отслеживает.

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

  • Вызов для запуска в потоке происходит до любого действия в запущенном потоке.

  • Все действия в потоке происходят до того, как любой другой поток успешно вернется из соединения в этом потоке.

Натан Хьюз
источник
3
На практике «своевременно» и «когда-либо» являются синонимами. Было бы очень возможно, что приведенный выше код никогда не завершится.
ДЕРЕВО
4
Также это демонстрирует другой антипаттерн. Не используйте volatile для защиты более чем одной части общего состояния. Здесь число и готовность - это две части состояния, и для их последовательного обновления / чтения требуется фактическая синхронизация.
ДЕРЕВО
5
То, что в конечном итоге станет видимым, неверно. Без каких-либо явных отношений «происходит до» нет гарантии, что какая-либо запись когда-либо будет замечена другим потоком, поскольку JIT вполне имеет право наложить чтение в регистр, и тогда вы никогда не увидите никаких обновлений. Любая возможная нагрузка - это удача, и на нее нельзя полагаться.
Джед Уэсли-Смит
2
"если вы не используете ключевое слово volatile или не синхронизируете". следует читать «если между автором и читателем не существует соответствующих отношений« произошло раньше »» и эту ссылку: download.oracle.com/javase/6/docs/api/java/util/concurrent/…
Джед Уэсли-Смит,
2
@bestsss хорошо заметны. К сожалению, ThreadGroup во многих отношениях не работает.
Джед Уэсли-Смит,
37

Он говорил о видимости, и не следует понимать ее слишком буквально.

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

В этой статье представлена ​​точка зрения, которая согласуется с тем, как он представил информацию:

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

  • Каждый поток в Java размещается в отдельном пространстве памяти (это явно неверно, так что терпите меня).

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

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

...

модель резьбы

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

Берт Ф
источник
12

В основном это правда, но на самом деле проблема более сложная. На видимость общих данных могут влиять не только кэши ЦП, но и выполнение инструкций вне очереди.

Поэтому Java определяет модель памяти , в которой говорится, при каких обстоятельствах потоки могут видеть согласованное состояние общих данных.

В вашем конкретном случае добавление volatileгарантирует видимость.

axtavt
источник
8

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

И теоретически записи, сделанные другим потоком, могут выглядеть в другом порядке, если только переменные не объявлены volatileили записи явно не синхронизированы.

бизиклоп
источник
4

В пределах одного загрузчика классов всегда используются общие статические поля. Для явной привязки данных к потокам вам нужно использовать такое средство, как ThreadLocal.

Кирк Уолл
источник
2

Когда вы инициализируете статическую переменную примитивного типа, java по умолчанию присваивает значение статическим переменным

public static int i ;

когда вы определяете переменную таким образом, значение по умолчанию i = 0; вот почему есть возможность получить 0. тогда основной поток обновляет значение boolean ready до true. поскольку ready - это статическая переменная, основной поток и другой поток ссылаются на тот же адрес памяти, поэтому переменная ready изменяется. поэтому вторичный поток выходит из цикла while и выводит значение. при печати значение инициализированное значение number равно 0., если процесс потока прошел цикл while до обновления номера переменной основного потока. тогда есть возможность напечатать 0

NuOne
источник
-2

@dontocsata, ты можешь вернуться к своему учителю и немного поучить его :)

несколько заметок из реального мира и независимо от того, что вы видите или что вам говорят. ОБРАТИТЕ ВНИМАНИЕ, что слова ниже относятся к этому конкретному случаю в точном указанном порядке.

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

private static boolean ready;  
private static int number;  

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

Запущенный поток ReaderThreadбудет поддерживать процесс, поскольку он не является демоном! Таким образом, readyи numberбудут сброшены вместе (или число до, если произойдет переключение контекста), и в этом случае нет реальной причины для переупорядочения, по крайней мере, я даже не могу об этом подумать. Чтобы увидеть что угодно, вам понадобится нечто действительно странное 42. Опять же, я предполагаю, что обе статические переменные будут в одной строке кеша. Я просто не могу представить строку кеша длиной 4 байта ИЛИ JVM, которая не будет назначать их в непрерывной области (строка кеша).

bestsss
источник
3
@bestsss, хотя сегодня это все верно, для истины он полагается на текущую реализацию JVM и аппаратную архитектуру, а не на семантику программы. Это означает, что программа все еще не работает, хотя может работать. Легко найти тривиальный вариант этого примера, который действительно дает сбой указанным образом.
Джед Уэсли-Смит
1
Я сказал, что он не соответствует спецификации, однако, как учитель, по крайней мере, нашел подходящий пример, который может действительно потерпеть неудачу в какой-то стандартной архитектуре, так что пример вроде бы реален.
bestsss
6
Возможно, худший совет по написанию поточно-безопасного кода, который я видел.
Лоуренс Дол
4
@Bestsss: Простой ответ на ваш вопрос: «Кодируйте спецификацию и документ, а не побочные эффекты вашей конкретной системы или реализации». Это особенно важно для платформы виртуальных машин, которая не зависит от базового оборудования.
Лоуренс Дол
1
@Bestsss: Учителя указывают на то, что (а) код может работать, когда вы его тестируете, и (б) код не работает, потому что он зависит от работы оборудования, а не от гарантий спецификации. Дело в том, что вроде нормально, но не нормально.
Лоуренс Дол