Java: насколько изменчиво гарантируется видимость данных в этом фрагменте кода?

Class Future
{
    private volatile boolean ready;
    private Object data;
    public Object get()
    {
        if(!ready) return null;
        return data;
    }

    public synchronized void setOnce(Object o)
    {
        if(ready) throw...;
        data = o;
        ready = true;
    }
}

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

Из своего обучения я знаю:

  1. volatile гарантирует, что каждое чтение / запись будет происходить в памяти, а не только в кеше или регистрах;
  2. volatile обеспечивает переупорядочение: то есть в методе setOnce () data = o может быть запланировано только после if (ready) throw ... и до ready = true; это гарантия того, что если в get () ready = true, данные должны быть отключены.

Мое замешательство

  1. возможно ли, что когда поток 1 находится в setOnce (), достигнет точки после data = o; до готовности = истина; В то же время поток 2 запускает get (), готовность к чтению ложна и возвращает ноль. И thead 1 продолжает готово = правда. В этом сценарии поток 2 не видел новых «данных», хотя данным было присвоено новое значение в потоке 1.

  2. get () не синхронизируется, это означает, что синхронизированная блокировка не может защитить setOnce (), поскольку поток 1 вызывает get (), которому не нужно получать блокировку для доступа к готовой переменной, data. Таким образом, потоку не гарантируется, что оно увидит последнее значение данных. Под этим я подразумеваю, что блокировка гарантирует только видимость между синхронизированными блоками. Несмотря на то, что один поток выполняет синхронизированный блок setOnce (), другой поток все еще может перейти в get () и получить доступ к готовым и данным без блокировки и может увидеть старое значение этих переменных.

  3. в get (), если ready = true, данные должны быть o? Я имею в виду, что эта ветка гарантированно увидит видимость данных? Я думаю, что данные не являются изменчивыми и не синхронизируются с get (). Может ли этот поток видеть старое значение в кеше?

Спасибо!


person Christy Lin    schedule 11.11.2015    source источник
comment
Что это за язык? Джава?   -  person David Schwartz    schedule 11.11.2015
comment
Кроме того, ваш 1 в основном ложный. Ключевое слово volatile имеет отношение к видимости памяти, а не кэшам. Кеши обрабатываются оборудованием когерентности кеша. И это был бы явно ужасный дизайн, который никто бы не использовал - память слишком медленная, чтобы использовать ее таким образом.   -  person David Schwartz    schedule 11.11.2015
comment
@DavidSchwartz в Java переменная может храниться в кеш-памяти. Кэш-память L1 и L2 невидима для отдельных потоков, при использовании энергозависимой значение сохраняется в основной памяти или в кеш-памяти L3 (основная память и кэш-память L3 совместно используются потоками). Дополнительная информация   -  person Velko Georgiev    schedule 11.11.2015
comment
@VelkoGeorgiev Это абсолютно неверно. Кеши работают не так. Это распространенный миф, но это всего лишь миф. Ключевое слово volatile не имеет никакого отношения к этим кешам. Доступ к volatile может оставаться полностью в кэше L1 без проблем. (К сожалению, статья, на которую вы ссылаетесь, повторяет миф.)   -  person David Schwartz    schedule 11.11.2015
comment
@VelkoGeorgiev Я оставил комментарии к статье. Это бесит, когда кто-то, кто так неправильно понимает важный вопрос, пытается научить этому других людей.   -  person David Schwartz    schedule 11.11.2015
comment
@DavidSchwartz Я не согласен, проверьте эту ссылку StackOverflow. Изменяемая переменная: если два потока (предположим, что t1 и t2) обращаются к одному и тому же объекту и обновляют переменную, объявленную как изменчивую, тогда это означает, что t1 и t2 могут создавать свой собственный локальный кеш объекта, за исключением переменной, объявленной как изменчивой.   -  person Velko Georgiev    schedule 11.11.2015
comment
@VelkoGeorgiev К сожалению, это тоже немного неверно. Я также прокомментировал этот ответ. Это досадно распространенное заблуждение.   -  person David Schwartz    schedule 11.11.2015
comment
Итак, если это неправильно, почему, когда вы делаете цикл while с флагом, а флаг НЕ является изменчивым, когда вы обновляете флаг из другого потока, цикл продолжает работать?   -  person Velko Georgiev    schedule 11.11.2015
comment
@VelkoGeorgiev Может ошибаться по любой причине. То есть нет ничего, что гарантирует, что он будет работать, поэтому он может вообще выйти из строя по любой причине. Наиболее распространенная причина, по которой он не работает на типичных современных компьютерах, заключается в том, что JVM оптимизирует инструкции ЦП, которые будут извлекать переменную из кеша, вместо того, чтобы сохранять ее в регистре. То есть он терпит неудачу из-за оптимизации JVM (которая volatile отключает), не имеет ничего общего с кешами ЦП. (Это действительно то, что действительно понимают очень, очень немногие люди.)   -  person David Schwartz    schedule 11.11.2015
comment
Хорошо, посмотрите эту цитату из книги «Параллелизм Java на практике» (это фотография) ССЫЛКА ЗДЕСЬ   -  person Velko Georgiev    schedule 11.11.2015
comment
Эта цитата абсолютно верна. Но это немного вводит в заблуждение, потому что кеши L1 / L2 / L3 на современных процессорах имеют согласованность аппаратного кеша и не скрывают вещи от других процессоров, и поэтому не имеют ничего общего с volatile. У некоторых ЦП есть буферы предварительной выборки или буферы записи, которые скрывают что-то от других процессоров, и volatile их нужно обойти.   -  person David Schwartz    schedule 11.11.2015
comment
Давайте продолжим это обсуждение в чате.   -  person Velko Georgiev    schedule 11.11.2015
comment
@VelkoGeorgiev Спасибо за ответ. Но не могли бы вы помочь мне с моими тремя вопросами? Меня это действительно сбивает с толку, и я хочу знать, что правильно.   -  person Christy Lin    schedule 11.11.2015
comment
@DavidSchwartz Спасибо. То же самое прошу помощи по моим трем вопросам.   -  person Christy Lin    schedule 11.11.2015


Ответы (1)


volatile гарантирует, что каждое чтение / запись будет происходить в памяти, а не только в кеше или регистрах;

Неа. Это просто гарантирует, что он будет виден другим потокам. На современном оборудовании это не требует доступа к памяти. (Что хорошо, основная память медленная.)

volatile обеспечивает переупорядочение: то есть в методе setOnce () data = o может быть запланировано только после if (ready) throw ... и до ready = true; это гарантия того, что если в get () ready = true, данные должны быть отключены.

Это правильно.

возможно ли, что когда поток 1 находится в setOnce (), достигнет точки после data = o; до готовности = истина; В то же время поток 2 запускает get (), готовность к чтению ложна и возвращает ноль. И thead 1 продолжает готово = правда. В этом сценарии поток 2 не видел новых «данных», хотя данным было присвоено новое значение в потоке 1.

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

get () не синхронизируется, это означает, что синхронизированная блокировка не может защитить setOnce (), поскольку поток 1 вызывает get (), которому не нужно получать блокировку для доступа к готовой переменной, data. Таким образом, потоку не гарантируется, что оно увидит последнее значение данных. Под этим я подразумеваю, что блокировка гарантирует только видимость между синхронизированными блоками. Несмотря на то, что один поток выполняет синхронизированный блок setOnce (), другой поток все еще может перейти в get () и получить доступ к готовым и данным без блокировки и может увидеть старое значение этих переменных.

Нет. И если бы это было правдой, синхронизацию было бы почти невозможно использовать. Например, распространенным шаблоном является создание объекта, затем получение блокировки для коллекции и добавление объекта в коллекцию. Это не сработало бы, если бы получение блокировки коллекции не гарантировало, что записи, участвующие в создании объекта, были видимы.

в get (), если ready = true, данные должны быть o? Я имею в виду, что эта ветка гарантированно увидит видимость данных? Я думаю, что данные не являются изменчивыми и не синхронизируются с get (). Может ли этот поток видеть старое значение в кеше?

Операция volatile в Java определена таким образом, что поток, который видит изменение одного, гарантированно видит, что вся остальная память изменяет поток, который внес это изменение, сделанное до того, как он внес изменение, увиденное потоком. Это неверно для других языков (например, C или C ++). Это может сделать нестабильные Java-файлы более дорогими на некоторых платформах, но, к счастью, не на типичных платформах.

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

person David Schwartz    schedule 11.11.2015
comment
Во-первых, я очень признателен, что вы не торопились и помогли мне с этим подробным ответом! - person Christy Lin; 11.11.2015
comment
Вопрос 1 решен. Но что касается второго вопроса, я прочитал это из документа. Когда поток освобождает внутреннюю блокировку, между этим действием и любым последующим получением той же блокировки устанавливается связь «происходит раньше». вот что меня смущает. Я думаю о твоем примере по поводу коллекции. Что произойдет, если t1 внесет некоторые изменения в коллекцию с помощью несинхронизированного метода change (), в то время как ваш синхронизированный add () выполняется t2. t1 не будет блокироваться правильно, поскольку это синхронизированный метод. t1 будет видеть добавленную коллекцию или нет или другое? - person Christy Lin; 11.11.2015
comment
@ChristyLin Это отношение происходит до того, как это означает, что все, что этот поток сделал до этого, снимает блокировку, будет видно любому потоку, который позже получит ту же самую блокировку. Чтобы это работало, вам потребуется синхронизировать все обращения к коллекции. В Java volatile устанавливает ту же взаимосвязь - если поток изменяет любую volatile переменную, поток, который видит это изменение, также увидит все изменения, сделанные до этого. Опять же, это характерно для Java. - person David Schwartz; 11.11.2015
comment
Теперь я понимаю, какие эффекты приносит volatile. Но предположим, что класс коллекции не включает никаких изменчивых переменных, все, что у него есть, это синхронизированные add () и несинхронизированные change (), тогда не гарантируется, что add () будет видимым для change (), верно? Если мне нужно, чтобы эти два метода были видны друг другу, я должен синхронизировать эти два метода, верно? - person Christy Lin; 11.11.2015
comment
@ChristyLin Да, это правильно. Вы должны делать что-то в каждой операции, чтобы эти две вещи устанавливали отношения до и после. - person David Schwartz; 11.11.2015
comment
Большое спасибо! Теперь все мое замешательство ушло. Так счастлив! Хорошего дня! - person Christy Lin; 11.11.2015
comment
@DavidSchwartz Я пытаюсь понять ваш Это связано с видимостью, а не с кешированием. Большинство кешей обеспечивают полную видимость кеша. Я разместил вопрос здесь. Меня беспокоит видимость ОДНОЙ переменной, а не проблема переупорядочения нескольких переменных. Мой вопрос: поскольку протокол согласования кеша (MESI) может гарантировать видимость одной переменной, зачем нам нужен volatile, чтобы обеспечить видимость для других потоков? - person hangyuan; 18.07.2021
comment
@hangyuan Потому что так сказано в соответствующих стандартах. ЦП не обязательно должен иметь протокол согласования кеша, и, поскольку разработчики вашего компилятора знают об этом, им разрешено выполнять оптимизацию, которая нарушается вашим предположением, что он есть в ЦП. Это все вокруг вверх. ЦП, имеющий протокол когерентности кеша, позволяет компилятору выполнять очень эффективную оптимизацию, как и тот факт, что вам не разрешено полагаться на протокол когерентности кеша (только компилятор). - person David Schwartz; 19.07.2021