Threads - Was passiert ?

Hallo,

Ich schreibe morgen eine Klausur und bin mir relativ sicher das eine der folgenden Aufgaben aus den Übungen drankommt. Ich habe mir auch ein paar Gedanken gemacht und würde diese gerne mit euch diskutieren, damit ich für morgen sicher sein kann die richtigen Lösungen parat zu haben.

Zunächst mal die erste

private String nachricht;
public synchronized void senden(String s) {
nachricht = s;
notifyAll(); }
public synchronized String empfangen() { wait();
return nachricht; }
}```

Die Frage ist 
a) Was passiert hier ?
b) Was passiert wenn notifyAll() durch notify() ersetzt wird.

Meine Gedanken dazu sind:
a) Die Methoden sind synchronized, also ist damit sichergestellt, dass nur ein Thread die Methode ausführt und die anderen warten. Wenn jetzt eine Nachricht empfangen wurde, gibt der Thread den Monitor ab via wait() und ein anderer Thread kann einen synchronized Block betreten. Wird nun eine Nachricht gesendet werden alle wartenden Threads via notifyAll() davon unterrichtet und sobald der Monitor wieder frei ist kann der am längsten wartenden Thread wieder einen synchronized Abschnitt "betreten".

b) Das Programm würde nicht terminieren, weil ein Thread immer am warten wäre und niemals wieder aus dem wartenden Zustand rauskommt. 

Aufgabe 2)

Erstellen Sie eine einfache Java-Bean mit einem Zähler.
Methoden der Bean: ```public void increase()
public int getCounter()```
- Erzeugen Sie zwei Threads, die gleichzeitig mehrfach die Zähl-Bean
parallel aufrufen. (Als Java Application)
- Welche "Zählergebnisse" erhalten Sie?

Antwort:
Ich hätte gesagt ich bekomme eine Reihe die ganz normal hochzählt, da wir die Methoden synchronized machen und die Variable count als globale Variable definieren. Dadurch das sie global ist liegt sie ja auf dem Heap und kann von allen Threads angesprochen werden. Das synchronized in den Methoden verhindert aber das eine Inkosistenz entsteht aufgrund von mehreren Threads die gleichzeitig zugreifen auf die Variable. Deklarieren wir unsere Variable also als int counter = 0;

dann wäre das Ergebnis 1,2,3,4,5,6,....


Wäre schon wenn sich jemand melden würde, ob ich etwas falsch gemacht habe, grundlegend falsch verstanden habe oder wie es mit meinen Antworten generell aussieht.

Viele Grüße

Meine Antwort ist nicht vollständig und es kann auch sein, dass ich einige Aspekte übersehen habe - Threading auf der Ebene ist nicht ganz trivial.

„Der am längsten wartende“ ist ein Trugschluss, es ist immer ein zufälliger Thread. Das Threading ist nicht fair, deshalb kann man einige High-Level-Klassen in einen fairen Modus „umschalten“.
Grob scheinst du mir das Konzept mit wait und notify verstanden zu haben. Deine Antworten sind aber meiner Meinung nach trotzdem nicht ganz richtig.

Zu a): sobald die senden-Methode aufgerufen wird, returnen alle empfangen-Methoden direkt nacheinander, weil alle waits gleichzeitig aufgehoben werden. Direkt nacheinander, weil sie wegen des synchronized nicht gleichzeitig ausgeführt werden. Sie liefern alle den gleichen String zurück.
Zu b): das Programm würde trotzdem terminieren, weil es nur eine Stelle gibt, die auf ein notify wartet. Der Unterschied ist, dass jede versandte Nachricht von nur genau einem Empfänger zurückgegeben wird, weil alle anderen weiterschlafen.

Aufgabe 2 ist für mich schlecht gestellt, allerdings fehlt mir der Kontext des Unterrichtes / Vorlesung.
Wenn man folgenden Counter implementiert, ist alles wie erwartet und so ähnlich wie du es beschrieben hast:

    private volatile int counter = 0;
    
    public void increase() {
        counter++;
    }
    
    public int getCounter() {
        return counter;
    }
}```

Wahrscheinlich soll man das volatile aber weglassen. Mit volatile hätte man eine monoton steigende Folge (alles unter der Annahme, dass ein Thread die ganze Zeit lang increase aufruft und der andere die ganze Zeit getCounter), ungefähr so:
0, 0, 1, 3, 4, 5, 5, 6, 8, 10, ....
Mit synchronized wäre das Verhalten genauso, allerdings mit schlechterer Nebenläufigkeit, weil sich die Threads ständig blockieren. Es würde also theoretisch etwas länger dauern, bis bspw. Integer.MAX_VALUE erreicht wird.
Ohne Synchronisationsmaßnahmen bekäme der getCounter-Thread nur eine konstante Zahlenfolge zurück, weil ihn das Update der Membervariable nicht erreicht und immer mit einer veralteten Kopie gearbeitet wird, die zum Zeitpunkt des Threadstarts angelegt wurde.


Aber wie gesagt: alles ohne Gewähr!

Super vielen Dank für deine Mühe ! Das hilft mir definitiv weiter :slight_smile:

… wenn es denn richtig ist!

Ich habe mal kurze Tests implementiert, um meine Theorie von gestern zu prüfen. Das passte eigentlich auch, allerdings konnte ich das von mir beschriebene Fehlverhalten in Aufgabe 2 (der Thread, der getCounter aufruft, bekommt immer den selben Wert) nicht provozieren.
Hat jemand eine Idee woran das liegt? (Meine Vermutung ist, dass es ganz einfach an der Implementierung der JVM liegt (HotSpot, 1.8.0u45), es wird schließlich nur nicht garantiert, dass Updates einer Variable sichtbar werden, wenn keine Synchronisation verwendet wird.)

Anbei meine (zugegebenermaßen ziemlich schlechten) Tests:

Aufgabe 1
[spoiler]```package concurrency;

public class Nachrichtenverteiler {
private int nachricht;

public synchronized void senden(int i) {
    nachricht = i;
    notifyAll();
}

public synchronized int empfangen() {
    try {
        wait();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return nachricht;
}

}``````package concurrency;

import java.util.concurrent.CountDownLatch;

public class NachrichtenverteilerTest {
public static void main(String[] args) {
Nachrichtenverteiler verteiler = new Nachrichtenverteiler();
CountDownLatch startLatch = new CountDownLatch(1);

    new Sender(verteiler).start();
    for (int i = 0; i < 5; i++) {
        new Thread(new Receiver(Integer.toString(i), verteiler, startLatch)).start();
    }
    startLatch.countDown();
}

private static class Sender extends Thread {
    private final Nachrichtenverteiler verteiler;
    private int counter = 0;

    public Sender(Nachrichtenverteiler verteiler) {
        this.verteiler = verteiler;
        setDaemon(true);
    }

    @Override
    public void run() {
        while (true) {
            verteiler.senden(counter++);
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

private static class Receiver implements Runnable {
    private final Nachrichtenverteiler verteiler;
    private final CountDownLatch startLatch;
    private final String name;

    public Receiver(String name, Nachrichtenverteiler verteiler, CountDownLatch startLatch) {
        this.name = name;
        this.verteiler = verteiler;
        this.startLatch = startLatch;
    }

    @Override
    public void run() {
        try {
            startLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        for (int i = 0; i < 3; i++) {
            System.out.println(name + ": " + verteiler.empfangen());
        }
    }
}

}```[/spoiler]

Aufgabe 2
[spoiler]```package concurrency;

public class ConcurrentCounter {
private int counter = 0;

public void increase() {
    counter++;
}

public int getCounter() {
    return counter;
}

}``````package concurrency;

import java.util.concurrent.CountDownLatch;

public class ConcurrentCounterTest {
public static void main(String[] args) {
CountDownLatch startLatch = new CountDownLatch(1);
ConcurrentCounter counter = new ConcurrentCounter();
new Reader(counter, startLatch).start();
new Thread(new Increaser(counter, startLatch)).start();
startLatch.countDown();
}

private static class Increaser implements Runnable {
    private final ConcurrentCounter counter;
    private final CountDownLatch startLatch;

    public Increaser(ConcurrentCounter counter, CountDownLatch startLatch) {
        this.counter = counter;
        this.startLatch = startLatch;
    }

    @Override
    public void run() {
        try {
            startLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        for (int i = 0; i < 50; i++) {
            counter.increase();
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

private static class Reader extends Thread {
    private final ConcurrentCounter counter;
    private final CountDownLatch startLatch;

    public Reader(ConcurrentCounter counter, CountDownLatch startLatch) {
        this.counter = counter;
        this.startLatch = startLatch;
        setDaemon(true);
    }

    @Override
    public void run() {
        try {
            startLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        while (true) {
            System.out.println(counter.getCounter());
        }
    }
}

}```[/spoiler]

*** Edit ***

In der selben JVM habe ich auch mal beide Methoden synchronized vs. volatile vs. unsynchronisiert getestet. Wie genau ist erstmal nicht so wichtig (habe kein println und keine sleep im Testcode), aber die Laufzeitunterschiede sind vielleicht interessant:

synchronized: 359.380 ms
volatile:      16.136 ms
ohne:             227 ms

Dabei ist natürlich zu beachten, dass alle drei Varianten unterschiedlich threadsicher sind. Bei der volatile-Variante könnte es mMn noch lost updates geben, wenn increment parallel aufgerufen wird und das Timing ungünstig ist (der +±Operator ist nicht atomar!). Die vollständig synchronisierte Variante ist vollständig threadsafe, die unsynchronisierte gar nicht.

Meine Frage bzgl. des beobachteten Verhaltens der unsynchronisierten Variante steht übrigens noch :wink: