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