Есть ли у меня проблема с переупорядочением и связано ли это с побегом ссылки?

У меня есть этот класс, в котором я кэширую экземпляры и клонирую их (данные изменяются), когда я их использую.

Интересно, могу ли я столкнуться с проблемой повторного заказа с этим.

Я просмотрел этот ответ и JLS, но я еще не уверен.

public class DataWrapper {
    private static final ConcurrentMap<String, DataWrapper> map = new ConcurrentHashMap<>();
    private Data data;
    private String name;

    public static DataWrapper getInstance(String name) {
        DataWrapper instance = map.get(name);
        if (instance == null) {
            instance = new DataWrapper(name);
        }
        return instance.cloneInstance();
    }

    private DataWrapper(String name) {
        this.name = name;
        this.data = loadData(name);  // A heavy method
        map.put(name, this);  // I know
    }

    private DataWrapper cloneInstance() {
        return new DataWrapper(this);
    }

    private DataWrapper(DataWrapper that) {
        this.name = that.name;
        this.data = that.data.cloneInstance();
    }
}

Мое мнение: среда выполнения может изменить порядок операторов в конструкторе и опубликовать текущий экземпляр DataWrapper (поместить на карту) перед инициализацией объекта data. Второй поток читает экземпляр DataWrapper из карты и видит нулевое поле data (или частично созданное).

Это возможно? Если да, то это только из-за побега ссылки?

Если нет, не могли бы вы объяснить мне, как рассуждать о непротиворечивости происходит до в более простых терминах?

Что, если бы я сделал это:

public class DataWrapper {
    ...
    public static DataWrapper getInstance(String name) {
        DataWrapper instance = map.get(name);
        if (instance == null) {
            instance = new DataWrapper(name);
            map.put(name, instance);
        }
        return instance.cloneInstance();
    }
    
    private DataWrapper(String name) {
        this.name = name;
        this.data = loadData(name);     // A heavy method
    }
    ...
}

Это все еще склонно к той же проблеме?

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

РЕДАКТИРОВАТЬ:

Что, если бы поля имени и данных были окончательными или изменчивыми?

public class DataWrapper {
    private static final ConcurrentMap<String, DataWrapper> map = new ConcurrentHashMap<>();
    private final Data data;
    private final String name;
    ... 
    private DataWrapper(String name) {
        this.name = name;
        this.data = loadData(name);  // A heavy method
        map.put(name, this);  // I know
    }
    ...
}

Это все еще небезопасно? Насколько я понимаю, гарантии безопасности инициализации конструктора применяются только в том случае, если ссылка не сбежала во время инициализации. Я ищу официальные источники, подтверждающие это.


person Gurwinder Singh    schedule 05.10.2017    source источник
comment
Из моего прочтения вопроса, на который вы ссылаетесь, - да, у вас может возникнуть проблема с переупорядочением, потому что вы видите частично инициализированный экземпляр с помощью побега ссылки. Не в другом случае, когда вы переместили map.put из конструктора. Но я не уверен, поэтому жду авторитетного ответа.   -  person lexicore    schedule 05.10.2017
comment
@GurV Вот мой ответ /35903528#35903528" title="есть ли какое-либо переупорядочение инструкций, выполненное jit-компилятором горячей точки, которое может быть"> stackoverflow.com/questions/35883354/ по тому же вопросу. Учтите, что final и volatile имеют семантику забора памяти (барьера).   -  person Ivan Mamontov    schedule 14.10.2017


Ответы (2)


Если вы хотите соответствовать спецификации, вы не можете применить этот конструктор:

private DataWrapper(String name) {
  this.name = name;
  this.data = loadData(name);
  map.put(name, this);
}

Как вы указываете, JVM разрешено переупорядочивать это примерно так:

private DataWrapper(String name) {
  map.put(name, this);
  this.name = name;
  this.data = loadData(name);
}

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

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

person Rafael Winterhalter    schedule 09.10.2017
comment
Спасибо за ответ. Вопрос - Что, если бы поля были volatile вместо final? Если в этом случае я переместил map.put за пределы конструктора, тогда код будет безопасным? Кроме того, не могли бы вы поделиться ссылками на какие-либо официальные источники, подтверждающие это? Спасибо. - person Gurwinder Singh; 09.10.2017
comment
С изменчивыми полями вы столкнулись бы с той же проблемой. На самом деле это общепризнанная проблема, которая должна быть исправлена ​​в JMM9 (openjdk.java.net/jeps /188), где предлагается дать конструктору гарантии заморозки для любого поля, а не только для конечных. - person Rafael Winterhalter; 09.10.2017
comment
В качестве справки взгляните на 17.5.1 JMM (docs.oracle.com/javase/specs/jls/se7/html/jls-17.html). К сожалению, природа спецификации заключается не в том, чтобы объявлять неопределенное поведение, а в том, чтобы указывать допустимое поведение и отображать все, что находится за пределами спецификации, как неопределенное. Но из упомянутого раздела совершенно ясно, что любое другое использование полей конструктора приводит к тем же условиям гонки, что и любое переупорядочивание без конструктора. - person Rafael Winterhalter; 09.10.2017
comment
Я наткнулся на эту страницу документов Java, в котором говорится: Действия в потоке до помещения объекта в любую параллельную коллекцию — до действий, следующих за доступом или удалением этого элемента из коллекции в другом потоке.. Я предполагаю, что это не поможет мне, если я не перенесу конструктор map.put за пределы конструктора. Не могли бы вы подтвердить? - person Gurwinder Singh; 14.10.2017
comment
Я полностью следил за тем, чтобы у вас была параллельная карта. Эта карта использует изменчивые значения внутри, что означает, что это происходит до гарантий для полей, назначенных до сохранения, для любого последующего чтения. Это имеет тот же эффект, что и действие замораживания. - person Rafael Winterhalter; 15.10.2017

Реализация имеет несколько очень тонких предостережений.

Кажется, вы знаете, но просто для ясности: в этом фрагменте кода несколько потоков могут получить экземпляр null и войти в блок if, без необходимости создавая новые экземпляры DataWrapper:

public static DataWrapper getInstance(String name) {
    DataWrapper instance = map.get(name);
    if (instance == null) {
        instance = new DataWrapper(name);
    }
    return instance.cloneInstance();
}

Кажется, вы согласны с этим, но это требует предположения, что loadData(name) (используемый DataWrapper(String)) всегда будет возвращать одно и то же значение. Если он может возвращать разные значения в зависимости от времени, нет гарантии, что последний поток, загружающий данные, сохранит их в map, поэтому значение может быть устаревшим. Если вы говорите, что этого не произойдет или это не важно, это нормально, но это предположение должно быть как минимум задокументировано.

Чтобы продемонстрировать еще одну тонкую проблему, позвольте мне встроить метод instance.cloneInstance():

public static DataWrapper getInstance(String name) {
    DataWrapper instance = map.get(name);
    if (instance == null) {
        instance = new DataWrapper(name);
    }
    return new DataWrapper(instance);
}

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

Для этого есть простое решение: если вы сделаете поля name и data final, класс станет неизменяемым. Неизменяемые классы пользуются особыми гарантиями инициализации, и return new DataWrapper(this); становится безопасной публикацией.

С этим простым изменением и при условии, что вы согласны с первым пунктом (loadData не зависит от времени), я думаю, что реализация должна работать правильно.


Я бы порекомендовал дополнительное улучшение, связанное не с корректностью, а с другими хорошими практиками. Текущая реализация имеет слишком много обязанностей: это обертка вокруг Data, и в то же время кеш. Дополнительная ответственность делает чтение немного запутанным. И, кстати, параллельная хеш-карта на самом деле не использует свой потенциал.

Если вы разделите обязанности, результат может быть проще, лучше и читабельнее:

class DataWrapperCache {

  private static final ConcurrentMap<String, DataWrapper> map = new ConcurrentHashMap<>();

  public static DataWrapper get(String name) {
    return map.computeIfAbsent(name, DataWrapper::new).defensiveCopy();
  }
}

class DataWrapper {

  private final String name;
  private final Data data;

  DataWrapper(String name) {
    this.name = name;
    this.data = loadData(name);  // A heavy method
  }

  private DataWrapper(DataWrapper that) {
    this.name = that.name;
    this.data = that.data.cloneInstance();
  }

  public DataWrapper defensiveCopy() {
    return new DataWrapper(this);
  }
}
person janos    schedule 07.10.2017
comment
Спасибо за ответ. Я не собираюсь создавать DataWrapperCache, но я вижу, что вы переместили вставленную часть карты из конструктора DataWrapper. Я хочу понять, как это влияет (или не находится) в конструкторе DataWrapper. Кроме того, вы правы насчет неконечных полей. Но что, если бы я сделал их final (или volatile) и сохранил вызов map.put внутри конструктора, как следует из первой части вашего ответа? Тогда я в безопасности? Я ищу ссылки на официальные/надежные источники. - person Gurwinder Singh; 08.10.2017
comment
@GurV Я написал в конце своего первого пункта, что окончательные поля должны сделать вашу реализацию безопасной. Вам не нужно создавать DataWrapperCache, но я все же рекомендую это сделать. Мои аргументы основаны на моей интерпретации книги Java Concurrency in Practice, особенно главы 3. У меня нет ссылок на документацию JLS, потому что это слишком загадочно для меня, чтобы читать :-/ - person janos; 08.10.2017
comment
Я не могу полностью следовать этому ответу. Кроме того, new DataWrapper(this) не будет компилироваться из метода static. - person Rafael Winterhalter; 09.10.2017
comment
Это была оплошность, я хотел написать return new DataWrapper(instance) - person janos; 12.10.2017