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

Учитывая следующий фрагмент кода Java 8, который превращает Поставщика в поставщика кеширования, который ссылается на нижележащего Поставщика только один раз и возвращает кешированное значение впредь:

@AllArgsConstructor
private final static class SupplierMemoBox<T> {
  private Supplier<T> supplier;
  private T value;
}

public static <T> Supplier<T> memoizing(@Nonnull final Supplier<T> supplier) {
  Objects.requireNonNull(supplier, "'supplier' must not be null");
  final SupplierMemoBox<T> box = new SupplierMemoBox<>(supplier, null);
  return () -> {
    if (box.supplier != null) {
      box.value = box.supplier.get();
      box.supplier = null;
    }
    return box.value;
  };
}

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

Чтобы сделать этот поток безопасным, можно синхронизировать boxобъект следующим образом:

public static <T> Supplier<T> memoizing(@Nonnull final Supplier<T> supplier) {
  Objects.requireNonNull(supplier, "'supplier' must not be null");
  final SupplierMemoBox<T> box = new SupplierMemoBox<>(supplier, null);
  return () -> {
    synchronized (box) {
      if (box.supplier != null) {
        box.value = box.supplier.get();
        box.supplier = null;
      }
      return box.value;
    }
  };
}

Теперь мне интересно, так SupplierMemoBox.supplierкак не отмечено, volatileможет ли случиться, что поток, входящий в монитор, boxчитает устаревшую переменную box.supplierили предотвращает ее синхронизацию на boxобъекте (т. Е. Делает ли доступ к полям членов безопасным?) , Или есть какой-то другой обман, который делает его безопасным, т. Е. Все чтения, происходящие из потока, который вошел в монитор, гарантированно не будут устаревать? Или это вообще не безопасно?

java,multithreading,java-memory-model,

3

Ответов: 2


2 принят

Безопасность определяется переходной ситуацией до отношения следующим образом:

17.4.5. Бывает-до заказа

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

Если мы имеем два действия x и y , пишем hb (x, y), чтобы указать, что x происходит до y .

  • Если x и y - действия одного и того же потока, а x - до y в программном порядке, то hb (x, y) .
  • Там является случается, перед тем краем с конца конструктор объекта до начала финализации (A§12.6) для этого объекта.
  • Если действие x синхронизируется со следующим действием y , то мы также имеем hb (x, y) .
  • Если hb (x, y) и hb (y, z) , то hb (x, z) .

В предыдущем разделе

Действие разблокировки на мониторе m синхронизируется со всеми последующими действиями блокировки на m (где «последующее» определяется в соответствии с порядком синхронизации).

что позволяет заключить, что спецификация также говорит явно:

Из приведенных выше определений следует, что:

  • Разблокировка на мониторе происходит до каждой последующей блокировки на этом мониторе.

a € |

Мы можем применить эти правила к вашей программе:

  • Первый поток назначая nullк box.supplierделает это перед выпуском монитора (оставляя synchronized (box) { a€¦ }) блок. Это упорядочено внутри самой нити из-за первой пули («Если x и y являются действиями одного и того же потока, а x идет до y в порядке выполнения программы, то hb (x, y) a €?)
  • Второй поток, впоследствии приобретающий один и тот же монитор (входящий в synchronized (box) { a€¦ }блок), имеет отношение « до отношения к первому выпуску монитора» (как было сказано выше: «Разблокировка на мониторе происходит до того, как каждый последующий замок этот монитор ...)
  • Чтение второй строки box.supplierпеременной в synchronizedблоке снова упорядочивается с приобретением монитора из-за порядка программы («Если x и y являются действиями одного и того же потока, а x идет до y в порядке выполнения программы, тогда hb (x, y) a €?)
  • Теперь три приведенных выше соотношения можно объединить из-за последнего правила: «Если hb (x, y) и hb (y, z) , то hb (x, z) a. Это транзитивность позволяет сделать вывод , что существует поточно упорядочение между записью о nullк box.supplierи последующего чтения в box.supplierпеременной, как в synchronizedблоке на том же объекте .

Обратите внимание, что это не имеет никакого отношения к тому, что box.supplierявляется переменной-членом объекта, для которого мы используем synchronized. Важным аспектом является то, что оба потока используют один и тот же объект synchronizedдля установления порядка, который взаимодействует с другими действиями из-за правила транзитивности.

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

В качестве встречного примера рассмотрим следующий код:

List<SomeType> list = a€¦;

Тема 1:

synchronized(list) {
    list.set(1, new SomeType(a€¦));
}

Тема 2:

List<SomeType> myList = list.subList(1, 2);

synchronized(list) {
    SomeType value = myList.get(0);
    // process value
}

Здесь важно, чтобы Thread 2 не использовался myListдля синхронизации, несмотря на то, что мы используем его для доступа к контенту, так как это другой объект. В Thread 2 все еще должен использоваться исходный экземпляр synchronizedList для синхронизации. Это реальная проблема ListList, чья документация демонстрирует ее с примером доступа к списку через boxэкземпляр, который по-прежнему должен быть защищен путем синхронизации на Listэкземпляре.


1

Да, если вы изменяете synchronized (box) { }свойства объекта только внутри, box = someValueто это потокобезопасно. Но будьте осторожны, чтобы не переназначить все значение объекта, т.е. box.supplierне является потокобезопасным (многие люди делают эту ошибку по неизвестной причине).

Маркировка , volatileкак synchronizedбы помочь в том случае , если вы хотите сделать неблокируемую модификацию к нему (т.е. без synchronizedили аналогичного замка).

Java, многопоточный, Java-памяти модель,
Похожие вопросы