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

1. Общ преглед

Рамката за колекции е ключов компонент на 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); } 

Както е показано по-горе, създаването на синхронизиран изглед на предоставената колекция с този метод е много лесно.

За да покажем, че методът всъщност връща колекция, безопасна за нишки, първо създаваме няколко нишки.

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

И накрая, ние просто проверяваме дали всяка нишка ефективно добавя шест елемента към синхронизираната колекция, така че крайният й размер е дванадесет.

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

Във всички случаи, когато трябва да повторим синхронизирана колекция, трябва да приложим този идиом. Това е така, защото итерацията на синхронизирана колекция се извършва чрез множество повиквания в колекцията. Следователно те трябва да се извършват като единична атомна операция.

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

4. synchronizedMap () Метод

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

Методът връща безопасен изглед на предоставената реализация на Map :

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

5. synchronizedSortedMap () Метод

Има и двойна реализация на метода synchronizedMap () . Тя се нарича синхронизиранаСортиранаМапа () , която можем да използваме за създаване на синхронизиран екземпляр на СортиранаМапа :

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

6. synchronizedSet () метод

След това, продължавайки в този преглед, имаме метода synchronizedSet () . Както подсказва името му, той ни позволява да създаваме синхронизирани комплекти с минимална суматоха.

Опаковката връща колекция, безопасна за нишки, подкрепена от посочения набор :

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

7. synchronizedSortedSet () метод

И накрая, последната обвивка за синхронизация, която ще покажем тук, е synchronizedSortedSet () .

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

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

8. Синхронизирани срещу едновременни колекции

До този момент разгледахме по-отблизо синхронизиращите обвивки на рамката за колекции.

Now, let's focus on the differences between synchronized collections and concurrent collections, such as ConcurrentHashMap and BlockingQueue implementations.

8.1. Synchronized Collections

Synchronized collections achieve thread-safety through intrinsic locking, and the entire collections are locked. Intrinsic locking is implemented via synchronized blocks within the wrapped collection's methods.

As we might expect, synchronized collections assure data consistency/integrity in multi-threaded environments. However, they might come with a penalty in performance, as only one single thread can access the collection at a time (a.k.a. synchronized access).

For a detailed guide on how to use synchronized methods and blocks, please check our article on the topic.

8.2. Concurrent Collections

Concurrent collections (e.g. ConcurrentHashMap), achieve thread-safety by dividing their data into segments. In a ConcurrentHashMap, for example, different threads can acquire locks on each segment, so multiple threads can access the Map at the same time (a.k.a. concurrent access).

Concurrent collections are much more performant than synchronized collections, due to the inherent advantages of concurrent thread access.

So, the choice of what type of thread-safe collection to use depends on the requirements of each use case, and it should be evaluated accordingly.

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

В тази статия разгледахме задълбочено набора от обвивки за синхронизация, реализирани в класа Collections .

Освен това подчертахме разликите между синхронизираните и едновременните колекции, а също така разгледахме подходите, които те прилагат за постигане на безопасност на нишките.

Както обикновено, всички примерни кодове, показани в тази статия, са достъпни в GitHub.