Рубрики
Без рубрики

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

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

Автор оригинала: Alejandro Ugarte.

1. Обзор

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

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

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

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

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

2. Метод synchronizedCollection()

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

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

Collection 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);
}

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

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

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

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

3. Метод synchronizedList()

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

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

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

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

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

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

List syncCollection = 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(). Мы могли бы использовать его для простого создания синхронизированной Карты .

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

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

5. Метод synchronizedSortedMap()

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

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

6. Метод синхронизированного набора()

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

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

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

7. Метод synchronizedSortedSet()

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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