Подтвердить что ты не робот

Является ли итерация через Collections.synchronizedSet(...). ForEach() гарантированной потокобезопасностью?

Как мы знаем, итерация по параллельной коллекции по умолчанию не является потокобезопасной, поэтому нельзя использовать:

Set<E> set = Collections.synchronizedSet(new HashSet<>());
//fill with data
for (E e : set) {
    process(e);
}

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

Это описано в javadoc Collections.synchronizedSet:

public static Set synchronizedSet (Set s)

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

Обязательно, чтобы пользователь вручную синхронизировал возвращаемый набор при его повторении:

Установить s = Collections.synchronizedSet(новый HashSet())

      ...
   synchronized (s) { Iterator i = s.iterator(); // Must be in the synchronized block while (i.hasNext()) foo(i.next()); }

Несоблюдение этого совета может привести к детерминированному поведению.

Однако, это не относится к Set.forEach, который наследует метод по умолчанию forEach от Iterable.forEach.

Теперь я посмотрел исходный код, и здесь мы видим, что мы имеем следующую структуру:

  • Мы запрашиваем Collections.synchronizedSet().
  • Мы получаем один:

    public static <T> Set<T> synchronizedSet(Set<T> s) {
        return new SynchronizedSet<>(s);
    }
    
    ...
    
    static class SynchronizedSet<E>
          extends SynchronizedCollection<E>
          implements Set<E> {
        private static final long serialVersionUID = 487447009682186044L;
    
        SynchronizedSet(Set<E> s) {
            super(s);
        }
        SynchronizedSet(Set<E> s, Object mutex) {
            super(s, mutex);
        }
    
        public boolean equals(Object o) {
            if (this == o)
                return true;
            synchronized (mutex) {return c.equals(o);}
        }
        public int hashCode() {
            synchronized (mutex) {return c.hashCode();}
        }
    }
    
  • Он расширяет SynchronizedCollection, который имеет следующие интересные методы рядом с очевидными:

    // Override default methods in Collection
    @Override
    public void forEach(Consumer<? super E> consumer) {
        synchronized (mutex) {c.forEach(consumer);}
    }
    @Override
    public boolean removeIf(Predicate<? super E> filter) {
        synchronized (mutex) {return c.removeIf(filter);}
    }
    @Override
    public Spliterator<E> spliterator() {
        return c.spliterator(); // Must be manually synched by user!
    }
    @Override
    public Stream<E> stream() {
        return c.stream(); // Must be manually synched by user!
    }
    @Override
    public Stream<E> parallelStream() {
        return c.parallelStream(); // Must be manually synched by user!
    }
    

Используемый здесь mutex - это тот же объект, с которым блокируются все операции Collections.synchronizedSet.

Теперь мы можем, судя по реализации, сказать, что поточно-безопасным использовать Collections.synchronizedSet(...).forEach(...), но также ли он безопасен по потоку по спецификации?

(достаточно смутно, Collections.synchronizedSet(...).stream().forEach(...) не является потокобезопасным по реализации, и вердикт спецификации также кажется неизвестным.)

4b9b3361

Ответ 1

Как вы писали, судя по реализации, forEach() является потокобезопасным для коллекций, поставляемых с JDK (см. отказ от ответственности ниже), так как требуется, чтобы монитор мьютекса был приобретен для продолжения.

Является ли он также безопасным по потоку по спецификации?

Мое мнение - нет, и вот объяснение. Collections.synchronizedXXX() javadoc, переписанный короткими словами, говорит: "все методы являются потокобезопасными, за исключением тех, которые используются для итерации по нему".

Мой другой, хотя очень субъективный аргумент - это то, что yshavit написал - если не сказал/не прочитал, рассмотрите API/класс/безотносительно к потокобезопасности.

Теперь давайте более подробно рассмотрим javadocs. Я предполагаю, что могу сказать, что метод forEach() используется для перебора по нему, поэтому, следуя рекомендациям javadoc, мы должны считать его небезопасным, хотя оно противоположно реальности (реализации).

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

Последнее, что я хотел бы упомянуть в этом обсуждении - мы можем предположить, что пользовательская коллекция может быть обернута Collections.synchronizedXXX(), а реализация forEach() этой коллекции может быть... может быть любой. Коллекция может выполнять асинхронную обработку элементов в методе forEach(), порождать поток для каждого элемента... он ограничен только воображением автора, а синхронизированный (mutex) перенос не может гарантировать безопасность потоков для таких случаев. Эта конкретная проблема может быть причиной не объявлять метод forEach() как поточно-безопасный.

Ответ 2

Его стоит взглянуть на документацию Collections.synchronizedCollection, а не Collections.synchronizedSet(), поскольку эта документация уже очищена:

Обязательно, чтобы пользователь вручную синхронизировал возвращаемую коллекцию при ее перемещении через Iterator, Spliterator или Stream:...

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

Обязательно, чтобы пользователь вручную синхронизировал возвращаемый набор при его итерации по нему:...

(выделение мной)

Сравните с документацией для Iterable.forEach:

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

Хотя разработчику ясно, что для достижения этой цели требуется (внутренняя) итерация, эта итерация представляет собой деталь реализации. Из данных спецификаций формулируется его просто (мета) действие для выполнения действия для каждого элемента.

При использовании этого метода пользователь не выполняет итерацию по элементам и, следовательно, не отвечает за синхронизацию, упомянутую в документации Collections.synchronized….

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