Связаны ли статические переменные между потоками?

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

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

По сути, он сказал, что возможно, что 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;  
    }  
}

Ответ 1

Нет проблемы видимости, характерной для статических переменных. Существует проблема видимости, навязанная моделью памяти JVM. Здесь рассказывается о модели памяти и о том, как записи становятся видимыми для потоков. Вы не можете рассчитывать на изменения, которые один поток становится видимым для других потоков своевременно (на самом деле JVM не обязана делать эти изменения видимыми для вас вообще), если вы не установите происходит до отношения, здесь цитата из этой ссылки (приведенная в комментарии Джеда Уэсли-Смита):

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

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

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

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

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

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

Ответ 2

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

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

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

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

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

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

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

...

thread model

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

Ответ 3

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

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

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

Ответ 4

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

Ответ 5

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

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

Ответ 6

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

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

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

private static boolean ready;  
private static int number;  

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

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