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

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

1. обзор

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

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

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

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

В этом руководствеwe’ll take a deep dive into these *static synchronization wrappers. Also, we’ll highlight the difference between synchronized collections and concurrent collections. *

2. МетодsynchronizedCollection()

Первая оболочка синхронизации, которую мы рассмотрим в этом обзоре, - это методsynchronizedCollection(). Как следует из названия,it returns a thread-safe collection backed up by the specified Collection.

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

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

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

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

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

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

3. МетодsynchronizedList()

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

Как и следовало ожидать,the method returns a thread-safe view of the specified List:

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

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

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

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

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

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

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

The use of the synchronized block ensures the atomicity of the operation.

4. МетодsynchronizedMap()

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

The method returns a thread-safe view of the supplied Map implementation:

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

5. МетодsynchronizedSortedMap()

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

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

6. МетодsynchronizedSet()

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

The wrapper returns a thread-safe collection backed by the specified Set:

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

7. МетодsynchronizedSortedSet()

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

Подобно другим реализациям оболочки, которые мы рассмотрели,the method returns a thread-safe version of the given SortedSet:

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

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

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

Теперь давайте сосредоточимся на the differences between synchronized collections and concurrent collections, например на реализацияхConcurrentHashMap иBlockingQueue.

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

Synchronized collections achieve thread-safety through intrinsihttps://docs.oracle.com/javase/tutorial/essential/concurrency/locksync.html[c locking], and the entire collections are locked. Внутренняя блокировка реализуется с помощью синхронизированных блоков в методах обернутой коллекции.

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

Для получения подробного руководства по использованию методов и блоковsynchronized, пожалуйста, проверьтеour article по теме.

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

Concurrent collections (e.g. ConcurrentHashMap), achieve thread-safety by dividing their data into segments. ВConcurrentHashMap, например, разные потоки могут получать блокировки на каждом сегменте, поэтому несколько потоков могут обращаться кMap одновременно (также известный как: одновременный доступ).

Параллельные коллекции - этоmuch more performant than synchronized collections из-за неотъемлемых преимуществ одновременного доступа к потокам.

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

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

In this article, we took an in-depth look at the set of synchronization wrappers implemented within the Collections class.

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

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