středa 20. června 2007

Synchronizace, JMM a další špeky - díl druhý

V prvním díle našeho povídání jsme si řekli něco málo k změnám paměťového modelu v Jave, který byl představen v rámci verze 5.0. Dnes se trochu blíže podíváme na zoubek tomu, co vězí za novými třídami v package java.util.concurrent. Všechny informace jsem čerpal z článků Briana Goetze, které naleznete na konci textu.

Pokud jste si stáhli Javu 5, možná jste zaznamenali package java.util.concurrent. Tento package obsahuje třídy pro bezpečnou a pohodlnou práci v prostředí multithread aplikací. Možná ovšem nevíte, že většina těchto tříd je implementována bez významného použíti synchronized sekcí. Tím pádem nedochází k tomu, že by thready zbytečně stály a nebo čekaly na zámek. Od Javy 5.0 je totiž možné využít neblokující algoritmy.

CAS

CAS zkratka pro Compare-And-Swap instrukci, která stojí za celým packagem java.util.concurrent, ale pěkně po pořádku. CAS je instrukce, která má tří argumenty paměťové místo M, předpokládanou hodnotu A a novou hodnotu B - CAS(M, A, B). Instrukce udělá to, že se podívá do paměti na místo M, z něj si přečte aktuální hodnotu, kterou porovná s předpokládanou hodnotou A. Pokud jsou stejné, nastaví na místo M novou hodnotu B, pokud se hodnoty nerovnají, neprovede se nic.

To podstatné co zbývá o CAS instrukci říct je, že se jedná o atomickou instrukci. Nemůže tak nastat případ, kdy dvě vlákna paralelně mění jedno paměťové místo. Je tak vyloučeno, že by mohlo dojít k tomu, že se ve vláknu X provede porovnání aktuální a předpokládané hodnoty a před nastavením nové hodnoty by vlákno Y tuto hodnotu změnilo. Sémantika CAS instrukce je v podstatě stejná jako synchronized s tím rozdílem, že je to na úrovni hardwaru namísto JVM. Z toho vyplývá mnohem lepší výkonnost.

Podpora CAS instrukce byla přidána od Javy 5.0 a v API je zpřístupněna v rámci package java.util.concurrent.atomic, kde jsou různá primitiva jako například AtomicInteger či AtomicReference, která modelují CAS instrukci pro různé datové typy. Díky CAS je možné nahradit synchronizaci a to tak, že implementace využije optimistický přístup.

Představme si jednoduchý counter implementovaný pomocí synchronized bloku.

     
publi class Counter {
  private int counter;
  
  public synchronized int inc() {
    return counter++;
  }
}      
     
    

Oproti tomu s využitím AtomicIntegeru.

     
publi class Counter {
  private final AtomicInteger counter = new AtomicInteger();
  
  public int inc() {
     return counter.incrementAndGet();
  }
}      
     
    

Podívejme se na kód metody incrementAndGet (za metodou compareAndSet si představte naší CAS instrukci, metoda get vrací aktuální hodnotu).

    
public final int incrementAndGet() {
  for (;;) {
    int current = get();
    int next = current + 1;
    if (compareAndSet(current, next))
    return next;
  }
}    
     
    

Jak je vidět z kódu, pokud se mezi získáním aktuální hodnoty a její inkrementací změní její hodnota dojde znova k provedení celého kolečka. Takhle se pokračuje dokud se danému vláknu nepodaří atomicky inkrementovat hodnotu. Na stejném principu (CAS) jsou založeny i další neblokující algoritmy, které byly použity pro implementaci concurrent tříd.

Změny v Jave 5.0, ať už se jedná o JMM a nebo CAS, umožnily vytvořit optimalizované třídy pro práci v konkurenčním prostředí. Jejich implementace založená na neblokujících algoritmech a nové sémantice volatile nabízí ve většině případů mnohem větší výkon než použití klasického synchronized.

V případě, že budete mít potřebu vytvářet thread safe objekty (mapy, listy apod.), tak doporučuji prozkoumat package java.util.concurrent. Ušetří vám to totiž spoustu času s hledáním případných problémů a bude to mít pozitivní vliv na výkon aplikace.