Introductie tot Lock Striping

1. Inleiding

In deze tutorial gaan we leren hoe we fijnmazige synchronisatie kunnen bereiken, ook wel bekend als Lock Striping, een patroon voor het omgaan met gelijktijdige toegang tot datastructuren met behoud van goede prestaties.

2. Het probleem

Hash kaart is geen threadveilige gegevensstructuur vanwege de niet-gesynchroniseerde aard ervan. Dat betekent dat opdrachten uit een omgeving met meerdere threads kunnen leiden tot inconsistentie van gegevens.

Om dat probleem te verhelpen, kunnen we ofwel de originele kaart converteren met Collecties # synchronizedMap methode of gebruik de HashTable data structuur. Beide retourneren een threadveilige implementatie van het Kaart interface, maar ze gaan ten koste van de prestaties.

De benadering om exclusief te definiëren toegang via datastructuren met een enkel vergrendelingsobject wordt genoemd grofkorrelige synchronisatie.

In een grofkorrelige synchronisatie-implementatie moet elke toegang tot het object per keer door één thread worden gemaakt. We hebben uiteindelijk opeenvolgende toegangen.

Ons doel is om gelijktijdige threads te laten werken aan de datastructuur en tegelijkertijd de thread-veiligheid te garanderen.

3. Vergrendel striping

Om ons doel te bereiken, gebruiken we het Lock Striping-patroon. Lock striping is een techniek waarbij de vergrendeling plaatsvindt op meerdere emmers of strepen, wat betekent dat toegang tot een emmer alleen die emmer vergrendelt en niet de volledige datastructuur.

U kunt dit op een aantal manieren doen:

  • Ten eerste kunnen we een vergrendeling per taak gebruiken, waardoor de gelijktijdigheid tussen taken wordt gemaximaliseerd - dit heeft echter een grotere geheugenvoetafdruk
  • Of we kunnen voor elke taak een enkele vergrendeling gebruiken, die minder geheugen gebruikt maar ook de prestaties bij gelijktijdigheid in gevaar brengt

Om ons te helpen deze afweging tussen prestatie en geheugen te beheren, wordt Guava geleverd met een klasse met de naam Gestreept. Het is vergelijkbaar met logica in ConcurrentHashMap, maar de Gestreept class gaat zelfs nog verder door de synchronisatie van verschillende taken te verminderen met behulp van semaforen of herintredende vergrendelingen.

4. Een snel voorbeeld

Laten we een kort voorbeeld geven om ons te helpen de voordelen van dit patroon te begrijpen.

We zullen het vergelijken Hash kaart vs. ConcurrentHashMap en een enkel slot versus een gestreept slot wat resulteerde in vier experimenten.

Voor elk experiment voeren we gelijktijdige lees- en schrijfbewerkingen uit op het onderliggende Kaart. Wat zal variëren, is hoe we toegang krijgen tot elke bucket.

En daarvoor maken we twee klassen - SingleLock en StripedLock. Dit zijn concrete implementaties van een abstracte klasse ConcurrentAccessExperiment dat doet het werk.

4.1. Afhankelijkheden

Omdat we Guava's gaan gebruiken Gestreept klasse, we voegen de guave afhankelijkheid:

 com.google.guava guave 28.2-jre 

4.2. Hoofdproces

Onze ConcurrentAccessExperiment class implementeert het eerder beschreven gedrag:

openbare abstracte klasse ConcurrentAccessExperiment {openbare definitieve Map doWork (Map map, int threads, int slots) {CompletableFuture [] verzoeken = nieuwe CompletableFuture [threads * slots]; voor (int i = 0; i <threads; i ++) {verzoeken [slots * i + 0] = CompletableFuture.supplyAsync (putSupplier (map, i)); verzoeken [slots * i + 1] = CompletableFuture.supplyAsync (getSupplier (kaart, i)); verzoeken [slots * i + 2] = CompletableFuture.supplyAsync (getSupplier (kaart, i)); verzoeken [slots * i + 3] = CompletableFuture.supplyAsync (getSupplier (kaart, i)); } CompletableFuture.allOf (verzoeken) .join (); kaart teruggeven; } beschermde samenvatting Leverancier putSupplier (Map map, int key); beschermde samenvatting Leverancier getSupplier (Map map, int key); }

Het is belangrijk op te merken dat, aangezien onze test CPU-gebonden is, we het aantal buckets hebben beperkt tot een veelvoud van de beschikbare processors.

4.3. Gelijktijdige toegang met ReentrantLock

Nu gaan we de methoden voor onze asynchrone taken implementeren.

Onze SingleLock class definieert een enkel slot voor de gehele datastructuur met behulp van een ReentrantLock:

openbare klasse SingleLock breidt ConcurrentAccessExperiment {ReentrantLock lock; openbare SingleLock () {lock = nieuwe ReentrantLock (); } beschermde leverancier putSupplier (Map map, int key) {return (() -> {lock.lock (); probeer {return map.put ("key" + key, "value" + key);} ten slotte {lock. ontgrendelen(); } }); } beschermde leverancier getSupplier (Map map, int key) {return (() -> {lock.lock (); probeer {return map.get ("key" + key);} eindelijk {lock.unlock ();}} ); }}

4.4. Gelijktijdige toegang met Gestreept

Dan de StripedLock klasse definieert een gestreept slot voor elke emmer:

public class StripedLock breidt ConcurrentAccessExperiment {Striped lock; public StripedLock (int buckets) {lock = Striped.lock (buckets); } beschermde leverancier putSupplier (Map map, int key) {return (() -> {int bucket = key% stripedLock.size (); Lock lock = stripedLock.get (bucket); lock.lock (); probeer {return map .put ("key" + key, "value" + key);} tenslotte {lock.unlock ();}}); } beschermde leverancier getSupplier (Map map, int key) {return (() -> {int bucket = key% stripedLock.size (); Lock lock = stripedLock.get (bucket); lock.lock (); probeer {return map .get ("key" + key);} eindelijk {lock.unlock ();}}); }}

Dus, welke strategie presteert beter?

5. Resultaten

Laten we JMH (de Java Microbenchmark Harness) gebruiken om erachter te komen. De benchmarks zijn te vinden via de broncodelink aan het einde van de tutorial.

Als we onze benchmark uitvoeren, kunnen we iets zien dat lijkt op het volgende (merk op dat een hogere doorvoer beter is):

Benchmark-modus Cnt Score Fout Eenheden ConcurrentAccessBenchmark.singleLockConcurrentHashMap thrpt 10 0,059 ± 0,006 ops / ms ConcurrentAccessBenchmark.singleLockHashMap thrpt 10 0,061 ± 0,005 ops / ms ConcurrentAccessBenchmark.stripedLockConcurrent 10 / ms. 

6. Conclusies

In deze tutorial hebben we verschillende manieren onderzocht om betere prestaties te bereiken met Lock Striping in Kaart-achtige structuren. We hebben een benchmark gemaakt om de resultaten met verschillende implementaties te vergelijken.

Op basis van onze benchmarkresultaten kunnen we begrijpen hoe verschillende gelijktijdige strategieën het algehele proces aanzienlijk kunnen beïnvloeden. Het Striped Lock-patroon levert een behoorlijke verbetering op, aangezien het bij beide ~ 10% extra scoort Hash kaart en ConcurrentHashMap.

Zoals gewoonlijk is de broncode voor deze tutorial beschikbaar op GitHub.


$config[zx-auto] not found$config[zx-overlay] not found