Úvod do blokovania prúžkovania

1. Úvod

V tomto výučbe sa naučíme, ako dosiahnuť jemnozrnnú synchronizáciu, známu tiež ako Lock Striping, vzor pre manipuláciu so súčasným prístupom k dátovým štruktúram pri zachovaní dobrého výkonu.

2. Problém

HashMap nie je vláknovo bezpečná dátová štruktúra kvôli svojej nesynchronizovanej povahe. To znamená, že príkazy z prostredia s viacerými vláknami môžu mať za následok nekonzistenciu údajov.

Na prekonanie tohto problému môžeme pôvodnú mapu previesť pomocou Zbierky # synchronizedMap metóda alebo použiť HashTable dátová štruktúra. Oba vrátia implementáciu Mapa rozhranie, prichádzajú však za cenu výkonu.

Prístup definovania výlučného volá sa prístup cez dátové štruktúry s jedným objektom zámku hrubozrnná synchronizácia.

Pri implementácii hrubozrnnej synchronizácie musí byť každý prístup k objektu uskutočňovaný súčasne jedným vláknom. Nakoniec skončíme s postupnými prístupmi.

Naším cieľom je umožniť súbežným vláknam pracovať na dátovej štruktúre a zároveň zaistiť bezpečnosť vlákien.

3. Uzamknite prúžkovanie

Na dosiahnutie nášho cieľa použijeme vzor Lock Striping. Striping lock je technika, pri ktorej dochádza k uzamknutiu na niekoľkých segmentoch alebo pruhoch, čo znamená, že prístup do segmentu uzamkne iba tento segment a nie celú dátovú štruktúru.

Existuje niekoľko spôsobov, ako to urobiť:

  • Najprv by sme mohli na každú úlohu použiť zámok, čím by sme maximalizovali súbežnosť medzi úlohami - má však vyššiu pamäťovú stopu
  • Alebo by sme mohli pre každú úlohu použiť jeden zámok, ktorý využíva menej pamäte, ale zároveň znižuje výkonnosť súčasne

Aby nám Guava pomohla zvládnuť tento kompromis výkonu a pamäte, dodáva sa s triedou s názvom Pruhované. Je to podobné ako s logikou v ConcurrentHashMap, ale Pruhované trieda ide ešte ďalej tým, že obmedzuje synchronizáciu jednotlivých úloh pomocou semaforov alebo zámkov na opätovné zadanie.

4. Rýchly príklad

Urobme si krátky príklad, ktorý nám pomôže pochopiť výhody tohto modelu.

Porovnáme HashMap vs. ConcurrentHashMap a jeden zámok vs. pruhovaný zámok, výsledkom čoho sú štyri experimenty.

Pre každý experiment vykonáme súbežné čítanie a zápis na podklade Mapa. Líši sa to, ako pristupujeme ku každému vedru.

A za to vytvoríme dve triedy - SingleLock a StripedLock. Toto sú konkrétne implementácie abstraktnej triedy ConcurrentAccessExperiment to robí prácu.

4.1. Závislosti

Pretože budeme používať Guavu Pruhované triedy, pridáme guava závislosť:

 com.google.guava guava 28.2-jre 

4.2. Hlavný proces

Náš ConcurrentAccessExperiment trieda implementuje skôr opísané správanie:

verejná abstraktná trieda ConcurrentAccessExperiment {verejné konečné Mapa doWork (mapa mapy, int vlákna, int sloty) {CompletableFuture [] požiadavky = nové CompletableFuture [vlákna * sloty]; for (int i = 0; i <threads; i ++) {requests [slots * i + 0] = CompletableFuture.supplyAsync (putSupplier (mapa, i)); požiadavky [sloty * i + 1] = CompletableFuture.supplyAsync (getSupplier (mapa, i)); požiadavky [sloty * i + 2] = CompletableFuture.supplyAsync (getSupplier (mapa, i)); požiadavky [sloty * i + 3] = CompletableFuture.supplyAsync (getSupplier (mapa, i)); } CompletableFuture.allOf (requests) .join (); spiatočná mapa; } chránený abstraktný dodávateľ putSupplier (mapa, kľúč int); chránený abstraktný dodávateľ getSupplier (mapa, kľúč int); }

Je dôležité si uvedomiť, že keďže náš test je viazaný na procesor, obmedzili sme počet segmentov na niekoľko dostupných procesorov.

4.3. Súbežný prístup s ReentrantLock

Teraz implementujeme metódy pre naše asynchrónne úlohy.

Náš SingleLock trieda definuje jediný zámok pre celú dátovú štruktúru pomocou a ReentrantLock:

verejná trieda SingleLock rozširuje ConcurrentAccessExperiment {zámok ReentrantLock; public SingleLock () {lock = new ReentrantLock (); } chránený dodávateľ putSupplier (mapa mapy, kľúč int) {return (() -> {lock.lock (); vyskúšajte {návrat map.put ("kľúč" + kľúč, "hodnota" + kľúč);} konečne {zamknúť. unlock ();}}); } chránený dodávateľ getSupplier (mapa mapy, kľúč int) {return (() -> {lock.lock (); vyskúšajte {return map.get ("key" + key);} konečne {lock.unlock ();}} ); }}

4.4. Súbežný prístup s Pruhované

Potom StripedLock trieda definuje pruhovaný zámok pre každý segment:

verejná trieda StripedLock rozširuje ConcurrentAccessExperiment {pruhovaný zámok; verejné StripedLock (int vedierka) {lock = Striped.lock (vedierka); } chránený dodávateľ putSupplier (mapa mapy, kľúč int) {return (() -> {int bucket = key% stripedLock.size (); Lock lock = stripedLock.get (bucket); lock.lock (); vyskúšať {návratovú mapu .put ("kľúč" + kľúč, "hodnota" + kľúč);} nakoniec {lock.unlock ();}}); } chránený dodávateľ getSupplier (mapa mapy, kľúč int) {return (() -> {int bucket = key% stripedLock.size (); Lock lock = stripedLock.get (bucket); lock.lock (); vyskúšať {návratovú mapu .get ("kľúč" + kľúč);} nakoniec {lock.unlock ();}}); }}

Ktorá stratégia teda funguje lepšie?

5. Výsledky

Použijeme na to JMH (Java Microbenchmark Harness). Referenčné hodnoty nájdete prostredníctvom odkazu na zdrojový kód na konci tutoriálu.

Ak spustíme náš benchmark, uvidíme niečo podobné tomuto (všimnite si, že vyššia priepustnosť je lepšia):

Benchmark Mode Cnt Score Error Units ConcurrentAccessBenchmark.singleLockConcurrentHashMap thrpt 10 0,059 ± 0,006 ops / ms ConcurrentAccessBenchmark.singleLockHashMap thpt 10 0,061 ± 0,005 ops / ms ConcurrentAccessBenchmark.stripedLockConcurrentHashMap 0,00 

6. Závery

V tomto tutoriáli sme preskúmali rôzne spôsoby, ako môžeme dosiahnuť lepší výkon pomocou funkcie Lock Striping in Mapa-aké štruktúry. Vytvorili sme benchmark na porovnanie výsledkov s niekoľkými implementáciami.

Z našich referenčných výsledkov môžeme pochopiť, ako môžu rôzne súbežné stratégie významne ovplyvniť celkový proces. Vzor Striped Lock zaručuje značné zlepšenie, pretože u oboch dosahuje skóre ~ 10% navyše HashMap a ConcurrentHashMap.

Ako obvykle je zdrojový kód tohto tutoriálu k dispozícii na GitHub.