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

Учитывая следующий фрагмент кода 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 (т. е. делает ли этот доступ все к полям-членам безопасным?).Или есть какой-то другой обман, который делает его безопасным, то есть все чтения, происходящие из потока, который вошел в монитор, гарантированно не устареют?Или это вообще не безопасно?

Ответы [ 2 ]

0 голосов
/ 12 июля 2018

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

17.4.5.Порядок "до"

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

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

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

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

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

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

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

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

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

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

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

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

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

List<SomeType> list = …;

Поток 1:

synchronized(list) {
    list.set(1, new SomeType(…));
}

Тема 2:

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

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

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

0 голосов
/ 22 мая 2018

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

Пометка box.supplier как volatile поможет вслучай, если вы хотите внести в него неблокирующую модификацию (т.е. без synchronized или подобной блокировки).

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