Gyakorlati alapok III.
A többszálú programozás alapjai (multithread)
Néhány alapvető szálmetódus
.start()
Amint az előző fejezetben említésre került és láthattuk is, a szálat ez a metódus indítja. Természetesen lehetőségünk van arra, hogy először létrehozzuk a szálat, majd később futtassuk...
Szal1 szal1 =
new Szal1("Szal 1");
szal1.start();
...a fenti parancsokkal:
public class Szal1 extends Thread {
public Szal1(String nev) {
super(nev);
}
public void run() {
for(int i = 1; i <= 10; i++) {
System.out.println(i + ". ciklus - "
+ getName());
}
System.out.println(getName() + " kesz!");
}
}
public class Main {
public static void main (String[] args) {
Szal1 szal1 = new Szal1("Szal 1");
szal1.start();
}
}
Végeredmény:
1. ciklus - Szal 1
2. ciklus - Szal 1
3. ciklus - Szal 1
4. ciklus - Szal 1
5. ciklus - Szal 1
6. ciklus - Szal 1
7. ciklus - Szal 1
8. ciklus - Szal 1
9. ciklus - Szal 1
10. ciklus - Szal 1
Szal 1 kesz!
.stop()
A metódus minden további kérdezés nélkül leállítja a szálat. Éppen ebben
rejlik használatának veszélye, hiszen fennáll a szál működésének
félbeszakítása, amely adatvesztést és egyéb zavarokat okozhat. Érdemes tehát
nagyon körültekintően használni, vagy hagyni a szál természetes elhalását,
amely a run() metódusban írt kódok problémamentes
lefutását jelenti.
.getState()
Ez a metódus tudósít minket a szál pillanatnyi futási állapotáról. Ezek a
következők lehetnek:
-
NEW – a szál létrejött, de még nem fut,
-
RUNNABLE – a szál fut és nincs blokkolva vagy várakoztatva,
-
BLOCKED – a szál blokkolva van,
-
WAITING – a szál várakozik az alábbi metódusok valamelyikének hívására:
-
Object.wait(),
-
Thread.join(),
-
LockSupport.park(),
-
-
TIMED_WAITING - a szál időzített metódus hívására várakozik,
-
TERMINATED – a szál megszüntetve (kilépett a run() metódusból).
NEW - RUNNABLE
A metódus alapstátuszainak elsődleges vizsgálatához illesszük azt a program fő pontjaira, azaz irassuk ki a szál pillanatnyi futási állapotait. A fő pontok:
-
a run() metódus elejéhez és végéhez,
-
a main() metódusban a példányosítás után, de a start() előtt,
-
valamint a start() után:
public class Szal1 extends Thread {
public Szal1(String nev) {
super(nev);
}
public void run() {
System.out.println("run() eleje: " + getState());
for(int i = 1; i <= 10; i++) {
System.out.println(i + ". ciklus - " + getName());
}
System.out.println(getName() + " kesz!");
System.out.println("run() vege: " + getState());
}
}
public class Main {
public static void main (String[] args) {
Szal1 szal1 = new Szal1("Szal 1");
System.out.println("main() - peldanyositas utan, start()
elott: " + szal1.getState());
szal1.start();
System.out.println("main() - start() utan: " +
szal1.getState());
}
}
Végeredmény:
main() - peldanyositas utan, start() elott: NEW
main() - start() utan: RUNNABLE
run() eleje: RUNNABLE
1. ciklus - Szal 1
2. ciklus - Szal 1
3. ciklus - Szal 1
4. ciklus - Szal 1
5. ciklus - Szal 1
6. ciklus - Szal 1
7. ciklus - Szal 1
8. ciklus - Szal 1
9. ciklus - Szal 1
10. ciklus - Szal 1
Szal 1 kesz!
run() vége: RUNNABLE
Mivel a helyzet nem bonyolult és a szál gond nélkül lefutott, első körben 2
állapotot tudtunk tettenérni, a NEW és RUNNABLE státuszokat.
BLOCKED - TIMED_WAITING - TERMINATED
Ezen állapotok tettenérése kissé bonyolultabb ügy. Először a TERMINATED vonatkozásában gondoljunk abba bele, hogy vajon egy elhalt szál ki tudja-e jelenteni magáról, hogy "én meg vagyok szüntetve (TERMINATED)"? Természetesen nem, elvégre a halott szál egészen az, ezért a szálfutás körülményeinek menedzseléséhez egy olyan másik osztályt is találnunk kell, amely hatékonyan képes a folyamatokat felügyelni. Ez az osztály a külön fejezetben ismertetett Runtime, amely fejezetet érdemes a Java-tanulónak eme fejezet továbbolvasása előtt felkeresnie.
A Runtime osztálynak van egy különleges metódusa, neve: addShutdownHook(Thread hook). Ez a metódus lehetőséget ad arra, hogy külön szálként tetszőleges kód fusson le a JVM közvetlen leállítása előtt. Ez például arra lehet jó, ha a JVM rutinszerű leállása mellett szükségünk van további, egyedi rutinokat tartalmazó leállítási műveletekre. Az alábbiakban csak szálstátusz meghatározására fogjuk felhasználni.
A TERMINATED státusz invokációjához tehát az előbbieknél jelentős mértékben kell futtatható Java-kódunkat átszabni:
-
a szálállapot vizsgálatát külön metódusba tesszük (szalAllapot()),
-
a külön szálat (Szal) belső osztályba tesszük, így a Szal külön, származtatott osztályba kerül, ám a szalAllapot() metódus minden funkció számára látható marad,
-
az összes kód így 1 oldalra kerül,
-
lambda-kifejezés segítségével (A funkcionális interfész és a lambda-kifejezések című fejezet) "röptében" aktiválunk egy Runtime-objektumot, hogy kifutott és/vagy halott szálról, mintegy "kívülről" jelentsen státuszt,
-
egyébiránt a szál ugyanazt végzi mint eddig, csak rövidebb ideig (5 ciklusban),
-
emlékezzünk vissza: a sleep() metódust kötelező try-catch blokkokba kell tennünk.
public class Main {
public static class Szal extends Thread {
@Override
public void run() {
szalAllapot("Szal elindult", this);
for(int i = 1; i <= 5; i++) {
System.out.println(i + ". ciklus - " + getName());
}
System.out.println(getName() + "
kesz!");
szalAllapot("Szal befejezi", this);
}
static void szalAllapot(String uzenet, Thread szal) {
System.out.printf("%s - %s allapot: %s%n", uzenet,
szal.getName(), szal.getState());
}
public static void main(String[] args) {
Szal szal = new Szal();
szal.setName("Szal");
szalAllapot("Szal start() elott", szal);
szal.start();
szalAllapot("Szal start() utan", szal);
szalAllapot("main() alszik 1 masodpercig", szal);
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
szalAllapot("main() felebred", szal);
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
szalAllapot("Leallas", szal);
}));
}
}
}
Végeredmény:
Szal start() elott - Szal allapot: NEW
Szal start() utan - Szal allapot: RUNNABLE
main() alszik 1 masodpercig - Szal allapot: RUNNABLE
Szal elindult - Szal allapot: RUNNABLE
1. ciklus - Szal
2. ciklus - Szal
3. ciklus - Szal
4. ciklus - Szal
5. ciklus - Szal
Szal kesz!
Szal befejezi - Szal allapot: RUNNABLE
main() felebred - Szal allapot: TERMINATED
Leallas - Szal allapot: TERMINATED
Másodsorban pedig a BLOCKED és a TIMED_WAITING státuszokkal kell foglalkoznunk, amely a szál blokkolását, valamint várakozását jelenti egy időzített metódus hívására. A torlódásos helyzetet, vele ezen státuszok felröppenését a várakozások megfelelő ütköztetésével tudjuk a rendszerből kiprovokálni. A kódmódosítás a fentiek szerinti, de a Szal folyamatból kivesszük a ciklikus kiírást, helyette kap egy kötelező, 3 másodperces elmerengést a Thread.sleep(3000) metódus hívásával.
public class Main {
private static class Szal extends Thread {
@Override
public void run() {
szalAllapot("Szal alszik 3 masodpercig", this);
try {
Thread.sleep(3000);
} catch (Exception e) {
e.printStackTrace();
System.out.println(getName() + "
kesz!");
szalAllapot("Szal befejezi", this);
}
}
static void szalAllapot(String uzenet, Thread szal) {
System.out.printf("%s - %s allapot: %s%n", uzenet,
szal.getName(), szal.getState());
}
public static void main(String[] args) {
Szal szal = new Szal();
szal.setName("Szal");
szalAllapot("Szal start() elott", szal);
szal.start();
szalAllapot("Szal start() utan", szal);
szalAllapot("main() alszik 1 masodpercig", szal);
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
szalAllapot("main() felebred", szal);
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
szalAllapot("Leallas", szal);
}));
}
}
}
Végeredmény:
Szal start() elott - Szal allapot: NEW
Szal start() utan - Szal allapot: RUNNABLE
main() alszik 1 masodpercig - Szal allapot: BLOCKED
Szal alszik 3 masodpercig - Szal allapot: RUNNABLE
main() felebred - Szal allapot: TIMED_WAITING
Leallas - Szal allapot: TERMINATED
Itt jegyezzük meg, hogy a BLOCKED státusz nem mindig kerül kiírásra, azaz a szálfutás a fenti kódban nem mindig blokkolódik. Ez a JVM pillanatnyi hangulatától, azaz attól függ, hogy a szálakat aktuálisan miképpen csoportosítja, illetve futtatja le.
.isAlive()
Ez a metódus boolean típusú függvényként
(TRUE – FALSE) igaz értéket ad vissza, ha a szál el lett indítva, de még nem
lett leállítva, azonban hamis, ha a szál státusza NEW vagy TERMINATED. Alapszintű teszteléséhez használjuk fel a
függvényt a
getState() függvény közelségében:
public class Szal1 extends Thread {
public Szal1(String nev) {
super(nev);
}
public void run() {
System.out.println("run() eleje - " + getState());
System.out.println("run() eleje - is alive? " + isAlive());
for(int i = 1; i <= 10; i++) {
System.out.println(i + ". ciklus - "
+ getName());
}
System.out.println(getName() + " kesz!");
System.out.println("run() vege - " + getState());
System.out.println("run() vege - is alive? " + isAlive());
}
}
public class Main {
public static void main (String[] args) {
Szal1 szal1 = new Szal1("Szal 1");
System.out.println("main() - peldanyositas utan, start()
elott - " + szal1.getState());
System.out.println("main() - peldanyositas utan, start()
elott - is alive? " + szal1.isAlive());
szal1.start();
System.out.println("main() - start() utan - is alive? " +
szal1.isAlive());
}
}
Végeredmény:
main() - peldanyositas utan, start() elott - NEW
main() - peldanyositas utan, start() elott - is alive? false
main() - start() utan - is alive? true
run() eleje - RUNNABLE
run() eleje - is alive? true
1. ciklus - Szal 1
2. ciklus - Szal 1
3. ciklus - Szal 1
4. ciklus - Szal 1
5. ciklus - Szal 1
6. ciklus - Szal 1
7. ciklus - Szal 1
8. ciklus - Szal 1
9. ciklus - Szal 1
10. ciklus - Szal 1
Szal 1 kesz!
.sleep()
Már említett módon a szál futása megszakítható vagy külső behatásra
megszakadhat. Egy szál akkor kerül nem futtatott-nem futtatható állapotba:
-
ha meghívásra kerül a sleep() metódus,
-
ha meghívásra kerül a wait() metódus, hogy az várjon egy bizonyos feltétel teljesülésére,
-
ha a szál blokkolt egy I/O művelet miatt.
Nézzük meg a sleep() egyszerű használatát a run() függvényen belül. A metódus Csipkerózsika-szerűen elaltatja a szálat 5 másodperc hosszan (sleep(5000)). Vegyük észre, hogy az altatást kötelező try-catch blokkba kell tennünk:
public class Szal1 extends Thread {
public Szal1(String nev) {
super(nev);
}
public void run() {
System.out.println("run() eleje - " + getState());
System.out.println("run() eleje - is alive? " + isAlive());
for(int i = 1; i <= 10; i++) {
System.out.println(i + ". ciklus - "
+ getName());
}
try {
System.out.println("Szál 1
várakozik... - "+ getState() + "... Is alive? " + isAlive());
sleep(5000);
System.out.println(getName() + "
kesz!");
System.out.println("run() vege - " +
getState());
System.out.println("run() vege - is
alive? " + isAlive());
} catch (InterruptedException e){
}
}
}
public class Main {
public static void main (String[] args) {
Szal1 szal1 = new Szal1("Szal 1");
System.out.println("main() - peldanyositas utan, start()
elott - " + szal1.getState());
System.out.println("main() - peldanyositas utan, start()
elott - is alive? " + szal1.isAlive());
szal1.start();
System.out.println("main() - start() utan - is alive? " +
szal1.isAlive());
}
}
Végeredmény:
main() - peldanyositas utan, start() elott - NEW
main() - peldanyositas utan, start() elott - is alive? false
main() - start() utan - is alive? true
run() eleje - RUNNABLE
run() eleje - is alive? true
1. ciklus - Szal 1
2. ciklus - Szal 1
3. ciklus - Szal 1
4. ciklus - Szal 1
5. ciklus - Szal 1
6. ciklus - Szal 1
7. ciklus - Szal 1
8. ciklus - Szal 1
9. ciklus - Szal 1
10. ciklus - Szal 1
Szal 1 várakozik... - RUNNABLE... Is alive? true
Szal 1 kesz!
run() vege - RUNNABLE
run() vege - is alive? True
set.Priority()
Ezen függvénnyel a szálak között azok futtatásakor prioritási
(elsődlegességi) sorrend állítható be. Az érték 1 és 10 között lehetséges,
ahol 10 a leginkább elsődleges,
1 pedig a legkevésbé az. A prioritási értékeket a
get.Priority() függvénnyel tudjuk lekérdezni.
Alapértelmezésben minden szál az 5 értéket kapja,
amely prioritási szempontból nem súlyozott.
Az alábbi futtatható Java-kódban a Szal2 szálat
állítottuk elsődlegesnek (10) és azt vizsgáljuk,
hogy valóban előbb fog-e végezni Szal1-nél (1).
A különbségek azonban még nem igazán jönnek ki alacsony szálterhelések
mellett, ezért a szálak
for
ciklusait 1000-re állítottam:
public class Szal1 extends Thread {
public Szal1(String nev) {
super(nev);
}
public void run() {
for(int i = 1; i <= 1000; i++) {
System.out.println(i + ". ciklus - "
+ getName());
}
System.out.println(getName() + " kesz!");
}
}
public class Szal2 extends Thread {
public Szal2(String nev) {
super(nev);
}
public void run() {
for(int i = 1; i <= 1000; i++) {
System.out.println(i + ". ciklus - "
+ getName());
}
System.out.println(getName() + " kesz!");
}
}
public class Main {
public static void main (String[] args) {
Szal1 szal1 = new Szal1("Szal 1");
Szal2 szal2 = new Szal2("Szal 2");
szal2.setPriority(10);
szal1.setPriority(1);
szal1.start();
szal2.start();
}
}
Végeredmény:
…
993. ciklus - Szal 2
994. ciklus - Szal 2
995. ciklus - Szal 2
996. ciklus - Szal 2
997. ciklus - Szal 2
998. ciklus - Szal 2
999. ciklus - Szal 2
1000. ciklus - Szal 2
Szal 2 kesz!
145. ciklus - Szal 1
146. ciklus - Szal 1
147. ciklus - Szal 1
148. ciklus - Szal 1
149. ciklus - Szal 1
150. ciklus - Szal 1
151. ciklus - Szal 1
152. ciklus - Szal 1
153. ciklus - Szal 1
…
A terhelés hatására Szal2 valóban jóval hamarabb
végez, hiszen ekkor Szal1 még csak a 145.
ciklusnál tart.