Een inleiding tot gesynchroniseerde Java-verzamelingen

1. Overzicht

Het verzamelingsraamwerk is een belangrijk onderdeel van Java. Het biedt een uitgebreid aantal interfaces en implementaties, waardoor we verschillende soorten collecties op een eenvoudige manier kunnen maken en manipuleren.

Hoewel het gebruik van gewone niet-gesynchroniseerde verzamelingen over het algemeen eenvoudig is, kan het ook een ontmoedigend en foutgevoelig proces worden bij het werken in omgevingen met meerdere threads (ook bekend als gelijktijdig programmeren).

Daarom biedt het Java-platform sterke ondersteuning voor dit scenario door verschillende synchronisaties wikkels geïmplementeerd binnen de Collecties klasse.

Deze wrappers maken het eenvoudig om gesynchroniseerde views te creëren van de aangeleverde collecties door middel van verschillende statische fabrieksmethodes.

In deze tutorial we zullen er een diepe duik in nemen statische synchronisatie-wrappers. We zullen ook het verschil benadrukken tussen gesynchroniseerde verzamelingen en gelijktijdige verzamelingen.

2. Het synchronizedCollection () Methode

De eerste synchronisatie-wrapper die we in deze samenvatting behandelen, is de synchronizedCollection () methode. Zoals de naam al doet vermoeden, het retourneert een threadveilige verzameling waarvan een back-up wordt gemaakt door het opgegeven Verzameling.

Om nu duidelijker te begrijpen hoe u deze methode kunt gebruiken, gaan we een basiseenheidstest maken:

Collectie syncCollection = Collections.synchronizedCollection (nieuwe ArrayList ()); Runnable listOperations = () -> {syncCollection.addAll (Arrays.asList (1, 2, 3, 4, 5, 6)); }; Thread thread1 = nieuwe Thread (listOperations); Thread thread2 = nieuwe Thread (listOperations); thread1.start (); thread2.start (); thread1.join (); thread2.join (); assertThat (syncCollection.size ()). isEqualTo (12); } 

Zoals hierboven getoond, is het creëren van een gesynchroniseerde weergave van de geleverde collectie met deze methode heel eenvoudig.

Om aan te tonen dat de methode daadwerkelijk een threadveilige verzameling retourneert, maken we eerst een aantal threads.

Daarna injecteren we een Runnable instantie in hun constructors, in de vorm van een lambda-uitdrukking. Laten we daar rekening mee houden Runnable is een functionele interface, dus we kunnen deze vervangen door een lambda-uitdrukking.

Ten slotte controleren we of elke thread effectief zes elementen toevoegt aan de gesynchroniseerde verzameling, dus de uiteindelijke grootte is twaalf.

3. Het synchronizedList () Methode

Evenzo vergelijkbaar met de synchronizedCollection () methode, kunnen we de synchronizedList () wrapper om een ​​gesynchroniseerd Lijst.

Zoals we zouden kunnen verwachten, de methode retourneert een thread-veilige weergave van het opgegeven Lijst:

Lijst syncList = Collections.synchronizedList (nieuwe ArrayList ());

Het is niet verwonderlijk dat het gebruik van de synchronizedList () methode lijkt bijna identiek aan zijn tegenhanger op een hoger niveau, synchronizedCollection ().

Daarom, zoals we net deden in de vorige unit-test, hebben we eenmaal een gesynchroniseerd Lijst, kunnen we verschillende threads spawnen. Nadat we dat hebben gedaan, zullen we ze gebruiken om toegang te krijgen tot / het doel te manipuleren Lijst op een draadveilige manier.

Als we bovendien een gesynchroniseerde verzameling willen herhalen en onverwachte resultaten willen voorkomen, moeten we expliciet onze eigen threadveilige implementatie van de lus bieden. Daarom kunnen we dat bereiken met een gesynchroniseerd blok:

Lijst syncCollection = Collections.synchronizedList (Arrays.asList ("a", "b", "c")); Lijst uppercasedCollection = nieuwe ArrayList (); Runnable listOperations = () -> {gesynchroniseerd (syncCollection) {syncCollection.forEach ((e) -> {uppercasedCollection.add (e.toUpperCase ());}); }}; 

In alle gevallen waarin we een gesynchroniseerde verzameling moeten herhalen, moeten we dit idioom implementeren. Dit komt doordat de iteratie op een gesynchroniseerde verzameling wordt uitgevoerd via meerdere aanroepen naar de verzameling. Daarom moeten ze worden uitgevoerd als een enkele atomaire bewerking.

Het gebruik van de gesynchroniseerd block zorgt voor de atomiciteit van de operatie.

4. Het synchronizedMap () Methode

De Collecties class implementeert een andere nette synchronisatie-wrapper, genaamd synchronizedMap (). We zouden het kunnen gebruiken om eenvoudig een gesynchroniseerd Kaart.

De methode retourneert een draadveilige weergave van het geleverde Kaart implementatie:

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

5. Het synchronizedSortedMap () Methode

Er is ook een tegenhanger-implementatie van de synchronizedMap () methode. Het heet synchronizedSortedMap (), die we kunnen gebruiken voor het maken van een gesynchroniseerd SortedMap voorbeeld:

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

6. Het synchronizedSet () Methode

Vervolgens gaan we verder met deze recensie, we hebben de synchronizedSet () methode. Zoals de naam al aangeeft, stelt het ons in staat om gesynchroniseerd te creëren Sets met minimale poespas.

De wrapper retourneert een threadveilige verzameling die wordt ondersteund door het opgegeven Set:

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

7. Het synchronizedSortedSet () Methode

Ten slotte is de laatste synchronisatie-wrapper die we hier laten zien synchronizedSortedSet ().

Net als bij andere wrapper-implementaties die we tot nu toe hebben beoordeeld, de methode retourneert een threadveilige versie van het opgegeven SortedSet:

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

8. Gesynchroniseerde versus gelijktijdige verzamelingen

Tot nu toe hebben we de synchronisatiewrappers van het verzamelingsraamwerk nader bekeken.

Laten we ons nu concentreren op de verschillen tussen gesynchroniseerde verzamelingen en gelijktijdige verzamelingen, zoals ConcurrentHashMap en BlockingQueue implementaties.

8.1. Gesynchroniseerde verzamelingen

Gesynchroniseerde verzamelingen bereiken draadveiligheid door intrinsieke vergrendeling, en de volledige verzamelingen worden vergrendeld. Intrinsieke vergrendeling wordt geïmplementeerd via gesynchroniseerde blokken binnen de methoden van de ingepakte verzameling.

Zoals we zouden verwachten, zorgen gesynchroniseerde verzamelingen voor gegevensconsistentie / -integriteit in omgevingen met meerdere threads. Ze kunnen echter een prestatieverbinding met zich meebrengen, omdat slechts één thread tegelijk toegang heeft tot de verzameling (ook bekend als gesynchroniseerde toegang).

Voor een gedetailleerde handleiding over het gebruik gesynchroniseerd methoden en blokken, lees dan ons artikel over het onderwerp.

8.2. Gelijktijdige collecties

Gelijktijdige verzamelingen (bijv. ConcurrentHashMap), bereiken draadveiligheid door hun gegevens in segmenten te verdelen. In een ConcurrentHashMapVerschillende threads kunnen bijvoorbeeld vergrendelingen op elk segment krijgen, zodat meerdere threads toegang hebben tot het Kaart tegelijkertijd (ook bekend als gelijktijdige toegang).

Gelijktijdige collecties zijn veel performanter dan gesynchroniseerde collecties, vanwege de inherente voordelen van gelijktijdige threadtoegang.

De keuze van het type draadveilige verzameling dat moet worden gebruikt, hangt dus af van de vereisten van elke use-case en moet dienovereenkomstig worden geëvalueerd.

9. Conclusie

In dit artikel hebben we dieper ingegaan op de set synchronisatiewrappers die zijn geïmplementeerd in het Collecties klasse.

Bovendien hebben we de verschillen tussen gesynchroniseerde en gelijktijdige collecties benadrukt en ook gekeken naar de benaderingen die ze implementeren om thread-veiligheid te bereiken.

Zoals gewoonlijk zijn alle codevoorbeelden die in dit artikel worden getoond, beschikbaar op GitHub.