Musterlösungen
Laboraufgabe “Counter mit Atomics threadsicher machen”
Quellcode von ICounter
Das Interface ist hier nur dazu da, die Lösungen besser zu strukturieren.
Quellcode von CounterTest
Dieses Interface wird von einer Hilfsmethode in Test genutzt, um unterschiedliche Implementierungen von Counter zu prüfen.
private static void testIncrement(ICounter counter) {
assertEquals(0, counter.get());
var t1 = new Thread(() -> {
for (var j = 0; j < RUNS; j++) {
counter.inc();
}
});
var t2 = new Thread(() -> {
for (var j = 0; j < RUNS; j++) {
counter.inc();
}
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
assertEquals(RUNS * 2, counter.get());
}In dieser Methode werden zwei Threads gestartet, die beide nebenläufig auf eine Instanz der als Parameter übergebenen ICounter-Implementerung zugreifen und ständig inc() daran aurufen. Am Ende, wenn beide Threads fertig sind, wird mit get() auf das ICounter-Objekt zugegriffen un der dort abgerufene Wert der Instanzvariable mit dem erwarteten verglichen.
Wenn Transaktionen (inc) sich fälschlicherweise überlappt haben, kann es dazu kommen, dass die dort abgerufene Zahl nicht der erwarteten entspricht.
In Test werden unterschiedliche Implementierungen von ICounter auf diese Art getestet. Diese Unit-Tests selber sind immer gleich aufgebaut:
Threadsicherheit mit synchronized
Die Threadsicherheit kann leicht als Intrinsic Lock mit dem synchronized-Schlüsselwort hergestellt werden:
package pp;
class SynchronizedCounter implements ICounter {
private int c;
public SynchronizedCounter(int init) {
this.c = init;
}
@Override
public synchronized void inc() {
this.c++;
}
@Override
public synchronized void dec() {
this.c--;
}
@Override
public synchronized int get() {
return this.c;
}
}Der Test dazu in Test:
Der befürchtete Nachteil, dass die beiden Threads fast gar nicht mehr parallel arbeiten können, bewahrheitet sich nicht bzw. die Implementierung von synchronized ist sehr effizient. Die Laufzeit auf einem Core i5-Rechner ist 45 ms.
Threadsicherheit durch AtomicInteger und compareAndSet
Statt in einem Bereich mit gegenseitigem Ausschluss mit ++ und -- zuzugreifen wird compareAndSet benutzt.
package pp;
import java.util.concurrent.atomic.AtomicInteger;
class AtomicCounter1 implements ICounter {
private final AtomicInteger c;
public AtomicCounter1(int init) {
this.c = new AtomicInteger(init);
}
@Override
public void inc() {
var temp = this.c.get();
while (!this.c.compareAndSet(temp, temp + 1)) {
temp = this.c.get();
}
}
@Override
public void dec() {
var temp = this.c.get();
while (!this.c.compareAndSet(temp, temp - 1)) {
temp = this.c.get();
}
}
@Override
public int get() {
return this.c.get();
}
}Der Test dazu in Test:
Die Laufzeit auf einem Core i5-Rechner ist 65 ms.
Threadsicherheit durch AtomicInteger und incrementAndGet bzw. decrementAndGet
Statt in einem Bereich mit gegenseitigem Ausschluss mit ++ und -- zuzugreifen wird incrementAndGet bzw. decrementAndGetbenutzt.
package pp;
import java.util.concurrent.atomic.AtomicInteger;
class AtomicCounter2 implements ICounter {
private final AtomicInteger c;
public AtomicCounter2(int init) {
this.c = new AtomicInteger(init);
}
@Override
public void inc() {
this.c.incrementAndGet();
}
@Override
public void dec() {
this.c.decrementAndGet();
}
@Override
public int get() {
return this.c.get();
}
}Der Test dazu in Test:
Die Laufzeit auf einem Core i5-Rechner ist 31 ms.
Weitere Lösungsmöglichkeit für Threadsicherheit durch AtomicInteger
Diese Lösung ist analog zu AtomicCounter1, jedoch sind bei dieser Implementierung die Schleifen etwas anders gebaut, um quasi Quelltext-Gleichheit mit der internen Implementierung von incrementAndGet() bzw. decrementAndGet() zu erreichen1.
package pp;
import java.util.concurrent.atomic.AtomicInteger;
class AtomicCounter3 implements ICounter {
private final AtomicInteger c;
public AtomicCounter3(int init) {
this.c = new AtomicInteger(init);
}
@Override
public void inc() {
for (;;) {
var current = this.c.get();
var next = current + 1;
if (this.c.compareAndSet(current, next)) {
break;
}
}
}
@Override
public void dec() {
for (;;) {
var current = this.c.get();
var next = current - 1;
if (this.c.compareAndSet(current, next)) {
break;
}
}
}
@Override
public int get() {
return this.c.get();
}
}Der Test dazu in Test:
Die Laufzeit auf einem Core i5-Rechner ist 58 ms.
Eine weitere Variante mit übergebenen Funktionen in Form von Lambda-Ausdrücken ist in AtomicCounter4:
package pp;
import java.util.concurrent.atomic.AtomicInteger;
class AtomicCounter4 implements ICounter {
private final AtomicInteger c;
public AtomicCounter4(int init) {
this.c = new AtomicInteger(init);
}
@Override
public void inc() {
this.c.updateAndGet(i -> i + 1);
}
@Override
public void dec() {
this.c.updateAndGet(i -> i - 1);
}
@Override
public int get() {
return this.c.get();
}
}Der Test dazu in Test:
Die Laufzeit auf einem Core i5-Rechner ist 43 ms.
Weitere Tests
In der zweiten Hälfte von Test wird dazu analog das nebenläufige Dekrementieren getestet:
private static void testDecrement(ICounter counter) {
assertEquals(RUNS * 2, counter.get());
var t1 = new Thread(() -> {
for (var j = 0; j < RUNS; j++) {
counter.dec();
}
});
var t2 = new Thread(() -> {
for (var j = 0; j < RUNS; j++) {
counter.dec();
}
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
assertEquals(0, counter.get());
}
@Test
void testCounterDecrement() throws InterruptedException {
testDecrement(new Counter(RUNS * 2));
}
@Test
void testSynchronizedCounterDecrement() throws InterruptedException {
testDecrement(new SynchronizedCounter(RUNS * 2));
}
@Test
void testAtomicCounter1Decrement() throws InterruptedException {
testDecrement(new AtomicCounter1(RUNS * 2));
}
@Test
void testAtomicCounter2Decrement() throws InterruptedException {
testDecrement(new AtomicCounter2(RUNS * 2));
}
@Test
void testAtomicCounter3Decrement() throws InterruptedException {
testDecrement(new AtomicCounter3(RUNS * 2));
}
@Test
void testAtomicCounter4Decrement() throws InterruptedException {
testDecrement(new AtomicCounter4(RUNS * 2));
}s. Implementierung von
AtomicIntegerim OpenJDK Github-Repository: https://github.com/openjdk-mirror/jdk7u-jdk/blob/master/src/share/classes/java/util/concurrent/atomic/AtomicInteger.java (letzter Zugriff: 23.10.2024)↩︎