Advanced Java Services | Synchronisieren mit synchronized |
Es gibt bekanntlich Resourcen, die exklusiv vergeben werden müssen, hierzu gehören u.a. Ausgaben auf den Drucker und Schreibzugriffe auf Dateien. Damit ein Thread eine Aufgabe exklusiv abarbeiten kann stellt Java das Schlüsselwort synchronized bereit. Betritt ein Thread einen Codebereich, der mit synchronized geschützt ist, kann er diesen Bereich exklusiv abarbeiten. Kein anderer Thread kann dann diesen Bereich betreten. Dazu hat jedes Javaobjekt einen Monitor. Diese Überwachungsinstanz enthält pro Objekt genau einen Lock. Will ein Thread einen mit synchronized geschützten Bereich betreten, so muß er vom zugehörigen Objekt einen Lock anfordern. Ist dieser bereits vergeben, so muß er warten. Da es nur einen Lock gibt sperrt ein Thread, der diesen Lock besitzt zwangsläufig alle anderen mit synchronized geschützten Bereiche dieses Objekts. Es werden damit evtl. auch Bereiche gesperrt, die mit der von dem Thread gerade abgearbeiteten Code garnichts zu tun haben. Performanceverluste sind die logische Folge dieses sozusagen rabiaten Verhaltens.
Mit synchronized kann man einen bestimmten Codebereich schützen oder auch eine ganze Methode. Im ersten Fall spricht man von einem synchronized-Block.
synchronized(einObjekt) { // statements }
Dieser Bereich wird nun durch das angegebene Objekt geschützt, das heißt, daß "einObjekt" den Lock vergibt. Oft ist dieses Objekt this, da sich der Block in einer nichtstatischen Methode des Objekts "einObjekt" befindet.
public synchronized void method() { // statements }
Hier wird die gesamte Methode exklusiv vergeben. Natürlich ist jeder beliebige Returntyp möglich und auch andere Schutzzustände. Die obige Schreibweise ist lediglich eine Abkürzung für die folgende:
public void method() { synchronized(this) { // statements } }
Auch auf der Ebene static kann man mit synchronized arbeiten. Da ein synchronized Block ein Objekt benötigt nimmt man in diesem Fall das der Klasse zugeordnete class-Objekt.
synchronized(MyKlassenName.class) { // statements }
Dieser Bereich wird nun durch das angegebene class-Objekt geschützt.
public static synchronized void method() { // statements }
Wie zu erwarten ist obige Schreibweise äquivalent zu der folgenden.
public static void method() { synchronized(MyKlassenName.class) { // statements } }
Der Name der Klasse ist natürlich der in dem die Methode angesiedelt ist.
Das folgende Beispiel zeigt eine statische Methode, die sowohl einen geschützten Bereich wie auch einen ungeschützten Bereich enthält. Beide Bereiche geben 10 Zeichen auf die Konsole aus. Dazu gibt es eine Threadklasse, die mit dieser Methode arbeitet. Den Threadobjekten kann übergeben werden welche Strings sie in welchem Bereich ausgeben sollen. Damit die beiden Bereiche nicht zu schnell ablaufen werden sie durch sleep gebremst. Der erste Thread gibt im ungeschützten Bereich "1" und im geschützten Bereich "A" aus, der zweite Thread im ungeschützten Bereich "2" und im geschützten Bereich "B" aus.
Die Hauptklasse mit der statischen Methode print
public class SynchronizedDemo { public static void main(String[] args) { Thread th1 = new AusgabeThread("1","A"); Thread th2 = new AusgabeThread("2","B"); th1.start(); th2.start(); } public static void print(String s1, String s2) { for(int i = 0; i < 10; i++) { System.out.print(s1); try { Thread.sleep(200); } catch(InterruptedException ex) { } } synchronized(SynchronizedDemo.class) { for(int i = 0; i < 10; i++) { System.out.print(s2); try { Thread.sleep(200); } catch(InterruptedException ex) { } } } } }
Die Threadklasse
public class AusgabeThread extends Thread { private String s1; private String s2; public AusgabeThread(String s1, String s2) { this.s1 = s1; this.s2 = s2; } public void run() { SynchronizedDemo.print(s1,s2); } }
Die Ausgabe ist nicht vollkommen determiniert. Die A's und die B's werden in Blöcken am Ende auftreten, alles andere kann man nicht vorhersagen.Hier zwei Durchläufe des Programms.
12212121212121122112BBBBBBBBBBAAAAAAAAAA
12121212212112121212AAAAAAAAAABBBBBBBBBB
Hier erhebt sich natürlich die Frage: Was passiert mit dem Lock während der Thread schläft?. Da es die sleep()-Phasen auch im synchronisierten Bereich gibt wird der Lock nicht zurückgegeben. Sonst würde ja der zweite Thread darauf zugreifen und die Ausgaben würden sich mischen. Die folgenden Tabelle klärt das für alle threadrelevanten Methoden.
Es ist wichtig zu wissen, welche Methoden beim Aufruf einen Lock zurückgeben oder nicht.
Lockverhalten der threadrelevanten Methoden | ||
Methode | Klasse | Lockverhalten |
join() | Thread | Behält den Lock |
sleep() | Thread | Behält den Lock |
yield() | Thread | Behält den Lock |
notify() | Object | Behält den Lock |
wait() | Object | Gibt den Lock zurück |
Merkregel: Nur wait() gibt den Lock zurück.
Was passiert, wenn ein Thread im Besitz eines Locks ist und einen weiteren Bereich betreten will, der mit demselben Lock geschützt ist. Er wird einen Lock anfordern, aber er kann ja keinen mehr bekommen. Kann sich der Monitor merken, an wen er den Lock gegeben hat. Kann er sagen, du hast ja den Lock schon, verwende ihn. Wir betrachten die folgende Situation.
public class SynchronizeIsReentrant { public synchronized first() { //do something second(); //do something second(); //do something } public synchronized second() { //do something } }
Beim Eintritt in die Methode second() fordert der ausführende Thread erneut einen Lock vom Monitor. Nun gibt es zwei Möglichkeiten wie der Monitor reagieren kann. Dazu stellen wir uns den Monitor mal als sprechendes Wesen vor.
Erste Möglichkeit:
Monitor sagt: Ich habe keinen Lock mehr (basta).
Zweite Möglichkeit:
Monitor sagt: Du hast doch schon den Lock, er gilt auch für die neue Situation.
In der ersten Situation spricht man von einem nichtreentranten Lock, die zweite Situation nennt man wie zu erwarten reentrant. Ein reentranter Lock ist so etwas wie ein Generalschlüssel, er sperrt überall. Wäre synchronized nicht reentrant implementiert, so käme es hier zu einem Deadlock da ja der Monitor nur einen Lock hat.
Erhält ein Thread T1 etwa Zugriff auf eine synchronized Methode m1, erhält er den Lock
und kann so die Methode exklusiv abarbeiten. Weil es aber nur einen einzigen Lock
pro Objekt gibt verhindert er damit aber zwangsläufig den den Zugriff auf alle anderen
synchronized Methoden. Kein Thread hat deswegen Zugriff auf irgendeine andere synchronized Methode
solange T1 mit m1 beschäftigt ist.
Das war einer der Gründe für die Neuentwicklung der Klasse Vector, in der zahlreiche
Methoden synchronized sind und die daher alles andere als performant ist. In der Klasse
ArrayList verzichtete man daher auf "synchronized". Dafür gibt es in der Utilityklasse
Collections eine statische Methode um eine List
nachträglich zu synchronisieren:
List list = Collections.synchronizedList(new ArrayList());
Eine Klasse MethodMagazine hat drei identisch aufgebaute synchronized Methoden. Diese melden sich mit einem Zeitstempel, wenn sie aufgerufen werden. Jede Methode dauert 5 Sekunden, was mit einem sleep() Aufruf erreicht wird, denn sleep() gibt den Lock nicht zurück. Drei Threads erhalten ein einziges Objekt dieser Klasse. Jeder Thread ruft genau eine der drei Methoden, aber eben verschiedene. Die Startaufrufe kommen direkt hintereinander. Egal welcher Thread starten kann blockiert er damit 5 Sekunden den Zugriff auf die beiden anderen Methoden. Da jede Methode mit einer Ausgabe des Zeitstempels beginnt sieht man genau, wie die restlichen Methoden blockiert werden.
Die Klasse MethodMagazine
class MethodMagazine { public synchronized void syncMethod_1() { System.out.println("syncMethod_1() " + new Timestamp(System.currentTimeMillis()) ); try { TimeUnit.SECONDS.sleep(5);} catch(InterruptedException ex) { ex.printStackTrace(); } } public synchronized void syncMethod_2() { System.out.println("syncMethod_2() " + new Timestamp(System.currentTimeMillis())); try { TimeUnit.SECONDS.sleep(5);} catch(InterruptedException ex) { ex.printStackTrace(); } } public synchronized void syncMethod_3() { System.out.println("syncMethod_3() " + new Timestamp(System.currentTimeMillis())); try { TimeUnit.SECONDS.sleep(5);} catch(InterruptedException ex) { ex.printStackTrace(); } } }
Die drei Threads
class Task1 implements Runnable { private MethodMagazine mm = null; public Task1(MethodMagazine mm) { this.mm = mm; } public void run() { mm.syncMethod_1(); } } class Task2 implements Runnable { private MethodMagazine mm = null; public Task2(MethodMagazine mm) { this.mm = mm; } public void run() { mm.syncMethod_2(); } } class Task3 implements Runnable { private MethodMagazine mm = null; public Task3(MethodMagazine mm) { this.mm = mm; } public void run() { mm.syncMethod_3(); } }
Die main-Klasse
public class SynchronizedDemo { public static void main(String[] args) { System.out.println("main"); MethodMagazine mm = new MethodMagazine(); Thread th1 = new Thread(new Task1(mm)); Thread th2 = new Thread(new Task2(mm)); Thread th3 = new Thread(new Task3(mm)); th1.start(); th2.start(); th3.start(); } System.out.println("end main"); } // end main } // end class
Einige Abläufe
main end main syncMethod_1() 2020-03-11 10:06:11.055 syncMethod_3() 2020-03-11 10:06:16.064 syncMethod_2() 2020-03-11 10:06:21.065 main end main syncMethod_1() 2020-03-11 10:07:04.475 syncMethod_3() 2020-03-11 10:07:09.484 syncMethod_2() 2020-03-11 10:07:14.485 main end main syncMethod_1() 2020-03-11 10:07:33.726 syncMethod_2() 2020-03-11 10:07:38.737 syncMethod_3() 2020-03-11 10:07:43.737
Wir arbeiten nicht mehr mit synchronized Methoden, sondern mit synchronized Blöcken in den Methoden. Dadurch können wir verschiedene Objekt zum locken verwenden. Die Objekt werden über den Konstruktor der Taskklassen injiziert. Im folgenden Beispiel erhalten zwei Tskklassen daselbe Objekt, der dritte Task jedoch ein anderes.
Die neue Klasse MethodMagazine
class MethodMagazine { public void syncMethod_1(Object ob) { synchronized(ob) { System.out.println("syncMethod_1() begin " + new Timestamp(System.currentTimeMillis()) ); try { TimeUnit.SECONDS.sleep(5);} catch(InterruptedException ex) { ex.printStackTrace(); } System.out.println("syncMethod_1() end " + new Timestamp(System.currentTimeMillis()) ); } } public void syncMethod_2(Object ob) { synchronized(ob) { System.out.println("syncMethod_2() begin " + new Timestamp(System.currentTimeMillis())); try { TimeUnit.SECONDS.sleep(5);} catch(InterruptedException ex) { ex.printStackTrace(); } System.out.println("syncMethod_2() end " + new Timestamp(System.currentTimeMillis())); } } public void syncMethod_3(Object ob) { synchronized(ob) { System.out.println("syncMethod_3() begin " + new Timestamp(System.currentTimeMillis())); try { TimeUnit.SECONDS.sleep(5);} catch(InterruptedException ex) { ex.printStackTrace(); } System.out.println("syncMethod_3() end " + new Timestamp(System.currentTimeMillis())); } } }
Die drei Threads die nun ein Objekt über den Konstruktor erhalten
class Task1 implements Runnable { private MethodMagazine mm = null; private Object monitor; public Task1(MethodMagazine mm, Object monitor) { this.mm = mm; this.monitor = monitor; } public void run() { mm.syncMethod_1(monitor); } } class Task2 implements Runnable { private MethodMagazine mm = null; private Object monitor; public Task2(MethodMagazine mm, Object monitor) { this.mm = mm; this.monitor = monitor; } public void run() { mm.syncMethod_2(monitor); } } class Task3 implements Runnable { private MethodMagazine mm = null; private Object monitor; public Task3(MethodMagazine mm, Object monitor) { this.mm = mm; this.monitor = monitor; } public void run() { mm.syncMethod_3(monitor); } }
Die main-Klasse
/** * Ein Lock sperrt alles ! * aber hier: verschiedene Objekte für verschiedene Methoden */ public class SynchronizedDemo { public static void main(String[] args) { System.out.println("main"); MethodMagazine mm = new MethodMagazine(); Object monitor1 = new Object(); Object monitor2 = new Object(); Object monitor3 = monitor1; Thread th1 = new Thread(new Task1(mm, monitor1)); Thread th2 = new Thread(new Task2(mm, monitor2)); Thread th3 = new Thread(new Task3(mm, monitor3)); th1.start(); th2.start(); th3.start(); System.out.println("end main"); } // end main } // end class
Einige Abläufe
main end main syncMethod_1() begin 2020-03-11 12:19:41.616 syncMethod_2() begin 2020-03-11 12:19:41.616 syncMethod_1() end 2020-03-11 12:19:46.625 syncMethod_2() end 2020-03-11 12:19:46.625 syncMethod_3() begin 2020-03-11 12:19:46.625 syncMethod_3() end 2020-03-11 12:19:51.626 main end main syncMethod_2() begin 2020-03-11 12:20:30.581 syncMethod_1() begin 2020-03-11 12:20:30.581 syncMethod_1() end 2020-03-11 12:20:35.591 syncMethod_2() end 2020-03-11 12:20:35.591 syncMethod_3() begin 2020-03-11 12:20:35.591 syncMethod_3() end 2020-03-11 12:20:40.592 main end main syncMethod_1() begin 2020-03-11 12:21:18.396 syncMethod_2() begin 2020-03-11 12:21:18.396 syncMethod_2() end 2020-03-11 12:21:23.405 syncMethod_1() end 2020-03-11 12:21:23.405 syncMethod_3() begin 2020-03-11 12:21:23.405 syncMethod_3() end 2020-03-11 12:21:28.406 main end main syncMethod_1() begin 2020-03-11 12:22:01.768 syncMethod_2() begin 2020-03-11 12:22:01.768 syncMethod_1() end 2020-03-11 12:22:06.777 syncMethod_3() begin 2020-03-11 12:22:06.777 syncMethod_2() end 2020-03-11 12:22:06.778 syncMethod_3() end 2020-03-11 12:22:11.778
Dieses Konzept wurde in Java 5 ausgebaut, indem man statt der Klasse Object eine eigene Klasse Lock einführte, die über zusätzliche Möglichkeiten verfügt einen Lock zu konfigurieren.