Advanced   Java   Services Prozesse, die Methoden join() und yield() der Klasse Thread


Eltern- und Kindprozesse

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:


Die nichtstatischen Methoden join(), join(long millis) von Thread

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.


Beispiel 1

Die Threadklasse

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";
   }
}

Die Mainklasse

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

Einige Abläufe

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

yield()

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.


Beispiel 2

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.


Die Threadklasse
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();
   }
}

Die Mainklasse
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();
   }
}

Die Ausgabe

Zwei mögliche Ausgaben

12122121212121212112
12121212121212121221

Zum Vergleich zwei mögliche Ausgaben bei auskommentiertem yield()

1111111111
2222222222
11111112222222222
111