Een gids voor gelijktijdige wachtrijen in Java

1. Overzicht

In deze zelfstudie bespreken we enkele van de belangrijkste implementaties van gelijktijdige wachtrijen in Java. Raadpleeg onze Guide to the Java voor een algemene inleiding tot wachtrijen Wachtrij Interface artikel.

2. Wachtrijen

In toepassingen met meerdere threads moeten wachtrijen meerdere gelijktijdige producenten-consumenten-scenario's kunnen verwerken. De juiste keuze van een gelijktijdige wachtrij kan cruciaal zijn voor het bereiken van goede prestaties in onze algoritmen.

Ten eerste zullen we enkele belangrijke verschillen zien tussen een blokkerende wachtrij en een niet-blokkerende wachtrij. Vervolgens bekijken we enkele implementaties en best practices.

2. Blokkerende versus niet-blokkerende wachtrij

BlockingQueue aanbiedingen een eenvoudig draadveilig mechanisme. In deze wachtrij moeten threads wachten op de beschikbaarheid van de wachtrij. Producenten wachten op beschikbare capaciteit alvorens elementen toe te voegen, consumenten wachten tot de wachtrij leeg is. In die gevallen genereert de niet-blokkerende wachtrij een uitzondering of retourneert een speciale waarde, zoals nul of false.

Om dit blokkeermechanisme te bereiken, is de BlockingQueue interface stelt twee functies bloot bovenop de normale Wachtrij functies: leggen en nemen. Die functies zijn het equivalent van toevoegen en verwijderen in een standaard Wachtrij.

3. Gelijktijdig Wachtrij Implementaties

3.1. ArrayBlockingQueue

Zoals de naam suggereert, gebruikt deze wachtrij intern een array. Bijgevolg is het een begrensde wachtrij, wat betekent dat deze een vaste grootte heeft.

Een eenvoudige werkwachtrij is een voorbeeld van een use-case. Dit scenario is vaak een lage producent-consumentverhouding, waarbij we tijdrovende taken over meerdere werknemers verdelen. Omdat deze wachtrij niet oneindig kan groeien, de groottelimiet fungeert als een veiligheidsdrempel als geheugen een probleem is.

Over geheugen gesproken, het is belangrijk op te merken dat de wachtrij de array vooraf toewijst. Hoewel dit de doorvoer kan verbeteren, is it kan ook meer geheugen verbruiken dan nodig is. Een wachtrij met een grote capaciteit kan bijvoorbeeld lange tijd leeg blijven.

Ook de ArrayBlockingQueue gebruikt voor beide een enkel slot leggen en nemen operaties. Dit zorgt ervoor dat vermeldingen niet worden overschreven, wat ten koste gaat van een prestatiehit.

3.2. LinkedBlockingQueue

De LinkedBlockingQueue gebruikt een LinkedList variant, waarbij elk wachtrij-item een ​​nieuw knooppunt is. Hoewel dit de wachtrij in principe onbegrensd maakt, heeft deze nog steeds een harde limiet van Geheel getal.MAX_VALUE.

Aan de andere kant kunnen we de wachtrijgrootte instellen met behulp van de constructor LinkedBlockingQueue (int capaciteit).

Deze wachtrij gebruikt verschillende vergrendelingen voor leggen en nemen operaties. Als gevolg hiervan kunnen beide bewerkingen parallel worden uitgevoerd en de doorvoer verbeteren.

Sinds de LinkedBlockingQueue kan zowel begrensd als onbegrensd zijn, waarom zouden we de ArrayBlockingQueue over deze? LinkedBlockingQueue moet elke keer dat een item wordt toegevoegd aan of verwijderd uit de wachtrij, knooppunten toewijzen en ongedaan maken. Om deze reden is een ArrayBlockingQueue kan een beter alternatief zijn als de wachtrij snel groeit en snel krimpt.

De optreden van LinkedBlockingQueue wordt gezegd dat het onvoorspelbaar is. Met andere woorden, we moeten onze scenario's altijd profileren om ervoor te zorgen dat we de juiste datastructuur gebruiken.

3.3. PriorityBlockingQueue

De PriorityBlockingQueue is onze go-to-oplossing wanneer we items in een specifieke volgorde moeten consumeren. Om dit te bereiken, heeft de PriorityBlockingQueue gebruikt een op een array gebaseerde binaire heap.

Hoewel het intern een enkel vergrendelingsmechanisme gebruikt, is de nemen bediening kan gelijktijdig plaatsvinden met de leggen operatie. Het gebruik van een eenvoudig spinlock maakt dit mogelijk.

Een typische use case is het verbruiken van taken met verschillende prioriteiten. We willen niet dat een taak met een lage prioriteit in de plaats komt van een taak met een hoge prioriteit.

3.4. DelayQueue

We gebruiken een DelayQueuewanneer een consument alleen een vervallen artikel kan meenemen. Interessant is dat het een Prioriteits-rij intern om de items te bestellen op hun vervaldatum.

Aangezien dit geen wachtrij voor algemene doeleinden is, omvat deze niet zoveel scenario's als de ArrayBlockingQueue of de LinkedBlockingQueue. We kunnen deze wachtrij bijvoorbeeld gebruiken om een ​​eenvoudige gebeurtenislus te implementeren, vergelijkbaar met wat wordt gevonden in NodeJS. We plaatsen asynchrone taken in de wachtrij voor latere verwerking wanneer ze verlopen.

3.5. LinkedTransferQueue

De LinkedTransferQueue introduceert een overdracht methode. Terwijl andere wachtrijen doorgaans blokkeren bij het produceren of consumeren van artikelen, is de LinkedTransferQueuestelt een producent in staat te wachten op de consumptie van een artikel.

We gebruiken een LinkedTransferQueue wanneer we een garantie nodig hebben dat een bepaald item dat we in de wachtrij hebben geplaatst, door iemand is ingenomen. We kunnen ook een eenvoudig tegendrukalgoritme implementeren met behulp van deze wachtrij. Door producenten te blokkeren tot consumptie, consumenten kunnen de stroom van geproduceerde berichten sturen.

3.6. Synchrone wachtrij

Hoewel wachtrijen doorgaans veel items bevatten, is de Synchrone wachtrij zal altijd hoogstens één item hebben. Met andere woorden, we moeten de Synchrone wachtrij net zo een eenvoudige manier om gegevens tussen twee threads uit te wisselen.

Als we twee threads hebben die toegang nodig hebben tot een gedeelde status, synchroniseren we deze vaak met CountDownLatch of andere synchronisatiemechanismen. Door een Synchrone wachtrij, wij kunnen vermijd deze handmatige synchronisatie van threads.

3.7. ConcurrentLinkedQueue

De ConcurrentLinkedQueue is de enige niet-blokkerende wachtrij van deze gids. Bijgevolg biedt het een "wachtvrij" -algoritme waar toevoegen en poll zijn gegarandeerd draadveilig en keren onmiddellijk terug. In plaats van vergrendelingen gebruikt deze wachtrij CAS (Compare-And-Swap).

Intern is het gebaseerd op een algoritme van Eenvoudig, snel en praktisch Niet-blokkerende en blokkerende algoritmen voor gelijktijdige wachtrijen door Maged M. Michael en Michael L. Scott.

Het is een perfecte kandidaat voor moderne reactieve systemen, waar het gebruik van blokkerende datastructuren vaak verboden is.

Aan de andere kant, als onze consument in een lus blijft wachten, moeten we waarschijnlijk een blokkeerwachtrij kiezen als een beter alternatief.

4. Conclusie

In deze handleiding hebben we verschillende gelijktijdige implementaties van wachtrijen doorlopen en hun sterke en zwakke punten besproken. Met dit in gedachten zijn we beter uitgerust om efficiënte, duurzame en beschikbare systemen te ontwikkelen.