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 .