Musterlösungen

Laboraufgabe “Anwendung von Memory Barrieren”

Möglichkeit 1: Zugriff auf volatile-Variablen (in MemoryBarrierTest1)

Die boolean-Variable, über deren Setzen das Abbrechen gesteuert wird, wird hier als volatile gekennzeichnet:

public volatile boolean stopped = false;

statt …

public boolean stopped = false;

Jeder Zugriff auf diese Variable wirkt deshalb wie eine Memory Barrier. Lesender Zugriff auf die Variable ist mit einem “pull” aus dem Heap verbunden, ändernder Zugriff (Zuweisungen) mit einem “push” in die lokale Kopie.

Möglichkeit 2: Lock-Objekte bzw. synchronized (in MemoryBarrierTest2)

Direkt vor dem lesenden Zugriff auf die Variable stopped in der while-Schleife der run-Methode wird ein (leerer) synchronized Block eingefügt. Darin passiert zwar nichts, aber das synchronized wirkt als Memory Barrier.

Alternativ könnte man auch Getter- und Setter-Methoden für den Zugriff auf stopped verwenden und diese Methoden synchronized markieren.

public void run() {
    while (!this.stopped) {
        // jedes synchronized wirkt als Memory-Barrier
        // work
        synchronized (this) {
            //
        }
    }

statt …

public void run() {
    while (!this.stopped) {
        // work
    }

Möglichkeit 3: Ende eines Threads (in MemoryBarrierTest5)

Die folgende Lösung ist nicht naheliegend und man würde normalerweise auch nicht so vorgehen, nur um die Variable stopped zwischen zwei Threads zu synchronisieren. Hier wird ausgenutzt, dass das Ende eines Threads auch eine Memory Barrier darstellt. Damit ein Thread endet, wird hier ein kurzlebiger Thread zu Beginn der run()-Methode gestartet. Dazu wird als erstes die Methode startStopper() aufgerufen.

@Override
public void run() {
    startStopper();

Die Methode startStopper(), erzeugt einen neuen Thread, speichert die Referenz darauf in der Instanzvariable stopper

private Thread stopper;

… und startet ihn. Der Thread, der über stopper erreichbar ist, sollte nach ca. 1 Sek. enden:

public void startStopper() {
    this.stopper = new Thread(() -> {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        this.stopped = true;
        System.out.println("Stopper thread set stopped on MemoryBarrierTest-Thread.");
    });
    this.stopper.start();
}

In der run()-Methode wird nun mit join auf das Ende des stopper-Threads gewartet. Der Thread, der wartet, soll aber nicht blockieren. Deshalb wird join mit dem Parameter 1 aufgerufen. Das bedeutet, dass maximal 1 ms auf das Ende des stopper-Threads gewartet wird. Sollte das Ende bis dahin noch nicht eingetreten sein, wird endet der Aufruf. Zwar kann es während des Wartens (für 1 ms) einen Interrupt geben, der behandelt werden muss, aber der wichtige Nebeneffekt ist, dass es einen “pull” vom Heap in den lokalen Cache gibt. Das aktualisiert den Wert von stopped, der dann wie gehabt in der Bedingung der while-Schleife geprüft werden kann.

startStopper();
while (!this.stopped) {
    // work
    if (this.stopper != null) {
        try {
            // Synchronisierung von Stopper-Thread Variablenänderungen
            // durch Warten auf das Ende von Stopper Thread
            this.stopper.join(1);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

Möglichkeit 4: Unterbrechung (in MemoryBarrierTest6)

Ebenso “unnatürlich” wie bei Möglichkeit 3, wird hier eine Zustandsänderung eines Threads provoziert. Dazu wird der Thread kurz schlafen gelegt. In der while-Schleife wird dazu in jedem Durchlauf die statische Methode sleep von Thread aufgerufen.

while (!this.stopped) {
    // work
    // wg. Zustandswechsel
    try {
        Thread.sleep(1);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
}

sleep(1) bedeutet, dass der Thread 1 ms warten soll. Er wacht kurze Zeit später wieder auf. Dabei wird ein “pull” vom globalen Heap in die lokal gecachte Version gemacht.

Bei der Prüfung von stopped im nächsten Schleifendurchlauf liegt also der synchronisierte Wert der Instanzvariablen vor.

Laboraufgabe “threadlokaler Speicher”

(fehlerhafter) Lösungsversuch (in MyThreadLocalRandom0)

Diese Lösung ist fehlerhaft. Es gibt in diesem Lösungsversuch genau eine Instanz von Random, die von allen Threads geteilt benutzt wird.

public class MyThreadLocalRandom0 implements Runnable {
    public static long now = System.currentTimeMillis();
    public Random rand = new Random(now);

    @Override
    public void run() {
        var strBuf = new StringBuffer();
        strBuf.append(Thread.currentThread().getName() + ": ");
        for (var j = 0; j < 20; j++) {
            strBuf.append(String.format("%02d ", this.rand.nextInt(100)));
        }
        System.out.println(strBuf);
    }
}

Deshalb wird eine Sequenz von 200 unabhängigen Zufallszahlen gezogen. Das Ergebnis sieht beispielsweise so aus:

Runner-00: 37 11 28 81 45 32 91 29 80 60 65 82 74 05 13 06 14 81 85 98
Runner-01: 92 57 61 21 37 99 25 88 30 61 30 70 47 93 17 91 43 39 02 27
Runner-02: 18 06 70 18 76 97 39 07 00 22 21 39 07 61 06 01 97 72 06 76
Runner-04: 87 87 87 07 82 33 55 81 54 78 06 52 63 82 56 35 23 22 70 27
Runner-03: 39 28 37 52 26 86 30 29 96 25 37 74 60 70 53 54 13 61 31 75
Runner-05: 72 57 74 42 77 72 98 73 66 81 81 99 20 04 80 22 77 26 85 66
Runner-06: 52 16 72 36 81 52 18 83 76 07 76 53 57 83 93 14 44 23 23 51
Runner-07: 38 56 87 14 69 75 08 47 22 92 92 01 72 75 67 93 87 99 03 34
Runner-08: 92 56 95 61 18 79 42 78 75 76 99 41 24 26 53 28 96 83 15 23
Runner-09: 56 19 41 03 00 28 98 79 37 35 90 19 75 73 94 64 14 43 06 98

Möglichkeit 1 (in MyThreadLocalRandom1)

Ein Random-Objekt wird beim Instanziieren von runner im threadlokalen Speicher vorbereitet. Sobald später aus mehreren Threads darauf zugegriffen wird, wird jeweils threadlokal eine neue Instanz von Random erzeugt, die alle denselben Seed verwenden, deshalb wird in den 10 Threads jeweils dieselbe Sequenz von 20 Zufallszahlen gezogen.

public class MyThreadLocalRandom1 implements Runnable {
    public static long now = System.currentTimeMillis();
    public ThreadLocal<Random> rand = new ThreadLocal<>() {
        @Override
        protected Random initialValue() {
            return new Random(now);
        }
    };

    @Override
    public void run() {
        var strBuf = new StringBuffer();
        strBuf.append(Thread.currentThread().getName() + ": ");
        for (var j = 0; j < 20; j++) {
            strBuf.append(String.format("%02d ", this.rand.get().nextInt(100)));
        }
        System.out.println(strBuf);
    }
}

Das Ergebnis sieht beispielsweise so aus:

Runner-03: 75 41 30 62 67 19 38 93 65 58 84 29 99 19 19 13 45 11 47 23
Runner-02: 75 41 30 62 67 19 38 93 65 58 84 29 99 19 19 13 45 11 47 23
Runner-00: 75 41 30 62 67 19 38 93 65 58 84 29 99 19 19 13 45 11 47 23
Runner-01: 75 41 30 62 67 19 38 93 65 58 84 29 99 19 19 13 45 11 47 23
Runner-04: 75 41 30 62 67 19 38 93 65 58 84 29 99 19 19 13 45 11 47 23
Runner-06: 75 41 30 62 67 19 38 93 65 58 84 29 99 19 19 13 45 11 47 23
Runner-05: 75 41 30 62 67 19 38 93 65 58 84 29 99 19 19 13 45 11 47 23
Runner-08: 75 41 30 62 67 19 38 93 65 58 84 29 99 19 19 13 45 11 47 23
Runner-07: 75 41 30 62 67 19 38 93 65 58 84 29 99 19 19 13 45 11 47 23
Runner-09: 75 41 30 62 67 19 38 93 65 58 84 29 99 19 19 13 45 11 47 23

Möglichkeit 2 (in MyThreadLocalRandom2)

Diese Lösung ist ebenso korrekt. Ein Random-Objekt wird beim Laden der Klasse im threadlokalen Speicher vorbereitet. Im Gegensatz zur vorigen Lösung wird die Referenz auf dieses Objekt einem Klassenattribut gespeichert. Sobald später aus mehreren Threads darauf zugegriffen wird, wird jeweils threadlokal eine neue Instanz von Random erzeugt, die alle denselben Seed verwenden, deshalb wird in den 10 Threads jeweils dieselbe Sequenz von 20 Zufallszahlen gezogen. Besser verallgemeinerbar, falls es mehrere Instanzen des Runnable geben sollte.

public class MyThreadLocalRandom2 implements Runnable {
    public static long now = System.currentTimeMillis();
    public static ThreadLocal<Random> rand = new ThreadLocal<>() {
        @Override
        protected Random initialValue() {
            return new Random(now);
        }
    };

    @Override
    public void run() {
        var strBuf = new StringBuffer();
        strBuf.append(Thread.currentThread().getName() + ": ");
        for (var j = 0; j < 20; j++) {
            strBuf.append(String.format("%02d ", rand.get().nextInt(100)));
        }
        System.out.println(strBuf);
    }
}

Das Ergebnis sieht beispielsweise so aus:

Runner-00: 94 51 28 82 07 72 35 05 38 51 78 09 76 08 76 19 41 65 07 21
Runner-03: 94 51 28 82 07 72 35 05 38 51 78 09 76 08 76 19 41 65 07 21
Runner-02: 94 51 28 82 07 72 35 05 38 51 78 09 76 08 76 19 41 65 07 21
Runner-01: 94 51 28 82 07 72 35 05 38 51 78 09 76 08 76 19 41 65 07 21
Runner-06: 94 51 28 82 07 72 35 05 38 51 78 09 76 08 76 19 41 65 07 21
Runner-05: 94 51 28 82 07 72 35 05 38 51 78 09 76 08 76 19 41 65 07 21
Runner-04: 94 51 28 82 07 72 35 05 38 51 78 09 76 08 76 19 41 65 07 21
Runner-08: 94 51 28 82 07 72 35 05 38 51 78 09 76 08 76 19 41 65 07 21
Runner-07: 94 51 28 82 07 72 35 05 38 51 78 09 76 08 76 19 41 65 07 21
Runner-09: 94 51 28 82 07 72 35 05 38 51 78 09 76 08 76 19 41 65 07 21

Möglichkeit 3 (in MyThreadLocalRandom3)

Ein threadlokaler Zufallsgenerator wird beim Laden der Klasse vorbereitet. Sobald später aus mehreren Threads darauf zugegriffen wird, wird jeweils threadlokal – und damit auf unterschiedliche Instanzen – des nicht threadsicheren Zufallszahlengenrators zugegriffen. Deshalb wird in den 10 Threads jeweils eine andere Sequenz von 20 Zufallszahlen gezogen. Allerdings sollte auf ThreadLocalRandom.current() nicht static, also beim Laden zugegriffen werden, sondern “Instanz-weise” (s. nächste Lösung).

public class MyThreadLocalRandom3 implements Runnable {

    public static Random rand = ThreadLocalRandom.current();

    @Override
    public void run() {
        var strBuf = new StringBuffer();
        strBuf.append(Thread.currentThread().getName() + ": ");
        for (var j = 0; j < 20; j++) {
            synchronized (MyThreadLocalRandom3.class) {
                strBuf.append(String.format("%02d ", rand.nextInt(100)));
            }
        }
        System.out.println(strBuf);
    }
}

Das Ergebnis sieht beispielsweise so aus:

Runner-07: 09 52 98 39 50 78 85 40 34 32 30 19 04 58 40 39 27 26 33 47
Runner-01: 78 76 69 39 18 38 44 50 09 04 62 76 85 75 85 49 96 93 42 94
Runner-09: 19 45 06 78 55 62 09 33 19 22 25 72 74 70 02 83 03 22 46 33
Runner-08: 83 15 83 30 40 38 22 47 85 86 33 99 90 30 78 77 00 35 41 08
Runner-05: 21 09 25 71 49 02 27 15 91 52 37 88 62 42 01 76 26 89 91 91
Runner-06: 90 27 89 89 21 77 68 41 43 46 91 46 23 38 78 38 03 40 64 28
Runner-02: 65 86 96 25 87 53 97 21 92 98 52 96 21 47 45 33 90 13 91 96
Runner-04: 93 22 24 44 87 79 19 42 38 00 23 09 99 27 06 73 99 98 57 01
Runner-03: 77 47 22 46 30 19 92 70 38 26 69 13 62 18 42 64 94 24 60 39
Runner-00: 57 69 76 25 29 86 52 08 75 83 67 25 72 04 59 96 36 04 30 65

Wenn Sie das Programm dieser Musterlösung mehrfach auf Ihrem Rechner ausführen, werden Sie dieselbe Abfolge von Zufallszahlen bekommen. Der Grund ist, dass der Zufallszahlengenerator, der von current() geliefert wird, immer mit demselben Seed initialisiert wird.

Ein anderer Seed kann nicht verwendet werden (z.B. die aktuelle Uhrzeit wie bei den anderen Lösungen), denn setSeed wirft immer eine UnsupportedOperationException.

Möglichkeit 4 (in MyThreadLocalRandom4)

Hier wird in jeder Instanz auf ThreadLocalRandom.current() zugegriffen.

public class MyThreadLocalRandom4 implements Runnable {

    public Random rand = ThreadLocalRandom.current();

    @Override
    public void run() {
        var strBuf = new StringBuffer();
        strBuf.append(Thread.currentThread().getName() + ": ");
        for (var j = 0; j < 20; j++) {
            synchronized (MyThreadLocalRandom4.class) {
                strBuf.append(String.format("%02d ", this.rand.nextInt(100)));
            }
        }
        System.out.println(strBuf);
    }
}

Das Ergebnis sieht beispielsweise so aus:

Runner-07: 09 52 98 39 50 78 85 40 34 32 30 19 04 58 40 39 27 26 33 47
Runner-01: 78 76 69 39 18 38 44 50 09 04 62 76 85 75 85 49 96 93 42 94
Runner-09: 19 45 06 78 55 62 09 33 19 22 25 72 74 70 02 83 03 22 46 33
Runner-08: 83 15 83 30 40 38 22 47 85 86 33 99 90 30 78 77 00 35 41 08
Runner-05: 21 09 25 71 49 02 27 15 91 52 37 88 62 42 01 76 26 89 91 91
Runner-06: 90 27 89 89 21 77 68 41 43 46 91 46 23 38 78 38 03 40 64 28
Runner-02: 65 86 96 25 87 53 97 21 92 98 52 96 21 47 45 33 90 13 91 96
Runner-04: 93 22 24 44 87 79 19 42 38 00 23 09 99 27 06 73 99 98 57 01
Runner-03: 77 47 22 46 30 19 92 70 38 26 69 13 62 18 42 64 94 24 60 39
Runner-00: 57 69 76 25 29 86 52 08 75 83 67 25 72 04 59 96 36 04 30 65

Auch hier kann der Seed nachträglich nicht mit setSeed gesetzt werden. setSeed wirft immer eine UnsupportedOperationException.

Laboraufgabe “gegenseitiger Ausschluss”

Factory0 ist die fehlerhafte Ausgangsklasse.

$ just Factory0

Möglichkeit 1 (in Factory1)

$ just Factory1

korrekt, aber wenig performant: Die Methode getInstance wird mit synchronized markiert.

public static synchronized Type getInstance() {

Der gegenseitige Ausschluss ist größer als die kritische Region, denn die ganze Methode getInstance() wird geschützt.

Möglichkeit 2 (in Factory2)

$ just Factory2

korrekt und performant: Der gegenseitige Ausschluss ist so kurz wie möglich, da nur der “check-then-act”-Teil in einen synchronized-Block verschoben wurde. Als Monitor wird wie bei der vorigen Lösung die Klasse selber (Factory2.class) verwendet. instance ist volatile da außerhalb des synchronized-Blocks darauf zugegriffen wird.

public static Type getInstance() {
    Type.prepare();
    synchronized (Factory2.class) {
        if (instance == null) {
            instance = new Type();
        }
    }
    return instance;
}

Möglichkeit 3 (in Factory3)

$ just Factory3

korrekt und performant: Der gegenseitige Ausschluss ist so kurz wie möglich. Eine Instanz von Object (es könnte auch jeder andere Untertyp von Object verwendet werden) wird als Klassenattribut gespeichert. Es wird instanziiert, wenn die Klasse geladen wird, ist somit von Anfang an vorhanden.

private static Object lock = new Object();

public static Type getInstance() {
    Type.prepare();
    synchronized (lock) {
        if (instance == null) {
            instance = new Type();
        }
    }
    return instance;
}

Dieses Klassenattribut wird als Monitor verwendet. instance ist volatile da außerhalb des synchronized-Blocks darauf zugegriffen wird.

Möglichkeit 4 (in Factory4)

$ just Factory4

korrekt und sehr performant: wie Factory2, aber “Double Checked”. Das dedeutet, dass nur dann in den synchronized-Block gegangen wird, wenn es tatsächlich noch keine Instanz gibt. Die zweite Prüfung auf null im synchronized-Block ist erforderlich, weil in der Zwischenzeit eine Instanz hätte erzeugt werden können.

public static Type getInstance() {
    Type.prepare();
    if (instance == null) {
        synchronized (Factory4.class) {
            if (instance == null) {
                instance = new Type();
            }
        }
    }
    return instance;
}