Автор оригинала: Alejandro Ugarte.
1. Обзор
Фреймворк collections является ключевым компонентом Java. Он предоставляет большое количество интерфейсов и реализаций, что позволяет нам создавать различные типы коллекций и управлять ими простым способом.
Хотя использование простых несинхронизированных коллекций в целом просто, это также может стать сложным и подверженным ошибкам процессом при работе в многопоточных средах (так называемое параллельное программирование).
Следовательно, платформа Java обеспечивает сильную поддержку этого сценария с помощью различных оболочек синхронизации , реализованных в классе Collections .
Эти оболочки позволяют легко создавать синхронизированные представления предоставляемых коллекций с помощью нескольких статических заводских методов.
В этом уроке мы глубоко погрузимся в эти оболочки статической синхронизации. Кроме того, мы выделим разницу между синхронизированными коллекциями и параллельными коллекциями .
2. Метод synchronizedCollection()
Первая оболочка синхронизации, которую мы рассмотрим в этом обзоре,-это метод synchronizedCollection () . Как следует из названия, он возвращает потокобезопасную коллекцию, резервную копию которой создает указанная Коллекция .
Теперь, чтобы более четко понять, как использовать этот метод, давайте создадим базовый модульный тест:
CollectionsyncCollection = 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); }
Как показано выше, создание синхронизированного представления предоставленной коллекции с помощью этого метода очень просто.
Чтобы продемонстрировать, что метод действительно возвращает потокобезопасную коллекцию, мы сначала создадим пару потоков.
После этого мы затем вводим Запускаемый экземпляр в их конструкторы в виде лямбда-выражения. Давайте иметь в виду, что Запускаемый – это функциональный интерфейс, поэтому мы можем заменить его лямбда-выражением.
Наконец, мы просто проверяем, что каждый поток эффективно добавляет шесть элементов в синхронизированную коллекцию, поэтому ее окончательный размер равен двенадцати.
3. Метод synchronizedList()
Аналогично, аналогично методу synchronizedCollection () , мы можем использовать оболочку synchronizedList() для создания синхронизированного Списка .
Как и следовало ожидать, метод возвращает потокобезопасное представление указанного Списка :
ListsyncList = Collections.synchronizedList(new ArrayList<>());
Неудивительно, что использование метода synchronizedList() выглядит почти идентично его аналогу более высокого уровня, synchronizedCollection() .
Поэтому, как мы только что сделали в предыдущем модульном тесте, как только мы создали синхронизированный Список , мы можем создать несколько потоков. После этого мы будем использовать их для доступа/управления целевым Списком потокобезопасным способом.
Кроме того, если мы хотим выполнить итерацию по синхронизированной коллекции и предотвратить неожиданные результаты, мы должны явно предоставить нашу собственную потокобезопасную реализацию цикла. Следовательно, мы могли бы достичь этого, используя синхронизированный блок:
ListsyncCollection = Collections.synchronizedList(Arrays.asList("a", "b", "c")); List uppercasedCollection = new ArrayList<>(); Runnable listOperations = () -> { synchronized (syncCollection) { syncCollection.forEach((e) -> { uppercasedCollection.add(e.toUpperCase()); }); } };
Во всех случаях, когда нам нужно выполнить итерацию по синхронизированной коллекции, мы должны реализовать эту идиому. Это связано с тем, что итерация синхронизированной коллекции выполняется с помощью нескольких вызовов в коллекцию. Поэтому они должны выполняться как единая атомарная операция.
Использование блока synchronized обеспечивает атомарность операции .
4. Метод synchronizedMap()
Класс Collections реализует другую аккуратную оболочку синхронизации, называемую synchronizedMap(). Мы могли бы использовать его для простого создания синхронизированной Карты .
Метод возвращает потокобезопасное представление предоставленной Карты реализации :
MapsyncMap = Collections.synchronizedMap(new HashMap<>());
5. Метод synchronizedSortedMap()
Существует также аналогичная реализация метода synchronizedMap () . Он называется synchronizedSortedMap() , который мы можем использовать для создания синхронизированного SortedMap экземпляра:
MapsyncSortedMap = Collections.synchronizedSortedMap(new TreeMap<>());
6. Метод синхронизированного набора()
Далее, переходя к этому обзору, у нас есть метод synchronized Set () . Как следует из его названия, он позволяет нам создавать синхронизированные Наборы с минимальной суетой.
Оболочка возвращает потокобезопасную коллекцию, поддерживаемую указанным Набором :
SetsyncSet = Collections.synchronizedSet(new HashSet<>());
7. Метод synchronizedSortedSet()
Наконец, последняя оболочка синхронизации, которую мы продемонстрируем здесь, – это synchronizedSortedSet() .
Подобно другим реализациям оболочки, которые мы рассмотрели до сих пор, метод возвращает потокобезопасную версию данного SortedSet :
SortedSetsyncSortedSet = Collections.synchronizedSortedSet(new TreeSet<>());
8. Синхронизированные и параллельные коллекции
До этого момента мы более подробно рассмотрели оболочки синхронизации платформы collections.
Теперь давайте сосредоточимся на различиях между синхронизированными коллекциями и параллельными коллекциями , такими как ConcurrentHashMap и BlockingQueue реализации.
8.1. Синхронизированные Коллекции
Синхронизированные коллекции обеспечивают потокобезопасность за счет блокировки intrinsi c , и все коллекции блокируются . Внутренняя блокировка реализуется с помощью синхронизированных блоков в рамках методов обернутой коллекции.
Как и следовало ожидать, синхронизированные коллекции обеспечивают согласованность/целостность данных в многопоточных средах. Однако они могут привести к снижению производительности, так как только один поток может одновременно получить доступ к коллекции (он же синхронизированный доступ).
Для получения подробного руководства по использованию синхронизированных методов и блоков, пожалуйста, ознакомьтесь с нашей статьей по этой теме.
8.2. Параллельные Коллекции
Параллельные коллекции (например, ConcurrentHashMap), обеспечивают потокобезопасность, разделяя свои данные на сегменты . Например , в ConcurrentHashMap разные потоки могут получать блокировки на каждом сегменте, поэтому несколько потоков могут одновременно обращаться к Карте (так называемый параллельный доступ).
Параллельные коллекции гораздо более эффективны , чем синхронизированные коллекции , благодаря неотъемлемым преимуществам параллельного доступа к потокам.
Таким образом, выбор того, какой тип потокобезопасной коллекции использовать, зависит от требований каждого варианта использования, и его следует оценивать соответствующим образом.
9. Заключение
В этой статье мы подробно рассмотрели набор оболочек синхронизации, реализованных в Коллекциях классе .
Кроме того, мы подчеркнули различия между синхронизированными и параллельными коллекциями, а также рассмотрели подходы, которые они реализуют для обеспечения потокобезопасности.
Как обычно, все примеры кода, показанные в этой статье, доступны на GitHub .