Gyakorlati alapok III.
A többszálú programozás alapjai (multithread)
A volatile kulcsszó
Az előző, A synchronized kulcsszó című fejezetben megoldást találtunk arra a speciális esetre, amely során 2 vagy több szál ugyanazon erőforrást kívánja használni, még pedig úgy, hogy a végeredmény elvárt módon ne legyen káosz (inkonzisztencia), holtpont vagy egyéb hibaüzenet. Ezt a problémát a synchronized kulcsszó bevezetésével, az erőforráshoz való hozzáférések szinkronizációjával sikerült megoldanunk. A kulcsszó nem használható osztályok, interfészek, valamint változók esetében.
A többszörös hozzáférés azonban nemcsak metódusokra, hanem változókra is igen veszélyes.
Ez a probléma olyan változók esetében fordul elő, amelyek egy megosztott objektumban, osztályszinten vannak deklarálva...
public class megosztottObjektum {
public int szamlalo = 0;
}
...és amelyet ezért egyszerre több, külön processzormagon futó szál is képes használni (amely használat alatt elsősorban írási és olvasási jogot értek). A probléma azonban nem az eléréssel, hanem a deklarált változó pillanatnyi értékeivel van, amelyek ilyenkor nem mindig láthatók minden szál számára. Ez viszont előre nem látható eredményeket produkálhat. A helyzet demonstrálására tekintsük meg az alábbi ábrát:
Forrás - Source: tutorials.jenkov.com
A változó nemcsak a főmemóriában (main memory) foglal helyet, hanem róla lokális másolatok is keletkeznek a gyorsító memóriában (cache memory), még pedig mindegyik dedikált szálon, amely változó értékeit a szálak átállíthatják anélkül, hogy az láthatóvá válna a "globális" főmemóriában.
A változó értékeinek folyamatos láthatóságát minden jogosult szál számára Java 5 verziótól kezdve a volatile (= ingadozó) kulcsszó bevezetése garantálja.
Az ilyen feltételekkel deklarált változót tehát reflexszerűen így érdemes deklarálnunk:
public class megosztottObjektum {
public volatile int szamlalo = 0;
}
Nézzük is meg gyorsan az ilyen módon módosított, futtatható Java-kódunkat, amelyben 2 szál egy közös változó értékeit növeli. Elvárt eredményünk a következő volna:
A valtozo tartalma: 1
A valtozo tartalma: 2
A valtozo tartalma: 3
A valtozo tartalma: 4
A valtozo tartalma: 5
A valtozo tartalma: 6
A valtozo tartalma: 7
A valtozo tartalma: 8
A valtozo tartalma: 9
A valtozo tartalma: 10
class Szal1 extends Thread {
munkaOsztaly mo;
Szal1(munkaOsztaly mo){
this.mo = mo;
this.setName("Szal1");
}
public void run(){
mo.metodus(this.getName());
}
}
class Szal2 extends Thread {
munkaOsztaly mo;
Szal2(munkaOsztaly mo){
this.mo = mo;
this.setName("Szal2");
}
public void run() {
mo.metodus(this.getName());
}
}
class munkaOsztaly{
public volatile int szamlalo = 1;
void metodus(String nev) {
for(int i = 1; i <= 5; i++) {
try{
Thread.sleep(1000);
}catch(Exception
e){System.out.println(e);
}
System.out.println(nev + " - A valtozo tartalma: " +
szamlalo);
szamlalo++;
}
}
}
class Main {
public static void main(String args[]) {
munkaOsztaly mo = new munkaOsztaly();
Szal1 szal1 = new Szal1(mo);
Szal2 szal2 = new Szal2(mo);
szal1.start();
szal2.start();
}
}
Végeredmény:
Szal1 - A valtozo tartalma: 1
Szal2 - A valtozo tartalma: 1
Szal2 - A valtozo tartalma: 3
Szal1 - A valtozo tartalma: 3
Szal2 - A valtozo tartalma: 5
Szal1 - A valtozo tartalma: 5
Szal1 - A valtozo tartalma: 7
Szal2 - A valtozo tartalma: 7
Szal1 - A valtozo tartalma: 9
Szal2 - A valtozo tartalma: 9
Az elvárt eredmények sajnos nem megfelelők. Vajon mi a hiba?
Hiba nincs, csak értelmezési zavar, hiszen a volatile kulcsszó kizárólag a változó tartalmának láthatóságára ad megoldást, nem pedig metódusok szinkronizációjára, arra, amelyről éppen az előző, A synchronized kulcsszó című fejezetben beszéltünk. Ha szükséges, metódusokat ezután is szinkronizálnunk kell. Ebből következően a metódus synchronized kulcsszóval való felvértezésével (synchronized void metodus(String nev)), meg fogjuk kapni elvárt eredményeinket:
class Szal1 extends Thread {
munkaOsztaly mo;
Szal1(munkaOsztaly mo){
this.mo = mo;
this.setName("Szal1");
}
public void run(){
mo.metodus(this.getName());
}
}
class Szal2 extends Thread {
munkaOsztaly mo;
Szal2(munkaOsztaly mo){
this.mo = mo;
this.setName("Szal2");
}
public void run() {
mo.metodus(this.getName());
}
}
class munkaOsztaly{
public volatile int szamlalo = 1;
synchronized void metodus(String nev) {
for(int i = 1; i <= 5; i++) {
try{
Thread.sleep(1000);
}catch(Exception
e){System.out.println(e);
}
System.out.println(nev + " - A valtozo tartalma: " +
szamlalo);
szamlalo++;
}
}
}
class Main {
public static void main(String args[]) {
munkaOsztaly mo = new munkaOsztaly();
Szal1 szal1 = new Szal1(mo);
Szal2 szal2 = new Szal2(mo);
szal1.start();
szal2.start();
}
}
Végeredmény:
Szal1 - A valtozo tartalma: 1
Szal1 - A valtozo tartalma: 2
Szal1 - A valtozo tartalma: 3
Szal1 - A valtozo tartalma: 4
Szal1 - A valtozo tartalma: 5
Szal2 - A valtozo tartalma: 6
Szal2 - A valtozo tartalma: 7
Szal2 - A valtozo tartalma: 8
Szal2 - A valtozo tartalma: 9
Szal2 - A valtozo tartalma: 10
A fenti példákban 2 szál együttesen használt fel egy számlálót. A Java-rendszerben azonban -pontosan ilyen esetekre-, elérhető egy sokkal biztonságosabb megoldás is az AtomicInteger nevű osztályban, amely a...
java.util.concurrent.atomic.AtomicInteger
...elérési úton található. Az alábbi futtatható Java-kód ilyen típusú objektummal lett módosítva a munkaOsztaly osztályban:
java.util.concurrent.atomic.AtomicInteger
class Szal1
extends Thread {
munkaOsztaly mo;
Szal1(munkaOsztaly mo){
this.mo = mo;
this.setName("Szal1");
}
public void run(){
mo.metodus(this.getName());
}
}
class Szal2 extends Thread {
munkaOsztaly mo;
Szal2(munkaOsztaly mo){
this.mo = mo;
this.setName("Szal2");
}
public void run() {
mo.metodus(this.getName());
}
}
class munkaOsztaly{
AtomicInteger atomicInteger = new
AtomicInteger(1);
synchronized void metodus(String nev) {
for(int i = 1; i <= 5; i++) {
try{
Thread.sleep(1000);
}catch(Exception
e){System.out.println(e);
}
System.out.println(nev + " - A valtozo tartalma: " +
atomicInteger);
atomicInteger.getAndIncrement();
}
}
}
class Main {
public static void main(String args[]) {
munkaOsztaly mo = new munkaOsztaly();
Szal1 szal1 = new Szal1(mo);
Szal2 szal2 = new Szal2(mo);
szal1.start();
szal2.start();
}
}
Végeredmény:
Szal1 - A valtozo tartalma: 1
Szal1 - A valtozo tartalma: 2
Szal1 - A valtozo tartalma: 3
Szal1 - A valtozo tartalma: 4
Szal1 - A valtozo tartalma: 5
Szal2 - A valtozo tartalma: 6
Szal2 - A valtozo tartalma: 7
Szal2 - A valtozo tartalma: 8
Szal2 - A valtozo tartalma: 9
Szal2 - A valtozo tartalma: 10
Utolsó gondolatként jegyezzük meg, hogy a volatile kulcsszó nem használható osztályok, interfészek, valamint metódusok esetében.