Advanced Java Services | Prozesse, die Methoden join() und yield() der Klasse Thread |
Wenn ein Programm startet gibt es nur einen Prozeß bzw. Thread, den main-Thread. Von da aus können weitere Prozesse
erzeugt werden. In diesem Zusammenhang ist der main-Thread der Elternprozeß und die weiteren Prozesse sind Kindprozesse,
usw. Die Bezeichnung Thread für die Javaklasse java.lang.Thread führt zu einer begrifflichen Ungenauigkeit. Man muß
nämlich unterscheiden zwischen einem Objekt vom Typ Thread und dem von diesem Objekt mit Hilfe der Methode start()
erzeugten Prozeß, den man auch Thread nennt.
Zunächst wird ein Objekt vom Typ Thread erzeugt, dies geschieht zwangsläufig
in irgendeinem Prozeß, z.Bsp. in main. In main wird dann mit Hilfe von start() ein neuer Prozeß (Thread) erzeugt.
start() wird dabei vom Elternprozeß aufgerufen und teilt der JVM mit, daß die Methode run() in einem eigenen Prozess
laufen soll. Die JVM sorgt dann in Zusammenarbeit mit dem Betriebssystem für den Aufruf der Methode run().
Der Aufruf
von start() findet also im Elternprozess statt, die Methode run() läuft dann aber im Kindprozess, die beiden
Methoden laufen also immer in verschiedenen Prozessen. Trotzdem kann man aus run() heraus auf den Datenteil
des Threadobjektes zugreifen.
Es ist wichtig diesen Weg einzuhalten und die Methode run() nicht direkt aufzurufen.
Ein direkter Aufruf von run() erzeugt keinen neuen Thread.
Des weiteren kann zu einem Threadobjekt die Methode start nur ein einziges Mal aufgerufen werden.
Ein erneuter Aufruf von start führt zu einer IllegalThreadStateException.
Beim Zugriff auf den Datenteil des Threadobjekts verhält sich die Methode run() wie jede
andere nichtstatische Methode.
Zusammenfassend:
Mit diesen einfach zu verwendeten Methoden kann ein Thread A auf die Beendigung eines anderen Threads B warten, etwa um das Ergebnis von B zu übernehmen. join() blockiert also den weiteren Ablauf von A. Will man nicht eine unbestimmte Zeit warten, so kann man die Wartezeit begrenzen, in dem man eine maximale Wartezeit in Millisekunden übergibt. Realisiert wird join() mit Hilfe der Methoden wait() und notify() aus der Klasse Object. Neben diesen beiden Methoden gibt es noch eine weitere Methode "join(long millis, int nanos)" mit der man die Wartezeit noch genauer angeben kann, die aber dasselbe Verhalten zeigt wie die Methode mit einem Parameter. Um auf einen anderen Thread warten zu können muß der Wartende den Thread als Javaobjekt kennen. Das folgende einfache Beispiel demonstriert die bisher erwähnten Eigenschaften.
Zunächst die Threadklasse. Das Beispiel verwendet die statische Methode currentThread() mit der man jederzeit feststellen kann, in welchem Prozeß sich ein Programmteil befindet.
class Task extends Thread { private String data = null; public Task() { System.out.println(Thread.currentThread().getName()); } /** * @return data */ public String getData() { return data; } /* * läuft in einem eigenen Prozess; */ @Override public void run() { System.out.println(Thread.currentThread().getName()); Timestamp timestamp = new Timestamp(System.currentTimeMillis()); System.out.println("begin: "+timestamp); // this is a timeconsuming task long l1 = 123456789; long l2 = 987654321; long l3, l4 ; for(int x=0; x < Integer.MAX_VALUE; x++) { for(int z=0; z < Integer.MAX_VALUE; z++) { l3 = l1*l2; l4 = l3/l2; l4 = l3/l2; } } Timestamp timestamp2 = new Timestamp(System.currentTimeMillis()); System.out.println("end : "+timestamp2); data = "data"; } }
Da man join bzw. timedJoin durch eine Interruptanforderung unterbrechen kann, erzwingt der Compiler ein try-catch.
public class JoinDemo { /** */ public static void main(String[] args) { System.out.println("JoinDemo"); Task task = new Task(); System.out.println("task.getData = " + task.getData()); task.start(); try { // main wartet auf task TimeUnit.SECONDS.timedJoin(task, 6); // 6 sekunden warten // ältere form //task.join(6000); //task.join(); // warten ohne timeout if (task.getData() == null) System.out.println("keine daten angekommen"); else System.out.println("daten angekommena = " + task.getData()); } catch(InterruptedException ex) { // join kann eine CheckedException werfen ex.printStackTrace(); } } // end main } // end main class
main wartet 6 sekunden
JoinDemo main task.getData = null Thread-0 begin: 2020-03-10 14:35:59.005 end : 2020-03-10 14:36:04.113 daten angekommena = data
main wartet 5 sekunden
JoinDemo main task.getData = null Thread-0 begin: 2020-03-10 14:37:23.823 keine daten angekommen end : 2020-03-10 14:37:28.97
Mit yield() (to yield - abgeben, abtreten) gibt ein Prozeß bekannt, daß er auf die Abarbeitung durch den Prozessor verzichtet. Er verhält sich somit fair und gibt anderen wartenden Prozessen eine Chance. Er wird dann vom Scheduler sofort wieder in die Warteschlange eingereiht und bewirbt sich daher erneut um den Prozessor. Allerdings ist yield() nur ein Vorschlag, wie das folgende Zitat aus der API belegt:
"A hint to the scheduler that the current thread is willing to yield its current use of a processor. The scheduler is free to ignore this hint."
Meistens wird jedoch ein yield() berücksichtigt wie das folgende kleine Beispiel zeigt.
Ein Thread gibt zehnmal seinen Namen aus und teilt mit, daß er sich nach jeder Ausgabe kurz zurückziehen möchte. Main legt zwei Threadobjekte an, vergibt als Namen die Ziffern 1 und 2 und startet die Threads. Zum Vergleich werden die Ausgaben bei auskommentiertem yield() gezeigt.
class Yield extends Thread { @Override public void run() { for(int i=0; i<10; i++) { System.out.print(Thread.currentThread().getName()); yield(); } System.out.println(); } }
public class YieldDemo { public static void main(String[] args) { Yield yield1 = new Yield(); Yield yield2 = new Yield(); yield1.setName("1"); yield2.setName("2"); yield1.start(); yield2.start(); } }
Zwei mögliche Ausgaben
12122121212121212112
12121212121212121221
Zum Vergleich zwei mögliche Ausgaben bei auskommentiertem yield()
1111111111 2222222222
11111112222222222 111