Monitor Synchronisation

Sinn und Zweck der Synchronisation

Wenn mehrere Prozesse beispielsweise eine Variable lesen und schreiben, können durch unterschiedliches Timing falsche Zustände entstehen. Am Einfachsten kann man es sich mit einem Bankkonto veranschaulichen. Ein Thread erhöht jeweils den Kontostand um 10 und ein anderer Thread reduziert den Kontostand um 10. Damit man den Kontostand um 10 verändern kann, muss man den Kontostand natürlich zuerst wissen. 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 man kein String Objekt nehmen, 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.