24.5 Monitoring - Locking
 
T. Hoare
erfand u.a. den Quick-Sort-Algorithmus

 

mehr Infos zu Hoare

 

Wir haben gelernt, dass man Threads dazu benutzt, nebenläufige Prozesse zu programmieren. Stellen wir uns einmal folgende Situation vor: Zwei Personen, nennen wir sie Herr Fritz und Frau Annerose, gehen im gleichen Supermarkt einkaufen. Sicher sind das zwei unabhängige Prozesse, die auch unterschiedlich lange dauern können. Beide benutzen für ihren Einkauf einen Einkaufswagen. Es ist klar, wenn Herr Fritz einen Einkaufswagen benutzt, kann dieser nicht gleichzeitig von Frau Anneliese benutzt werden. Sie benutzt einen anderen oder, falls keiner mehr frei ist, wartet sie bis wieder ein Einkaufswagen frei wird. Versuchen wir die Situation in einem Programm zu modellieren, würden wir die (Supermarkt)Kunden als Threads anlegen; Herr Fritz und Frau Annerose wären dann Instanzen dieser Klasse. Um den Einkauf tätigen zu können, greifen beide Kunden auf Objekte der Klasse Einkaufswagen, z.B. wagen1, wagen2, etc. zu. Damit es dabei zu keinen Konflikten (Inkonstistenzen) kommt, Herr Fritz und Frau Annerose also nicht gleichzeitig auf den gleichen Einkaufswagen z.B. wagen1 zugreifen, müssen wir diese Objekte vor gleichzeitigem Zugriff schützen. Zur Lösung dieses Problems benutzen wir einen sog. Monitor. Ein Monitor 'überwacht' ein Objekt z.B. wagen1 und stellt fest, dass ein anderes Objekt, nämlich fritz auf wagen1 zugreift. Den Monitor können wir nun dazu veranlassen dass er keinen zweiten Zugriff auf wagen1, etwa durch annerose zulässt, Es ist also so, als besitze fritz für eine gewisse Zeit die Schlüsselgewalt über wagen1, der dann für den Zugriff anderer Objekte gesperrt ist (Lockin). Der wagen1 wird erst wieder frei, wenn fritz seinen Einkauf beendet und den Wagen zurückgebracht hat. Er gibt den Schlüssel zurück (Schloss: lock). Ein anderes Objekt kann die Schlüsselgewalt übernehmen. Der Begriff des Monitors geht auf C.A.R.Hoare zurück, der dieses Konzept zum ersten mal 1978 in seinem Aufsatz "Communicating Sequencential Processes" beschrieben hat.
 
Locking in Java In Java können wir das Monitor-Konzept auf zwei Weisen realisieren. Beide Fälle werden mit Hilfe des Java-Schlüsselwortes synchronized realisiert
  • Sperren eines Blockes innerhalb einer Methode

    public void set(WertTyp wert){
      ...
       synchronized (obj){
         ... //geschützte Anweisungen
       }
    }


    Der Monitor kapselt einen Bereich, in dem die Anweisungen stehen, auf die nur ein Thread-Objekt Zugriff haben soll.
     
  • Um den Zugriff auf ein Objekt selbst zu schützen, können wir eine ganze Methode sperren. Dazu setzen wir in den Methodenkopf vor den Rückgabetyp (bzw. void) das reservierte Wort synchronized. z.B.

    public synchronized void set(WertTyp wert){
    ...
    }

    Die Methode kann jetzt nur noch von einem Thread aufgerufen werden. Für andere ist sie gesperrt.

Abgewiesene Threads  werden in eine Art 'Warteschleife' gespeichert. Hat die Methode des zugelassenen Threads 'ihren Job' erfüllt, der Thread gibt seinen 'Schlüssel wieder ab' und die Sperre ist wieder aufgehoben, kommt der nächste Thread aus der Warteschlange an die Reihe. Die Reihenfolge kann man dadurch beeinflussen, dass man einzelnen Threads unterschiedliche Prioritäten gibt.
 

Download:
Monitor0.java
 
public class Monitor0 extends Thread {



  public Monitor0(String name){

    super(name);

  }



  public void run(){

    schlafen();

  }

  

  public static void schlafen(){

    for (int i = 1; i<=10; i++){

      try{

        Thread.sleep((int)(Math.random()*1000));

        System.out.println(Thread.currentThread().getName()

                           +": "+i);

      }

      catch(InterruptedException exp){

        return;

      }

    }

  }

  

  public static void main (String[] args) {

    Monitor0 t1 = new Monitor0("Thread t1: ");

    Monitor0 t2 = new Monitor0("Thread t2: ");

    t1.start();

    t2.start();

  }

}

Bemerkungen Monitor0.java ist den Threadsprogrammen der letzten Unterkapitel nachempfunden. Die Klasse Monitor0 erbt von Threads, Den Standartkonstruktor ersetzen wir durch einen, der es erlaubt, der Instanz von Monitor0 auch einen Namen zugeben. Wir überschreiben wie gewohnt die run()-Methode, die Monitor0 von Thread geerbt hat. Sie ruft die statische Methode schlafen() auf, die wie gewohnt implementiert ist. Schließlich werden in der main(..)-Methode zwei Instanzen t1 und t2 dieser Klasse erzeugt und, sie sind ja Threads, gestartet. Das Ergebnis, wie es unten dargestellt ist, ist uns bekannt.
 
Ausgabe Thread t1: : 1
Thread t1: : 2
Thread t1: : 3
Thread t1: : 4
Thread t2: : 1
Thread t2: : 2
Thread t2: : 3
Thread t1: : 5
Thread t1: : 6
Thread t1: : 7
 
Thread t2: : 4
Thread t1: : 8
Thread t1: : 9
Thread t2: : 5
Thread t1: : 10
Thread t2: : 6
Thread t2: : 7
Thread t2: : 8
Thread t2: : 9
Thread t2: : 10
Veränderung Wir verändern nun unser Programm und wenden synchronized auf einen Block an. Die Methode getClass() aus der Klasse Object liefert das Objekt, das run() gerade aufruft und durch eine  mit synchronized statisch Methode (hier schlafen()) gesperrt wird. Die Veränderung ist im Quelltext gelb unterlegt.

 

Download Monitor1.java
public class Monitor1 extends Thread {



  public Monitor1(String name){

    super(name);

  }



  public void run(){

    synchronized(getClass()){

       schlafen();           

    }                        

  }

  

  public static void schlafen(){

    for (int i = 1; i<=10; i++){

      try{

        Thread.sleep((int)(Math.random()*1000));

        System.out.println(Thread.currentThread().getName()

                           +": "+i);

      }

      catch(InterruptedException exp){

        return;

      }

    }

  }



  public static void main (String[] args) {

      Monitor1 t1 = new Monitor1("Thread t1: ");

      Monitor1 t2 = new Monitor1("Thread t2: ");

      t1.start();

      t2.start();

  }

}
Bemerkungen Der Thread t2 versucht auf schlafen() zuzugreifen, während diese noch 'für den Thread t1 tätig ist'. synchronized verhindert dies und lässt den zweiten Thread warten, bis der erste seinen Aufruf von schlafen() beendet hat.  Die Ausgabe belegt dies: Zuerst kommt t1 voll zum Zuge (linke Spalte) danach erst t2 (rechte Spalte)
 
Ausgabe Thread t1: : 1
Thread t1: : 2
Thread t1: : 3
Thread t1: : 4
Thread t1: : 5
Thread t1: : 6
Thread t1: : 7
Thread t1: : 8
Thread t1: : 9
Thread t1: : 10
 
Thread t2: : 1
Thread t2: : 2
Thread t2: : 3
Thread t2: : 4
Thread t2: : 5
Thread t2: : 6
Thread t2: : 7
Thread t2: : 8
Thread t2: : 9
Thread t2: : 10

 
Alternative Alternativ können wir die ganze Methode schlafen() mit einem Monitor versehen und dadurch vor doppeltem Zugriff schützen
 
Download:
Monitor2.java
public class Monitor2 extends Thread {



  public Monitor2(String name){

    super(name);

  }



  public void run(){

    schlafen();

  }

  

  public static synchronized void schlafen(){

    for (int i = 1; i<=10; i++){

      try{

        Thread.sleep((int)(Math.random()*500));

        System.out.println(Thread.currentThread().getName()

                           +": "+i);

      }

      catch(InterruptedException exp){

        return;

      }

    }

  }

  

  public static void main (String[] args) {

    Monitor2 t1 = new Monitor2("Thread t1: ");

    Monitor2 t2 = new Monitor2("Thread t2: ");

    t1.start();

    t2.start();

  }

}
Ausgabe Die Ausgabe ist die gleiche, wie in Monitor1.java
 
  In der Regel wird man beim Programmieren die zweite Variante verwenden. Auf die erste greift man zu, wenn man den Aufruf einer Methode für einen zweiten Thread sperren will, die Methode aber in einer Klasse liegt, die man selbst nicht geschrieben und auf deren Quelltext man keinen Zugriff hat.
zu 24.6 Einkaufen - Ein Beispiel
zur Startseite www.pohlig.de  (C) MPohlig 2004