JavaRush /Java блог /Random /Кофе-брейк #278. Что такое синхронизированные коллекции в...

Кофе-брейк #278. Что такое синхронизированные коллекции в Java и как они работают. Различия между коллекторами потоков GroupingBy и ToMap в Java 8

Статья из группы Random

Что такое синхронизированные коллекции в Java и как они работают

Источник: Hackernoon Это учебное руководство объясняет, что делает синхронизированные коллекции в Java потокобезопасными, и что следует учесть при их использовании. Кофе-брейк #278. Что такое синхронизированные коллекции в Java и как они работают. Различия между коллекторами потоков GroupingBy и ToMap в Java 8 - 1Платформа Java Collection является одним из основополагающих компонентов языка Java. Коллекции представляют собой группу объектов, а Java предоставляет несколько интерфейсов и реализаций для работы с этими коллекциями. С появлением многопоточного программирования возросла потребность в потокобезопасных коллекциях. Именно тогда в структуру Java Collection были добавлены синхронизированные коллекции.

Как синхронизированные коллекции обеспечивают потокобезопасность?

Синхронизированные коллекции (Synchronized Collections) обеспечивают потокобезопасность за счет принудительной синхронизации каждого из общедоступных методов. Кроме того, это гарантирует, что его внутреннее состояние никогда не будет опубликовано. То есть, единственный способ изменить коллекцию — это использовать ее общедоступные синхронизированные методы! Синхронизированные коллекции можно представить, как простые несинхронизированные коллекции с инкапсуляцией состояния и синхронизированными общедоступными методами. Поскольку одна и та же (внутренняя) блокировка защищает каждый общедоступный метод синхронизированной коллекции, какие-либо два потока не могут изменять/читать коллекцию одновременно. Это гарантирует, что коллекция всегда сохранит свои варианты и, следовательно, станет потокобезопасной.

Как создавать синхронизированные коллекции

Класс Collections предоставляет несколько статических методов для создания синхронизированных коллекций. Эти статические методы имеют имена в следующем формате — synchronizedXxx. Вот список этих методов:
  • synchronizedCollection(Collection<T> c)
  • synchronizedList(List<T> list)
  • synchronizedMap(Map<K, V> m)
  • synchronizedNavigableMap(NavigableMap<K, V> m)
  • synchronizedNavigableSet(NavigableSet<T> s)
  • synchronizedSet(Set<T> s)
  • synchronizedSortedMap(SortedMap<K, V> m)
  • synchronizedSortedSet(SortedSet<T> s)
Теперь мы создадим синхронизированный список, используя класс Collections.

List<Integer> synchronizedIntegerList = Collections.synchronizedList(new ArrayList<>());

Ловушка при работе с синхронизированными коллекциями

Хотя методы, предоставляемые из синхронизированных коллекций, являются потокобезопасными, действия на стороне клиента по-прежнему требуют надлежащей блокировки. Рассмотрим следующий пример кода:

public void putIfAbsent(List<Integer> synchronizedList, Integer elem) {
  if(!synchronizedList.contains()) {
    synchronizedList.add(elem);
  }
}
Этот невинный на вид метод небезопасен для потоков! Даже с потокобезопасными методами contains и add, метод putIfAbsent не достигает того, для чего он предназначен. Между шагами “проверка” и “действие” нашего метода другой поток может добавить elem в список. Ответственность за защиту от таких случаев лежит на клиенте. Синхронизированные коллекции позволяют блокировать на стороне клиента, чтобы гарантировать, что такие составные действия являются атомарными по отношению к другим операциям. Каждый общедоступный метод синхронизированной коллекции защищен внутренней блокировкой. Таким образом, клиент может создавать атомарные операции, получая ту же блокировку. Вот как мы исправим ситуацию с putIfAbsent с помощью блокировки на стороне клиента.

public void putIfAbsent(List<Integer> synchronizedList, Integer elem) {
  synchronized(synchronizedList) {
    if(!synchronizedList.contains()) {
      synchronizedList.add(elem);
    }
  }
}

Подводные камни итерации синхронизированных коллекций

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

final Set<String> syncStringSet = Collections.synchronizedSet(new HashSet<>());

// Может вызвать исключение ConcurrentModificationException
for(String s: syncStringSet) {
  doSomething(s);
}
Чтобы избежать выброса исключения ConcurrentModificationException, мы можем удерживать блокировку синхронизированной коллекции на протяжении всей итерации. Конечно, что это не самый эффективный подход, поскольку итерация может занять много времени и заблокирует доступ к коллекции другим выполняющимся потокам. Помните, что синхронизированные классы предоставляют простой механизм, позволяющий сделать классы коллекций потокобезопасными. Их глубокое понимание поможет вам избежать многих распространенных ошибок!

Различия между коллекторами потоков GroupingBy и ToMap в Java 8

Источник: Devops.dev В этой статье подробно рассмотрены различия между коллекторами groupingBy и toMap() в Java, а также варианты их использования. Java Stream API значительно улучшает работу с коллекциями объектов. Два этого есть распространенных инструмента: коллекторы (сборщики) groupingBy и toMap(). Оба они собирают элементы в Map, но делают это по-разному.

Коллектор GroupingBy

Коллектор groupingBy выполняет группировку по функциям. Он образует группы элементов, которые имеют что-то общее. Вот как работает groupingBy:

groupingBy(Function<? super T, ? extends K> classifier)
  • classifier: эта функция определяет, к какой группе принадлежит элемент.
Вот наглядный пример, когда у нас есть список людей и мы хотим сгруппировать их по возрасту:

Map<Integer, List<Person>>peopleByAge =people.stream() 
    .collect(Collectors.groupingBy(Person::getAge));
Здесь мы используем Person::getAge для группировки людей по возрасту. Он создает карту (Map), где ключами (key) являются возрасты (age), а значениями (value) — списки (lists) людей этого возраста.

Коллектор ToMap

Коллектор toMap() напрямую превращает элементы в ключи и значения. Ему нужны две функции: одна для ключей (key) и одна для значений (value). Вот как работает toMap:

toMap(Functionv? super T, ? extends K> keyMapper, 
      Function<? super T, ? extends U> valueMapper)
  • keyMapper: Эта функция определяет, каким должен быть ключ.
  • valueMapper: Эта функция определяет, каким должно быть значение.
Например, если у нас есть список людей и мы хотим создать Map, где имена — это ключи, а возраст — значения:

Map<String, Integer> nameToAgeMap =people.stream() 
    .collect(Collectors.toMap(Person::getName, Person::getAge));
В данном примере Person::getName дает нам имена (name) в качестве ключей, а Person::getAge предоставляет возраст (age) в качестве значений.

Ключевые различия между GroupingBy и ToMap

  • groupingBy группирует элементы на основе общих характеристик, тогда как toMap сопоставляет элементы непосредственно с ключами и значениями.
  • Значения на карте. В версии groupingBy значения на карте (Map) представляют собой списки элементов с одинаковым ключом, тогда как в версии toMap значения берутся непосредственно из элементов.
  • Варианты использования: используйте groupingBy, когда вы хотите объединить объекты в группы на основе чего-то общего. Используйте toMap, если вы хотите напрямую сопоставить каждый элемент с парой ключ-значение.
Подводя итоги, можно заметить, что коллекторы groupingBy и toMap делают похожие вещи, но работают они по-разному и используются для разных целей. Понимание этих различий помогает эффективно использовать Java Stream API в различных ситуациях.
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ