neděle 17. června 2007

Synchronizace, JMM a další špeky - díl první

Nejsem si úplně jistý, kolik z Vás zaznamenalo všechny změny kolem problematiky konkurenčního zpracování, které představila Java 5. Byla to skutečně Java 5, která představila některé nové vlastnosti, které umožňují efektivní práci multithread aplikací. Pokud jste novinky v této oblasti nesledovali, pak Vám tento článek poslouží jako takové malé intro.

Java Memory Model

Java Memory Model (JMM) definuje vztah mezi proměnnými programu a tím jak jsou uloženy a získávány z paměti počítače. JMM je součástí Java Language Specification a po pravdě není potřeba, aby jej vývojáři znali. Nicméně z jeho sémantiky vyplývá několik věcí, které si musí každý vývojář uvědomit pokud pracuje s multithread (vícevláknovou) aplikací.

Říkám několik věcí, ale ve skutečnosti jsou to dva podstatné fakty a to viditelnost proměnných mezi vlákny a přeskupení pořadí zápisu a čtení proměnných. Každá proměnná leží v paměti avšak během práce s ní může docházet k různým výkonovým optimalizacím, například se proměnná uloží do lokální cache či CPU registru. Tyto optimalizace může dělat HotSpot (JVM runtime compiler) a nebo to může být hardwarová optimalizace. JMM určuje sémantiku, která definuje jak se mohou tyto optimalizace promítnout vzhledem k práci s proměnnými v Jave a to především z hlediska threadů.

Mnoho vývojářů vidí v rámci řešení konkurenčních problémů pouze jeden aspekt klíčového slova synchronized a to atomicitu v podobě vzájemného vyloučení vláken nad kritickou sekcí. Jenže se synchronized je spojena viditelnost proměnných mezi vlákny a přeskupení pořadí jejich čtení zápisu, to jest přímá souvislost s JMM.

     
class Reordering {
  int x = 0, y = 0;
  public void writer() {
    x = 1;
    y = 2;
  }

  public void reader() {
    int r1 = y;
    int r2 = x;
  }
}
     
    

Představme si dvě vlákna, která pracují s výše uvedeným kouskem kódu a řekněme, že jedno vlákno nám provádí metodu writer. Vzápětí přijde druhé vlákno, které začne provádět metodu reader. Předpoklad, že po provedení int r1 = y; (r1 == 2) automaticky platí, že r2 == 1 není správný. První vlákno mohlo resp. kompiler mohl přehodit pořadí těch dvou příkazů, což může udělat v případě, že to neovlivní běh samotného vlákna. Druhou možností je, že vlákno vykonávající metodu reader vidí resp. pracuje s nacacheovanými hodnotami x,y.

Proto, aby nedošlo k přehození těchto příkazů a zároveň k zaručení viditelnosti aktuálních hodnot přes všechny vlákna slouží také klíčové slovo synchronized. Synchronized je z pohledu JMM definováno tak, že při získání zámku dojde načtení čerstvých hodnot z hlavní paměti a při uvolnění zámku dojde k jejich zápisu do hlavní paměti. Zároveň v rámci synchornized bloku nemůže dojít k takové optimalizaci, že by paměť zůstala v nekonzistentním stavu. Díky synchronized tak vlákna vždy vidí skutečný obraz paměti.

Pokud se ptáte jak s tímto tématem souvisí Java 5, tak s ním souvisí tak, že upravuje některé nepřesnosti a mezery v JMM specifikaci. V předchozí verzi JMM (do verze Javy 1.5) mohlo například dojít k tomu, že vlákna viděli na objekt, který nebyl plně zinicializován viz. double checked locking. Stejně tak mohlo dojít k tomu, že dvě vlákna mohla vidět final proměnnou ve dvou rozdílných stavech.

Další změnou v JMM specifikaci je rozšířená sémantika proměnné označené klíčovým slovem volatile. Volatile o proměnné říká, že thread musí vždy pracovat s proměnnou v hlavní paměti. Starý model ovšem pro volatile umožňoval, že mohlo dojít k přeskupení non volatile práce s proměnnou a obyčejnou proměnnou.

     
class Reordering {
  int x = 0
  volatile y = 0;
  public void writer() {
    x = 1;
    y = 2;
  }

  public void reader() {
    if(y == 2) {
      //x ==1
    }
  }
}
     
  

Díky úpravě JMM ve verzi 1.5 sémantice volatile je garantováno, že vlákno vidí i předchozí paměťové změny. Volatile má totiž po novu stejné chování (viditelnost, reorder) při práci s pamětí jako synchronized.

Celý nový JMM je pokrytý JSR 133, ale doporučuji si spíše přečíst některý z následujících článků, protože těm porozumí i běžný vývojář.

V dalším článku se podíváme na témata, která už budou blíže reálnému vývoji a to na alternativní způsoby k synchronizaci, které nabízí balík java.util.concurrent a jeho neblokující algoritmy.