From 33613a85afc4b1481367fbe92a17ee59c240250b Mon Sep 17 00:00:00 2001 From: Sven Eisenhauer Date: Fri, 10 Nov 2023 15:11:48 +0100 Subject: add new repo --- .../hjp5/html/k100147.html | 875 +++++++++++++++++++++ 1 file changed, 875 insertions(+) create mode 100644 Master/Reference Architectures and Patterns/hjp5/html/k100147.html (limited to 'Master/Reference Architectures and Patterns/hjp5/html/k100147.html') diff --git a/Master/Reference Architectures and Patterns/hjp5/html/k100147.html b/Master/Reference Architectures and Patterns/hjp5/html/k100147.html new file mode 100644 index 0000000..2c61f43 --- /dev/null +++ b/Master/Reference Architectures and Patterns/hjp5/html/k100147.html @@ -0,0 +1,875 @@ + + + +Handbuch der Java-Programmierung, 5. Auflage + + + + + + + + + +
 Titel  + Inhalt  + Suchen  + Index  + DOC  +Handbuch der Java-Programmierung, 5. Auflage +
 <<  +  <   +  >   + >>  + API  +Kapitel 22 - Multithreading +
+
+ + + + +

22.4 Synchronisation +

+
+ +
+ + + + +

22.4.1 Synchronisationsprobleme

+ +

+Wenn man sich mit Nebenläufigkeit beschäftigt, muss man +sich in aller Regel auch mit Fragen der Synchronisation nebenläufiger +Prozesse beschäftigen. In Java erfolgt die Kommunikation zweier +Threads auf der Basis gemeinsamer Variablen, die von beiden Threads +erreicht werden können. Führen beide Prozesse Änderungen +auf den gemeinsamen Daten durch, so müssen sie synchronisiert +werden, denn andernfalls können undefinierte Ergebnisse entstehen. + +

+Wir wollen uns als einleitendes Beispiel ein kleines Programm ansehen, +bei dem zwei Threads einen gemeinsamen Zähler hochzählen: + + +

+ + + + + +
+ +
+001 /* Listing2209.java */
+002 
+003 public class Listing2209
+004 extends Thread
+005 {
+006   static int cnt = 0;
+007 
+008   public static void main(String[] args)
+009   {
+010     Thread t1 = new Listing2209();
+011     Thread t2 = new Listing2209();
+012     t1.start();
+013     t2.start();
+014   }
+015 
+016   public void run()
+017   {
+018     while (true) {
+019       System.out.println(cnt++);
+020     }
+021   }
+022 }
+
+
+Listing2209.java
+ +Listing 22.9: Zwei Zählerthreads

+ +

+Läßt man das Programm eine Weile laufen, könnte es +beispielsweise zu folgender Ausgabe kommen: + +

+0
+1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+33    <-- Nanu? Wo ist die 32?
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+32    <-- Ach so, hier!
+58
+59
+
+ + +

+Beide Prozesse greifen unsynchronisiert auf die gemeinsame Klassenvariable +cnt zu. Da die Operation System.out.println(cnt++); +nicht atomar ist, kommt es zu dem Fall, dass die Operation +mitten in der Ausführung unterbrochen wird und der Scheduler +mit dem anderen Thread fortfährt. Erst später, wenn der +unterbrochene prozess wieder Rechenzeit erhält, kann er seinen +vor der Unterbrechung errechneten Zählerwert von 32 ausgeben. +Sein Pendant war in der Zwischenzeit allerdings bereits bis 56 fortgefahren. +Um diese Art von Inkonsistenzen zu beseitigen, bedarf es der Synchronisation +der beteiligten Prozesse. + + + + +

22.4.2 Monitore

+ +

+Zur Synchronisation nebenläufiger Prozesse hat Java das Konzept +des Monitors implementiert. Ein Monitor ist die Kapselung eines +kritischen Bereichs (also eines Programmteils, +der nur von jeweils einem prozess zur Zeit durchlaufen werden darf) +mit Hilfe einer automatisch verwalteten Sperre. Diese Sperre wird +beim Betreten des Monitors gesetzt und beim Verlassen wieder zurückgenommen. +Ist sie beim Eintritt in den Monitor bereits von einem anderen prozess +gesetzt, so muss der aktuelle prozess warten, bis der Konkurrent die +Sperre freigegeben und den Monitor verlassen hat. + +

+Das Monitor-Konzept wird mit Hilfe des in die Sprache integrierten +Schlüsselworts synchronized +realisiert. Durch synchronized +kann entweder eine komplette Methode oder ein Block innerhalb einer +Methode geschützt werden. Der Eintritt in den so deklarierten +Monitor wird durch das Setzen einer Sperre auf einer Objektvariablen +erreicht. Bezieht sich synchronized +auf eine komplette Methode, wird als Sperre der this-Pointer +verwendet, andernfalls ist eine Objektvariable explizit anzugeben. + + + + +

Anwendung von synchronized auf einen Block von Anweisungen

+ +

+Wir wollen uns diese Art der Verwendung an einem Beispiel ansehen, +welches das oben demonstrierte Synchronisationsproblem löst. +Die naheliegende Lösung, die Anweisung System.out.println(cnt++); +durch einen synchronized-Block +auf der Variablen this +zu synchronisieren, funktioniert leider nicht. Da der Zeiger this +für jeden der beiden Threads, die ja unterschiedliche Instanzen +repräsentieren, neu vergeben wird, wäre für jeden Thread +der Eintritt in den Monitor grundsätzlich erlaubt. + +

+Statt dessen verwenden wir die (in Abschnitt 43.2.2 +erläuterte) Methode getClass, +die uns ein Klassenobjekt beschafft (ein und dasselbe für alle +Instanzen), mit dem wir die Klassenvariable cnt +schützen können: + + +

+ + + + + +
+ +
+001 /* Listing2210.java */
+002 
+003 public class Listing2210
+004 extends Thread
+005 {
+006   static int cnt = 0;
+007 
+008   public static void main(String[] args)
+009   {
+010     Thread t1 = new Listing2210();
+011     Thread t2 = new Listing2210();
+012     t1.start();
+013     t2.start();
+014   }
+015 
+016   public void run()
+017   {
+018     while (true) {
+019       synchronized (getClass()) {
+020         System.out.println(cnt++);
+021       }
+022     }
+023   }
+024 }
+
+
+Listing2210.java
+ +Listing 22.10: Synchronisation von Threads mit Klassenobjekten

+ +

+Nun werden alle Zählerwerte in aufsteigender Reihenfolge ausgegeben. + + + + +

Anwendung von synchronized auf eine Methode

+ +

+Ein anderer Fall ist der, bei dem der Zugriff auf ein Objekt selbst +synchronisiert werden muss, weil damit zu rechnen ist, dass mehr als +ein Thread zur gleichen Zeit das Objekt verwenden will. + +

+Im folgenden werden die potentiellen Probleme am Beispiel eines Zählerobjekts +erläutert, dessen Aufgabe es ist, einen internen Zähler +zu kapseln, auf Anforderung den aktuellen Zählerstand zu liefern +und den internen Zähler zu inkrementieren. Hierbei handelt es +sich um eine Aufgabe, die beispielsweise in der Datenbankprogrammierung +sehr häufig vorkommt, um Schlüsselnummern zu generieren. + +

+Typischerweise wird das Synchronisationsproblem dadurch verschärft, +dass die Verwendung des Zählers einige vergleichsweise langsame +Festplattenzugriffe erforderlich macht. In unserem Beispiel wird der +Zähler von fünf Threads verwendet. Die Langsamkeit und damit +die Wahrscheinlichkeit, dass der Scheduler die Zugriffsoperation unterbricht, +wird in unserem Beispiel durch eine Sequenz eingestreuter Fließkommaoperationen +erhöht: + + +

+ + + + + +
+ +
+001 /* Listing2211.java */
+002 
+003 class Counter2211
+004 {
+005   int cnt;
+006 
+007   public Counter2211(int cnt)
+008   {
+009     this.cnt = cnt;
+010   }
+011 
+012   public int nextNumber()
+013   {
+014     int ret = cnt;
+015     //Hier erfolgen ein paar zeitaufwändige Berechnungen, um
+016     //so zu tun, als sei das Errechnen des Nachfolgezählers
+017     //eine langwierige Operation, die leicht durch den
+018     //Scheduler unterbrochen werden kann.
+019     double x = 1.0, y, z;
+020     for (int i= 0; i < 1000; ++i) {
+021       x = Math.sin((x*i%35)*1.13);
+022       y = Math.log(x+10.0);
+023       z = Math.sqrt(x+y);
+024     }
+025     //Jetzt ist der Wert gefunden
+026     cnt++;
+027     return ret;
+028   }
+029 }
+030 
+031 public class Listing2211
+032 extends Thread
+033 {
+034   private String name;
+035   private Counter2211 counter;
+036 
+037   public Listing2211(String name, Counter2211 counter)
+038   {
+039     this.name = name;
+040     this.counter = counter;
+041   }
+042 
+043   public static void main(String[] args)
+044   {
+045     Thread[] t = new Thread[5];
+046     Counter2211 cnt = new Counter2211(10);
+047     for (int i = 0; i < 5; ++i) {
+048       t[i] = new Listing2211("Thread-"+i,cnt);
+049       t[i].start();
+050     }
+051   }
+052 
+053   public void run()
+054   {
+055     while (true) {
+056       System.out.println(counter.nextNumber()+" for "+name);
+057     }
+058   }
+059 }
+
+
+Listing2211.java
+ +Listing 22.11: Eine unzureichend synchronisierte Zählerklasse

+ +

+Das Ergebnis des Programms ist - wie nicht anders zu erwarten - schlecht, +denn es werden sehr viele doppelte Schlüssel produziert. Ein +Beispiellauf brachte bereits in den ersten 15 Aufrufen 6 doppelte +Zählerwerte: + +

+10 for Thread-2
+11 for Thread-4
+10 for Thread-0
+10 for Thread-1
+11 for Thread-2
+11 for Thread-3
+12 for Thread-4
+13 for Thread-0
+14 for Thread-1
+15 for Thread-2
+16 for Thread-3
+17 for Thread-4
+18 for Thread-0
+19 for Thread-1
+20 for Thread-2
+
+ + +

+Auch hier gibt es eine einfache Lösung für das Synchronisationsproblem. +Eine einfache Markierung der Methode nextNumber +als synchronized +macht diese zu einem Monitor und sorgt dafür, dass der komplette +Code innerhalb der Methode als atomares Programmfragment behandelt +wird. Eine Unterbrechung des kritischen Abschnitts durch einen anderen +Thread ist dann nicht mehr möglich: + + +

+ + + + +
+ +
+001 public synchronized int nextNumber()
+002 {
+003   int ret = cnt;
+004   //Hier erfolgen ein paar zeitaufwändige Berechnungen, um so
+005   //zu tun, als sei das Errechnen des Nachfolgezählerstandes
+006   //eine langwierige Operation, die leicht durch den
+007   //Scheduler unterbrochen werden kann.
+008   double x = 1.0, y, z;
+009   for (int i= 0; i < 1000; ++i) {
+010     x = Math.sin((x*i%35)*1.13);
+011     y = Math.log(x+10.0);
+012     z = Math.sqrt(x+y);
+013   }
+014   //Jetzt ist der Wert gefunden
+015   cnt++;
+016   return ret;
+017 }
+
+
+ +Listing 22.12: Synchronisieren der Zählermethode

+ +

+Durch das synchronized-Attribut +wird beim Aufruf der Methode die Instanzvariable this +gesperrt und damit der Zugriff für andere Threads unmöglich +gemacht. Erst nach Verlassen der Methode und Entsperren von this +kann nextNumber wieder von anderen +Threads aufgerufen werden. +

+ + + + + + + + + +
+ +

+Diese Art des Zugriffschutzes wird in Java von vielen Klassen verwendet, +um ihre Methoden thread-sicher zu machen. Nach Aussage der +Sprachspezifikation kann davon ausgegangen werden, dass die gesamte +Java-Klassenbibliothek in diesem Sinne thread-sicher ist. Dies gilt +allerdings nicht für die Swing-Klassen.

+ + + + +
 Hinweis 
+
+ + + + +

22.4.3 wait und notify

+ +

+Neben dem Monitorkonzept stehen mit den Methoden wait +und notify +der Klasse Object +noch weitere Synchronisationsprimitive zur Verfügung. Zusätzlich +zu der bereits erwähnten Sperre, die einem Objekt zugeordnet +ist, besitzt ein Objekt nämlich auch noch eine Warteliste. +Dabei handelt es sich um eine (möglicherweise leere) Menge von +Threads, die vom Scheduler unterbrochen wurden und auf ein Ereignis +warten, um fortgesetzt werden zu können. + +

+Sowohl wait +als auch notify +dürfen nur aufgerufen werden, wenn das Objekt bereits gesperrt +ist, also nur innerhalb eines synchronized-Blocks +für dieses Objekt. Ein Aufruf von wait +nimmt die bereits gewährten Sperren (temporär) zurück +und stellt den prozess, der den Aufruf von wait +verursachte, in die Warteliste des Objekts. Dadurch wird er unterbrochen +und im Scheduler als wartend markiert. Ein Aufruf von notify +entfernt einen (beliebigen) prozess aus der Warteliste des Objekts, +stellt die (temporär) aufgehobenen Sperren wieder her und führt +ihn dem normalen Scheduling zu. wait +und notify +sind damit für elementare Synchronisationsaufgaben geeignet, +bei denen es weniger auf die Kommunikation als auf die Steuerung der +zeitlichen Abläufe ankommt. + +

+Das folgende Beispiel demonstriert den Einsatz von wait +und notify +an einem Producer/Consumer-Beispiel. +Ein prozess arbeitet dabei als Produzent, der Fließkommazahlen +»herstellt«, und ein anderer als Konsument, der die produzierten +Daten verbraucht. Die Kommunikation zwischen beiden erfolgt über +ein gemeinsam verwendetes Vector-Objekt, +das die produzierten Elemente zwischenspeichert und als Medium für +die wait-/notify-Aufrufe +dient: + + +

+ + + + + +
+ +
+001 /* Listing2213.java */
+002 
+003 import java.util.*;
+004 
+005 class Producer2213
+006 extends Thread
+007 {
+008   private Vector v;
+009 
+010   public Producer2213(Vector v)
+011   {
+012     this.v = v;
+013   }
+014 
+015   public void run()
+016   {
+017     String s;
+018 
+019     while (true) {
+020       synchronized (v) {
+021         s = "Wert "+Math.random();
+022         v.addElement(s);
+023         System.out.println("Produzent erzeugte "+s);
+024         v.notify();
+025       }
+026       try {
+027         Thread.sleep((int)(100*Math.random()));
+028       } catch (InterruptedException e) {
+029         //nichts
+030       }
+031     }
+032   }
+033 }
+034 
+035 class Consumer2213
+036 extends Thread
+037 {
+038    private Vector v;
+039 
+040    public Consumer2213(Vector v)
+041    {
+042       this.v = v;
+043    }
+044 
+045    public void run()
+046    {
+047       while (true) {
+048          synchronized (v) {
+049             if (v.size() < 1) {
+050                try {
+051                   v.wait();
+052                } catch (InterruptedException e) {
+053                   //nichts
+054                }
+055             }
+056             System.out.print(
+057               " Konsument fand "+(String)v.elementAt(0)
+058             );
+059             v.removeElementAt(0);
+060             System.out.println(" (verbleiben: "+v.size()+")");
+061          }
+062          try {
+063             Thread.sleep((int)(100*Math.random()));
+064          } catch (InterruptedException e) {
+065             //nichts
+066          }
+067       }
+068    }
+069 }
+070 
+071 public class Listing2213
+072 {
+073    public static void main(String[] args)
+074    {
+075       Vector v = new Vector();
+076 
+077       Producer2213 p = new Producer2213(v);
+078       Consumer2213 c = new Consumer2213(v);
+079       p.start();
+080       c.start();
+081    }
+082 }
+
+
+Listing2213.java
+ +Listing 22.13: Ein Producer-/Consumer-Beispiel mit wait und notify

+ +

+Um die Arbeitsverteilung zwischen den Prozessen etwas interessanter +zu gestalten, werden beide gezwungen, nach jedem Schritt eine kleine +Pause einzulegen. Da die Wartezeit zufällig ausgewählt wird, +kann es durchaus dazu kommen, dass der Produzent eine größere +Anzahl an Elementen anhäuft, die der Konsument noch nicht abgeholt +hat. Der umgekehrte Fall ist natürlich nicht möglich, da +der Konsument warten muss, wenn keine Elemente verfügbar sind. +Eine Beispielsitzung könnte etwa so aussehen: + +

+Produzent erzeugte Wert 0.09100924684649958
+ Konsument fand Wert 0.09100924684649958 (verbleiben: 0)
+Produzent erzeugte Wert 0.5429652807455857
+ Konsument fand Wert 0.5429652807455857 (verbleiben: 0)
+Produzent erzeugte Wert 0.6548096532111007
+ Konsument fand Wert 0.6548096532111007 (verbleiben: 0)
+Produzent erzeugte Wert 0.02311095955845288
+ Konsument fand Wert 0.02311095955845288 (verbleiben: 0)
+Produzent erzeugte Wert 0.6277057416210464
+ Konsument fand Wert 0.6277057416210464 (verbleiben: 0)
+Produzent erzeugte Wert 0.6965546173953919
+Produzent erzeugte Wert 0.6990053250441516
+Produzent erzeugte Wert 0.9874467815778902
+Produzent erzeugte Wert 0.12110075531692543
+Produzent erzeugte Wert 0.5957795111549329
+ Konsument fand Wert 0.6965546173953919 (verbleiben: 4)
+Produzent erzeugte Wert 0.019655027417308846
+ Konsument fand Wert 0.6990053250441516 (verbleiben: 4)
+ Konsument fand Wert 0.9874467815778902 (verbleiben: 3)
+Produzent erzeugte Wert 0.14247583735074354
+ Konsument fand Wert 0.12110075531692543 (verbleiben: 3)
+
+ +

+ + + + + + + + + +
+ +

+Durch eine konstante Pause nach jedem produzierten Element könnte +der Produzent bewußt langsamer gemacht werden. Der schnellere +Konsument würde dann einen Großteil seiner Zeit damit verbringen, +festzustellen, dass keine Elemente verfügbar sind. Zwar würde +das Beispiel (in leicht modifizierter Form) auch ohne den Einsatz +von wait/notify +funktionieren. Durch ihre Verwendung aber ist der Konsumentenprozess +nicht gezwungen, aktiv zu warten, sondern wird vom Produzenten +benachrichtigt, wenn ein neues Element verfügbar ist. Der Rechenzeitbedarf +reduziert sich dadurch auf einen Bruchteil dessen, was andernfalls +benötigt würde.

+ + + + +
 Hinweis 
+
+ + + + +

22.4.4 PipedInputStream und PipedOutputStream

+ +

+Sollen wie im vorigen Beispiel zwei Threads so miteinander verbunden +werden, dass einer von beiden Daten erzeugt, die der andere verarbeitet, +gibt es die Möglichkeit, beide mit Hilfe einer Pipe +zu synchronisieren. Dabei werden die beiden Threads über einen +ByteStream miteinander verbunden, der von einem Thread geschrieben +und von dem anderen gelesen wird. + +

+Dieses Piping-Konzept wird in Java durch die Klassen PipedInputStream +und PipedOutputStream +realisiert. Beide Klassen werden immer paarweise und immer in getrennten +Threads verwendet. Daten, die der eine Thread in den PipedOutputStream +schreibt, kann der andere aus dem angebundenen PipedInputStream +lesen. + +

+Da die Kommunikation gepuffert erfolgt, kann der schreibende +Thread in einem gewissen Rahmen mehr Daten produzieren, als der lesende +verarbeiten kann. Ist der Puffer voll (im JDK 1.2 ist er 1024 Byte +groß), wird der schreibende Thread angehalten, bis der lesende +ausreichend Zeichen gelesen hat. Greift der lesende Thread auf eine +Pipe zu, die nicht genügend Daten enthält, muss er warten, +bis ein anderer Thread die erforderliche Anzahl an Bytes hineingeschrieben +hat. + +

+Wir können das Producer-/Consumer-Beispiel unter Verwendung von +PipedInputStream +und PipedOutputStream +vereinfachen, denn die gesamte Synchronisationsarbeit wird automatisch +beim Aufruf der read- +und write-Methoden +erledigt. + +

+Das folgende Listing zeigt zwei Threads, die über eine Pipe einzelne +Datenbytes miteinander austauschen. Die Pipe wird vom Hauptprogramm +erzeugt, indem zunächst der PipedInputStream +angelegt und an den danach erzeugten PipedOutputStream +übergeben wird. Alternativ hätte die Verbindung auch hergestellt +werden können, indem einer der beiden Streams an die connect-Methode +des anderen übergeben worden wäre. + + +

+ + + + + +
+ +
+001 /* Listing2214.java */
+002 
+003 import java.io.*;
+004 
+005 class Producer2214
+006 extends Thread
+007 {
+008   private PipedOutputStream pipe;
+009 
+010   public Producer2214(PipedOutputStream pipe)
+011   {
+012     this.pipe = pipe;
+013   }
+014 
+015   public void run()
+016   {
+017     while (true) {
+018       byte b = (byte)(Math.random() * 128);
+019       try {
+020         pipe.write(b);
+021         System.out.println("Produzent erzeugte " + b);
+022       } catch (IOException e) {
+023         System.err.println(e.toString());
+024       }
+025       try {
+026         Thread.sleep((int)(100*Math.random()));
+027       } catch (InterruptedException e) {
+028         //nichts
+029       }
+030     }
+031   }
+032 }
+033 
+034 class Consumer2214
+035 extends Thread
+036 {
+037   private PipedInputStream pipe;
+038 
+039   public Consumer2214(PipedInputStream pipe)
+040   {
+041     this.pipe = pipe;
+042   }
+043 
+044   public void run()
+045   {
+046     while (true) {
+047       try {
+048         byte b = (byte)pipe.read();
+049         System.out.println(" Konsument fand " + b);
+050       } catch (IOException e) {
+051         System.err.println(e.toString());
+052       }
+053       try {
+054         Thread.sleep((int)(100*Math.random()));
+055       } catch (InterruptedException e) {
+056         //nichts
+057       }
+058     }
+059   }
+060 }
+061 
+062 public class Listing2214
+063 {
+064   public static void main(String[] args)
+065   throws Exception
+066   {
+067     PipedInputStream inPipe = new PipedInputStream();
+068     PipedOutputStream outPipe = new PipedOutputStream(inPipe);
+069     Producer2214 p = new Producer2214(outPipe);
+070     Consumer2214 c = new Consumer2214(inPipe);
+071     p.start();
+072     c.start();
+073   }
+074 }
+
+
+Listing2214.java
+ +Listing 22.14: Das Producer-/Consumer-Beispiel mit einer Pipe

+

+ + + + + + + + + + + +
+ +

+Piping gibt es auch für zeichenbasierte Kommunikation. Dazu werden +die Klassen PipedWriter +und PipedReader +verwendet. Bis auf den Unterschied, dass Zeichen anstelle von Bytes +ausgetauscht werden, entspricht ihre Arbeitsweise genau den hier vorgestellten +Byte-Pipes.

+ + + + +
 Tipp 
+
+


+ + + +
 Titel  + Inhalt  + Suchen  + Index  + DOC  +Handbuch der Java-Programmierung, 5. Auflage, Addison +Wesley, Version 5.0.1 +
 <<  +  <   +  >   + >>  + API  +© 1998, 2007 Guido Krüger & Thomas +Stark, http://www.javabuch.de +
+ + + -- cgit v1.2.3