Безопасная публикация содержимого массива / коллекции / карты, записанного один раз - PullRequest
0 голосов
/ 08 января 2020

Основной вопрос:
Каков наилучший способ безопасной публикации содержимого массива, коллекции или карты в Java?

Вот что я Я пробовал и некоторые дополнительные вопросы у меня есть:


Дополнительный вопрос # 1

В теме 1, я пишу HashMap:

Map<K, V> map = new HashMap<>();
map.put(key, value);

Насколько я понимаю,

Я не прав?


Дополнительный вопрос № 2

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

Я придумал эту альтернативу:

class MapSafePublication<K, V> {
    private final Map<K, V> map = new HashMap<>();

    private final ThreadLocal<Map<K, V>> safeMap = ThreadLocal.withInitial(() -> {
        synchronized (MapSafePublication.this) {
            return new HashMap<>(map);
        }
    });

    synchronized void write(K key, V value) {
        map.put(key, value);
    }

    V read(K key) {
        return safeMap.get().get(key);
    }
}

Это правильно? Есть ли лучшие способы сделать это?


Дополнительный вопрос # 3

В потоке 1 я пишу в энергозависимый массив:

volatile Object[] array = new Object[size];
array[index] = value;

Насколько я понимаю, чтение array[index] из другого потока небезопасно. Чтобы сделать его безопасным, нужно использовать AtomicReferenceArray.

Вот выдержка из CopyOnWriteArrayList:

final transient Object lock = new Object();
private transient volatile Object[] array;

public E set(int index, E element) {
    synchronized (lock) {
        Object[] es = getArray();
        E oldValue = elementAt(es, index);

        if (oldValue != element) {
            es = es.clone();
            es[index] = element;
        }
        // Ensure volatile write semantics even when oldvalue == element
        setArray(es);
        return oldValue;
    }
}

public E get(int index) {
    return elementAt(getArray(), index);
}

@SuppressWarnings("unchecked")
static <E> E elementAt(Object[] a, int index) {
    return (E) a[index];
}

Как может get() безопасно прочитать из массива, если он не получит ту же блокировку, которая использовалась set()?

То же самое относится к результат com.google.common.collect.ImmutableList.copyOf(), который, согласно моему текущему пониманию, не является безопасной публикацией ссылок, сохраненных или возвращенных объектом параметра. Я не прав?


Разъяснение # 1

С точки зрения видимости памяти, что из следующего безопасно?

class Test<K, V> {
    private final Test1<K, V> test1;
    private Test2<K, V> test2;

    Test(Test1<K, V> test1, Test2<K, V> test2) {
        this.test1 = test1;
        this.test2 = test2;
    }

    void test(K key) {
        System.out.println(test1.read(key));
        System.out.println(test2.read(key));
    }
}

class Test1<K, V> {
    private Map<K, V> map = new HashMap<>();

    void write(K key, V value) {
        map.put(key, value);
    }

    V read(K key) {
        return map.get(key);
    }
}

class Test2<K, V> {
    private final Map<K, V> map = new HashMap<>();

    void write(K key, V value) {
        map.put(key, value);
    }

    V read(K key) {
        return map.get(key);
    }
}

// Thread 1
Test1<K, V> test1 = new Test1<>();
test1.write(key, value);
Test2<K, V> test2 = new Test2<>();
test2.write(key, value);

// Thread 2
Test<K, V> test = new Test<>(test1, test2);
test.test(key);

Пояснение № 2:

Рассмотрим следующий пример:

// Thread 1
Object obj = new Object();
volatile Object object = obj;

// Thread 2
System.out.println(object);

// Thread 1 or 3
object = obj;

// Thread 2
System.out.println(object);

Так что в последнем случае между читателем и Автор темы?

Ответы [ 2 ]

3 голосов
/ 09 января 2020

Не существует «лучшего способа выполнить безопасную публикацию», поскольку решение о способе публикации зависит от фактического варианта использования, связанного с публикацией.

Так что неверно говорить, что Collections.unmodifiableMap(…) не является безопасная публикация. Этот метод вообще не является публикацией.

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

A ConcurrentHashMap может решить эту проблему не потому, что это делает публикацию карты безопасной, а потому, что каждая модификация является безопасной публикацией сама по себе. Это по-прежнему делает его использование потокобезопасным только в том случае, если использующие потоки могут справиться с тем фактом, что непротиворечивого общего состояния карты не существует, когда существуют изменения во время обработки карты, но согласованы только отдельные отображения. И каждый ключ или значение не должны изменяться после публикации.

Если вы соблюдаете правило, согласно которому вы не должны изменять объект (ы) после публикации, существует множество возможностей для правильного издание. Рассмотрим:

HashMap<String, List<Integer>> map = new HashMap<>();
List<Integer> l = new ArrayList<>();
l.add(42);
map.put("foo", l);

Thread t = new Thread() {
    @Override
    public void run() {
        System.out.println(map);
    }
};
t.start();
System.out.println(map);
t.join();

Между вызовом t.start() и любым действием, выполняемым потоком, существует отношение , которое происходит до . Этого достаточно для безопасной публикации HashMap и содержимого ArrayList без каких-либо дополнительных усилий - при условии соблюдения правила «без изменений после публикации». Тот факт, что оба потока могут читать карту одновременно, не имеет значения.

Поэтому, когда у вас есть код, такой как

volatile Object[] array = new Object[size];
array[index] = value;

, вы правы, это небезопасно, потому что нарушает «нет правило «модификация после публикации».

Но CopyOnWriteArrayList отличается, если внимательно посмотреть опубликованный код:

public E set(int index, E element) {
    synchronized (lock) {
        Object[] es = getArray();
        E oldValue = elementAt(es, index);

        if (oldValue != element) {
            es = es.clone(); // <- create a local copy
            es[index] = element; // <- modifies the copy that has not been published
        }
        // Ensure volatile write semantics even when oldvalue == element
        setArray(es); // <- publishes the copy
        return oldValue;
    }
}

Таким образом, нет нарушения «нет изменений после публикации» Как правило, модификации всегда выполняются локальными копиями перед их публикацией, а запись volatile совершенно новой ссылки на массив устанавливает отношение случай-до с последующими чтениями volatile этой ссылки. synchronized существует только для обеспечения согласованности нескольких модификаций.

Обратите внимание, что опубликованный вами код в некоторых отношениях отличается от этой реализации OpenJDK :

public E set(int index, E element) {
    synchronized (lock) {
        Object[] es = getArray();
        E oldValue = elementAt(es, index);

        if (oldValue != element) {
            es = es.clone();
            es[index] = element;
            setArray(es);
        }
        return oldValue;
    }
}

Хотя оба работают без проблем при правильном использовании, разница

        // Ensure volatile write semantics even when oldvalue == element
        setArray(es); // invoked even when oldvalue == element

показывает неправильное мышление, точно соответствующее обсуждаемому правилу. Когда oldvalue == element, заданный элемент уже был в списке, другими словами, уже опубликован. Поэтому, если элемент был изменен перед этим избыточным вызовом set, он нарушил бы правило «без изменений после публикации», и выполнение еще одной записи volatile не исправило бы это. С другой стороны, если не было сделано никаких изменений, запись volatile была бы устаревшей. Таким образом, нет никакой причины выполнять запись volatile, когда oldvalue == element.

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

2 голосов
/ 08 января 2020

Чтобы ответить на вашу первую часть:

Collections.unmodifiableMap () не является безопасной публикацией ссылок, хранящихся на карте.

Правильно.

Изучая реализацию Map.of () ...

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


Чтобы ответить на вашу третью часть:

Как можно получить () читать из массива безопасно, если он не получит ту же блокировку, которая использовалась set ()?

Семантика volatile такова, что происходит запись в энергозависимое поле перед чтением этого изменчивого поля.

Итак, эта упрощенная последовательность действий в set:

Object[] es = getArray() /* volatile read of array (not especially relevant) */;
es = es.clone();
es[index] = element;
setArray(es);  // Internally does `array = es;`, so a volatile write.

происходит до этого, в get.

return elementAt(getArray() /* volatile read of array */, index);

Таким образом, обновленный элемент массива виден потокам, вызывающим get.

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

...