Перейти к основному содержимому

Введение в синхронизированные коллекции Java

· 5 мин. чтения

1. Обзор

Каркас коллекций является ключевым компонентом Java. Он предоставляет большое количество интерфейсов и реализаций, что позволяет нам создавать различные типы коллекций и управлять ими простым способом.

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

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

Эти оболочки упрощают создание синхронизированных представлений предоставленных коллекций с помощью нескольких статических фабричных методов.

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

2. Метод synchronizedCollection ()

Первая оболочка синхронизации, которую мы рассмотрим в этом обзоре, — это метод synchronizedCollection() . Как следует из названия, он возвращает потокобезопасную коллекцию, резервную копию которой обеспечивает указанная коллекция .

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

Collection<Integer> syncCollection = Collections.synchronizedCollection(new ArrayList<>());
Runnable listOperations = () -> {
syncCollection.addAll(Arrays.asList(1, 2, 3, 4, 5, 6));
};

Thread thread1 = new Thread(listOperations);
Thread thread2 = new Thread(listOperations);
thread1.start();
thread2.start();
thread1.join();
thread2.join();

assertThat(syncCollection.size()).isEqualTo(12);
}

Как показано выше, создать синхронизированное представление предоставленной коллекции с помощью этого метода очень просто.

Чтобы продемонстрировать, что метод действительно возвращает потокобезопасную коллекцию, мы сначала создадим пару потоков.

После этого мы внедряем экземпляр Runnable в их конструкторы в виде лямбда-выражения. Не будем забывать, что Runnable — это функциональный интерфейс, поэтому мы можем заменить его лямбда-выражением.

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

3. Метод synchronizedList ()

Аналогично методу synchronizedCollection() мы можем использовать оболочку synchronizedList() для создания синхронизированного списка .

Как и следовало ожидать, метод возвращает потокобезопасное представление указанного List :

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

Неудивительно, что использование метода synchronizedList() выглядит почти идентично его аналогу более высокого уровня, synchronizedCollection() .

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

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

List<String> syncCollection = Collections.synchronizedList(Arrays.asList("a", "b", "c"));
List<String> uppercasedCollection = new ArrayList<>();

Runnable listOperations = () -> {
synchronized (syncCollection) {
syncCollection.forEach((e) -> {
uppercasedCollection.add(e.toUpperCase());
});
}
};

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

Использование синхронизированного блока обеспечивает атомарность операции .

4. Метод synchronizedMap ()

Класс Collections реализует еще одну удобную оболочку синхронизации, которая называется synchronizedMap(). Мы могли бы использовать его для простого создания синхронизированной карты .

Метод возвращает потокобезопасное представление предоставленной реализации Map :

Map<Integer, String> syncMap = Collections.synchronizedMap(new HashMap<>());

5. Метод synchronizedSortedMap ()

Существует также аналогичная реализация метода synchronizedMap() . Он называется synchronizedSortedMap() , и мы можем использовать его для создания синхронизированного экземпляра SortedMap :

Map<Integer, String> syncSortedMap = Collections.synchronizedSortedMap(new TreeMap<>());

6. Метод synchronizedSet ()

Далее, в этом обзоре, у нас есть метод synchronizedSet() . Как следует из названия, он позволяет нам создавать синхронизированные наборы с минимальными усилиями.

Оболочка возвращает потокобезопасную коллекцию, поддерживаемую указанным Set :

Set<Integer> syncSet = Collections.synchronizedSet(new HashSet<>());

7. Метод synchronizedSortedSet ()

Наконец, последняя оболочка синхронизации, которую мы здесь продемонстрируем, — synchronizedSortedSet() .

Подобно другим реализациям оболочки, которые мы рассмотрели до сих пор, метод возвращает потокобезопасную версию данного SortedSet :

SortedSet<Integer> syncSortedSet = Collections.synchronizedSortedSet(new TreeSet<>());

8. Синхронизированные и параллельные коллекции

До этого момента мы более подробно рассмотрели оболочки синхронизации фреймворка коллекций.

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

8.1. Синхронизированные коллекции

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

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

Подробное руководство по использованию синхронизированных методов и блоков можно найти в нашей статье на эту тему.

8.2. Параллельные коллекции

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

Параллельные коллекции намного более эффективны, чем синхронизированные коллекции , благодаря неотъемлемым преимуществам доступа к параллельным потокам.

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

9. Заключение

В этой статье мы подробно рассмотрели набор оберток синхронизации, реализованных в классе Collections .

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

Как обычно, все примеры кода, показанные в этой статье, доступны на GitHub .