Параллельный доступ Java к полю, трюк, чтобы не использовать volatile

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

У меня есть поле List<Something>, которое заполняется после построения. Чтобы сохранить некоторую производительность, я хотел бы преобразовать список в карту только для чтения. Для этого в любой момент требуется как минимум переменное поле карты, поэтому сделайте изменения видимыми для всех потоков.

Я думал сделать следующее:

Map map;

public void get(Object key){
    if(map==null){
        Map temp = new Map();
        for(Object value : super.getList()){
            temp.put(value.getKey(),value);
        }
        map = temp;
    }
     return map.get(key);
}

Это может привести к тому, что несколько потоков будут генерировать карту, даже если они входят в блок get сериализованным способом. Это не было бы большой проблемой, если бы потоки работали на разных идентичных экземплярах карты. Меня больше беспокоит:

Возможно ли, что один поток присваивает новую временную карту полю карты, а затем второй поток видит это map!=null и, следовательно, обращается к полю карты, не создавая нового, но, к моему удивлению, обнаруживает, что карта пуста, потому что помещенный операции, которые еще не были помещены в какую-либо область общей памяти?

Ответы на комментарии:

  • Потоки изменяют временную карту только после того, как она доступна только для чтения.
  • Я должен преобразовать список в карту из-за какой-то специальной настройки JAXB, которая не позволяет начать с карты.

person Franz Kafka    schedule 25.03.2012    source источник
comment
'volatile' может иметь некоторую стоимость, особенно в многопроцессорных средах. Это может потребовать барьеров памяти/очистки памяти, чтобы гарантировать, что другие потоки увидят изменения.   -  person seand    schedule 26.03.2012
comment
Можете ли вы объяснить, как/когда потоки изменяют какой список/карту и почему вы хотите скопировать список в карту?   -  person zapl    schedule 26.03.2012
comment
Я не вижу никаких проблем (кроме того, что ваш код не скомпилируется, нет возвращаемого типа)   -  person stefan bachert    schedule 26.03.2012
comment
@stefan bachert неужели невозможно, чтобы только некоторые из операций put были видны другим потокам? Я думал, что это было бы чистым изменением, какие части сбрасываются в общую память. Это позволяет видеть только половину карты из другого потока.   -  person Franz Kafka    schedule 26.03.2012
comment
Вы не можете изменить карту, которая доступна только для чтения, и если вы хотите, чтобы результаты модификации каждого потока были видны для других потоков, вам нужно выполнить некоторую синхронизированную запись в список/карту. - Ваш метод get() нуждается в синхронизации, потому что действительно поток A может видеть map==null и вводить if, в то время как поток B срабатывает и все еще видит map==null и также вводит if.   -  person zapl    schedule 26.03.2012
comment
У вас есть более 30 вопросов без принятого ответа.   -  person Peter Lawrey    schedule 26.03.2012


Ответы (4)


Возможно ли, что один поток присваивает новую временную карту полю карты, а затем второй поток видит это map!=null и, следовательно, обращается к полю карты без создания нового, но, к моему удивлению, обнаруживает, что карта пуста, потому что помещенный операции, которые еще не были помещены в какую-либо область общей памяти?

Да, это абсолютно возможно; например, оптимизирующий компилятор может фактически полностью избавиться от локальной переменной temp и просто использовать поле map все время, при условии, что он восстанавливает map в null в случае исключения.

Точно так же поток может также видеть ненулевое, непустое map, которое, тем не менее, не заполнено полностью. И если ваш класс Map не спроектирован таким образом, чтобы обеспечить одновременное чтение и запись (или не использует synchronized, чтобы избежать этой проблемы), вы также можете получить странное поведение, если один поток вызывает свой метод get, а другой вызывает свой put.

person ruakh    schedule 25.03.2012
comment
Вот как нужно быть осторожным: mailinator.blogspot.co .uk/2009/06/beautiful-race-condition.html - person biziclop; 26.03.2012
comment
@biziclop Интересно, я видел это пару раз, приятно видеть объяснение. - person Peter Lawrey; 26.03.2012
comment
@biziclop: Это интересно, но его отношение к коду OP тонкое. Это конкретное состояние гонки может произойти только в том случае, если два разных потока одновременно вызывают put на одной и той же карте, что не должно быть возможным в коде OP. (Однако это может произойти в коде OP, если компилятор удаляет переменную temp и если два потока видят map == null, поэтому оба устанавливают map в новую карту и начинают добавлять элементы.) - person ruakh; 26.03.2012
comment
@ruakh Вот именно. Но я также сделал ссылку на статью, чтобы узнать о более общем моменте: как только вы отказываетесь от правильной синхронизации, вы не всегда можете получить повреждение данных, на которое рассчитывали. - person biziclop; 26.03.2012
comment
Статья интересная, но на самом деле в ней не говорится о том, что может произойти, если в гонку вступит несколько процессоров, что сделает очистку памяти более интересной. - person Franz Kafka; 26.03.2012

Можете ли вы создать свою карту в ctor и объявить ее окончательной? При условии, что вы не утечете карту, чтобы другие могли ее изменить, этого должно быть достаточно, чтобы сделать ваш get() безопасным для совместного использования несколькими потоками.

person seand    schedule 25.03.2012
comment
Я мог сделать его окончательным только при использовании какого-то уродливого шаблона делегирования и передаче списка через конструктор, чего я не хочу делать для таких классов. Список создается JAXB, и я хочу создать карту на втором этапе в классе, наследующемся от базового класса, который содержит список. - person Franz Kafka; 26.03.2012
comment
Я считаю, что создание вашей карты в ctor и пометка ее как финальная — это единственное, что вы можете сделать, чтобы безопасно избежать синхронизации. Как насчет того, чтобы использовать другой класс для создания списка из JAXB, а затем передать его ctor нашего исходного класса? - person seand; 26.03.2012
comment
Да, это сработало бы, но мой код действительно раздулся бы, и автоматическая генерация bean-компонента не достигла бы своей цели. Я думаю, что я выберу какое-то изменчивое поле для корневого объекта. Любой другой поток, обращающийся к любому объекту, каким-либо образом связанному с корнем, будет вынужден обновлять каждый общий объект. - person Franz Kafka; 26.03.2012

Если вы действительно сомневаетесь, сможет ли другой поток прочитать "наполовину завершенную" карту (я так не думаю, но никогда не говорите никогда ;-), вы можете попробовать это.

карта пустая или полная

static class MyMap extends HashMap {
   MyMap (List pList) {
    for(Object value : pList){
        put(value.getKey(), value);
    }
   }
}

MyMap map;

public Object get(Object key){
    if(map==null){
        map = new MyMap (super.getList());
    }
    return map.get(key);
 }

Или кто-то видит новую введенную проблему?

person stefan bachert    schedule 26.03.2012
comment
Это имеет ту же проблему: другой поток может увидеть назначение map до завершения конструктора. См. cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking. .html найдите на странице Symantec. - person ruakh; 26.03.2012
comment
Что ж, если вы не хотите верить фактам, я, конечно, не могу вас заставить. :-/ - person ruakh; 26.03.2012
comment
Я нашел абзац, в котором говорилось об этом. Это означает, что любое новое должно быть синхронизировано, иначе вы рискуете работать с незавершенным объектом. Так что новое не атомарно. - person stefan bachert; 26.03.2012

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

return this.map.get(key)

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

Пример кода для предотвращения этого:

Map temp;
if ((temp = this.map) == null)
{

    temp = new ImmutableMap(getList());
    this.map = temp;
}
return temp.get(key);
person igaz    schedule 06.06.2016