Является ли явная блокировка автоматически обеспечивающей видимость памяти?

Пример кода:

class Sample{
    private int v;
    public void setV(){
        Lock a=new Lock();
        a.lock();
        try{
            v=1;
        }finally{
            a.unlock();
        }
    }
    public int getV(){
        return v;
    }
}

Если у меня есть поток, постоянно вызывайте getV, и я просто делаю setV один раз в другом потоке. Является ли этот поток чтения гарантированным, чтобы увидеть новое значение сразу после написания? Или мне нужно сделать "V" volatile или AtomicReference?

Если ответ отрицательный, тогда я должен изменить его на:

class Sample{
    private int v;
    private Lock a=new Lock();
    public void setV(){
        a.lock();
        try{
            v=1;
        }finally{
            a.unlock();
        }
    }
    public int getV(){
        a.lock();
        try{
            int r=v;
        }finally{
            a.unlock();
        }
        return r;
    }
}

Ответ 1

Из документации:

Все реализации блокировки должны обеспечивать реализацию той же семантики синхронизации памяти, которая обеспечивается встроенной блокировкой монитора:

  • Успешная операция блокировки действует как успешное действие действия MonitorEnter
  • Успешная операция разблокировки действует как успешное действие monitorExit

Если вы используете Lock в обоих потоках (т.е. чтение и запись), поток чтения увидит новое значение, потому что monitorEnter очищает кеш. В противном случае вам нужно объявить переменную volatile для принудительного чтения из памяти в поток чтения.

Ответ 2

Согласно закону Брайана...

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

Итак, было бы уместно синхронизировать как сеттер, так и геттер......

Или

Используйте AtomicInteger.incrementAndGet() вместо , если вы хотите избежать блока lock-unlock (т.е. синхронизированного блока)

Ответ 3

Если у меня есть поток, постоянно вызывайте getV, и я просто делаю setV один раз в другой поток. Является ли этот поток чтения гарантированным, чтобы увидеть новое значение сразу после написания?

НЕТ, поток чтения может просто прочитать собственную копию (автоматически кэшированную центральным процессором, на котором работает поток чтения) значения v, и, таким образом, не получить последнее значение.

Или мне нужно сделать "V" volatile или AtomicReference?

ДА, оба они работают.

Выполнение v volatile просто останавливает процессорное ядро ​​из кеширования v, то есть каждая операция чтения/записи переменной v должна обращаться к основной памяти, которая медленнее (примерно в 100 раз медленнее, чем чтение из кеша L1, Подробнее см. interactive_latency

Использование v = new AtomicInteger() работает, потому что AtomicInteger использует private volatile int value; внутренне, чтобы обеспечить видимость.

И он также работает, если вы используете блокировку (объект Lock или используйте блок или метод synchronized) при чтении и записи потока (как это делает второй сегмент кода), потому что (согласно Второй выпуск спецификации виртуальной машины Java ®, раздел 8.9)

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

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

P.S. AtomicXXX также предоставляют операции CAS (Compare and Swap), которые полезны для доступа mutlthread.

P.P.S. Спецификация jvm по этой теме не изменилась со времени Java 6, поэтому они не включены в спецификацию jvm для java 7, 8 и 9.

P.P.P.S. Согласно этой статье, кэши CPU всегда согласованы, независимо от каждого основного представления. Ситуация в вашем вопросе вызвана "Буферами упорядочения памяти", в которых инструкции store и load (которые используются для записи и чтения данных из памяти, соответственно) могут быть переупорядочены для производительности. В деталях буфер позволяет инструкции load опережать более старую команду store, что в точности вызывает проблему. Однако, на мой взгляд, это сложнее понять, поэтому "кеш для разных ядер (как это сделали спецификации JVM)" может быть лучшей концептуальной моделью.

Ответ 4

Вы должны сделать volatile или AtomicInteger. Это гарантирует, что поток чтения в конце концов увидит изменение и достаточно близко к "прямо после" для большинства целей. И технически вам не нужен Lock для простого атомного обновления, подобного этому. Ознакомьтесь с API AtomicInteger. set(), compareAndSet() и т.д. все будут задавать значение, которое должно быть видимым, путем чтения потоков атомарно.

Ответ 5

Явные блокировки, synchronized, атомная ссылка и volatile, все обеспечивают видимость памяти. Блокировка и synchronized делают это для кодового блока, который они окружают, и для атомной ссылки и volatile для указанной переменной. Однако для правильной работы видимости оба метода чтения и записи должны быть защищены одним и тем же объектом блокировки.

Это не будет работать в вашем случае, потому что ваш метод getter не проецируется блокировкой, которая защищает метод setter. Если вы внесете изменения, он будет работать по мере необходимости. Также будет работать только объявление переменной volatile или AtomicInteger или AtomicReference<Integer>.