Monitor Synchronisation #
Wenn mehrere Prozesse eine Variable lesen und schreiben, können durch unterschiedliches Timing falsche Zustände entstehen. Am Einfachsten kann es mit einem Bankkonto veranschaulicht werden. Ein Thread erhöht jeweils den Kontostand um 10 und ein anderer Thread reduziert den Kontostand um 10. Damit der Kontostand um 10 verändert werden kann, muss der Kontostand natürlich zuerst gelesen werden. Es könnte also zum Beispiel folgendes passieren:
- Thread 1 liest Kontostand (0)
- Thread 2 liest Kontostand (0)
- Thread 2 erhöht Kontostand (10)
- Thread 1 reduziert Kontostand (-10)
Der obige Ablauf ist möglich, weil das Betriebssystem entscheidet, wann und wie lange ein Thread laufen darf. Wenn nur eine fehlerhaften Reihenfolge möglich ist, kann es zu Fehlern kommen, aber leider nicht bei jeder Programmdurchführung. Das macht das Testen und das Debuggen schwer.
Einführung in die Monitor Synchronisation #
Das keyword synchronized
führt zu einer Synchronisation.
public synchronized void deposit(int amount) {
this.balance += amount; //lese balance und erhöhe um amount
}
Durch dieses Keyword kann nur ein Thread jeweils in der Methode sein. Das Lesen und Schreiben der Variable ist dadurch eine Einheit also atomar.
Auch ist folgendes möglich:
public synchronized void withdraw(int amount) {
this.balance -= amount;
}
Die zwei Methoden synchronisieren jeweils mit dem gleichen “Lock”. In diesem Fall ist es “this”.
Auch ist folgendes möglich:
Object lock = new Object();
public void test() {
synchronized(lock) {
//atomarer Code
}
}
Es ist wichtig, dass auf das gleiche Lock synchronisiert wird. Ausserdem sollte kein String Objekt verwendet werden, da durch das String-pooling mehrere immutable Objekte effektiv das gleiche Objekt sein können. Gleiches gilt auch für primitiven Datentypen also zum Beispiel das Integer-Objekt.
Ein weiteres Beispiel:
class BankAccount {
private int balance = 0;
public synchronized void withdraw(int amount) throws InterruptedException {
while (amount > balance) {
wait();
}
balance -= amount;
}
public synchronized void deposit(int amount) {
balance += amount;
notifyAll();
}
}
In diesem Beispiel sind noch ein paar Fallstricke versteckt. Die While-Schleife über amount > balance bezweckt, dass wenn der Thread aufgeweckt wird, nochmals die Warte-Bedinung geprüft wird.
Bei der wait()
Methode gibt der Thread temporär sein Lock wieder frei, sodass andere Threads in den synchronized Block gehen können und so die wait-Bedingung erfüllen. Das notifyAll()
ist das Gegenstück zu wait()
und weckt alle Threads auf. Es gäbe noch das notify()
. Diese Funktion kann aber nicht für das obige Beispiel benutzt werden. notify()
weckt nur ein Thread und dieser könnte zum Beispiel gerade in der Schleife der withdraw-Funktion sein. Dadurch käme es zu einem Deadlock.