Java programozók körében elterjedt tévhit, hogy a synchronized módosítóval ellátott metódusok vagy blokkok által bezárt programsorokat csak egy szál futtathatja egy időben. A valóság az, hogy a kérdéses osztály példányaira vonatkozik a synchronized, ha az osztályból van kettő példányunk, amelyek egy közös static változót használnának, akkor mit sem ér a synchronized... Hogy ne a levegőbe beszéljek, nézzünk egy konkrét példát, vegyünk például egy osztályt, amelynek egy statikus változója és két metódusa van:
public class Test { private static Integer counter=0; public synchronized void addToCounter(Integer number) { counter += number; } public Integer getCounter() { return counter; } }
Láthatjuk, hogy az addToCounter metódus szinkronizált blokkban van, így azt a counter nevű változóhoz több szál nem férhet hozzá:
public class ThreadA implements Runnable { @Override public void run() { try { Test test = new Test(); for (int count = 0; count < 100; count++) { test.addToCounter(1); Thread.sleep(7); } System.out.println(test.getCounter()); } catch (InterruptedException ex) { Logger.getLogger(ThreadA.class.getName()).log(Level.SEVERE, ex.toString(), ex); } } }
Ahogy fentebb látszik, létrehoztunk egy ThreadA (és egy ThreadB) osztályt, amelyekkel több szálon tudjuk futtatni a Test osztály "szálbiztos" addToCounter metódusát:
public class Main { public Main() { ThreadA a=new ThreadA(); ThreadB b=new ThreadB(); new Thread(a).start(); new Thread(b).start(); } public static void main(String[] args) { new Main(); } }
Az eremény meglepő lehet:
197 198
Elvileg az eremény 200 kellene lennie az utolsónak lefutó szál esetén, hiszen minden hozzáadás szinkronizált volt... azonban a synchronized két külön példányra volt értelmezett, így a statikus változót két szál akár egyidőben is módosíthatta. A fent vázolt probléma abból a tényból adódik, hogy a synchronized módosítóval jelölt metódus (lásd fentebb) az alábbi kóddal lesz azonos:
public void addToCounter(Integer number) { synchronized (this) { counter += number; } }
Ahogy a kódból is kiolvasható: a kód szálzárása példány szintű lesz (this), s ebből adódóan csak akkor jön elő a probléma, ha statikus változót akarunk egy nem statikus - de szinkronizált metódusból elérni. A megoldás egyszerű, kell készítenünk egy statikus és szinkronizált metódust, vagy osztály szintű szálzárást állítunk be:
public void addToCounter(Integer number) { synchronized (getClass()) { counter += number; } }
Érdemes figyelni ilyen apróságokra, hiszen egy többszálú programban hibát keresni nem egyszerű, s egy tévhit még nehezebbé teheti a hiba okának felderítését.
4 Comments
Peter Verhas
Ha jól sejtem elírás a
ThreadB
, és helyesenAuth Gábor AUTHOR
Nem, direkt van két szálimplementáció (nehogy azt mondja valaki, hogy azért téveszt a számláló mert azonos osztály példányaival számolok), de akár így is működhetne, a probléma tekintetében mindegy...
Peter Verhas
Akkor aztért az valahol le kellene írni, hogy a
ThreadB
copy-paste ugyanaz, mint aThreadA
. Amúgy didaktikailag azt tartanám helyesnek, haThreadA
lenne mind a két objektum, és meg lehet említeni, ha valakinek nem lenne egyértelmű, hogy ha a két osztály nem azonos, csak mind a kettő párhuzamosan matat, akkor is ez van.Esetleg érdemes lenne ezt a cikket frissíteni és hozzátenni az atomi osztályokról és CAS-ról is egy megoldást. Mert amúgy a példafeladathoz a mai Java-val már nem kell synchronized.
Auth Gábor AUTHOR
Leírtam...
Bárkinek nyitott a lehetőség, hogy tartalmat adjon hozzá...