Advanced Java Services | AtomicInteger |
Variable eines Ganzzahldatentyps werden als Zählvariable unbrauchbar, sobald mehrere Threads auf den Counter zugreifen. Eine Operation wie i++ ist zwar nur eine Anweisung in Java, der dahinterstehende Vorgang besteht jedoch auf Assemblerebene aus mehreren Anweisungen und entspricht in etwa dem folgendem Funktionaufruf.
public int getAndIncrement() { int oldValue = counter; counter = oldValue +1 ; return oldValue; }
Analog entspricht die Anweisung ++i in etwa dem folgendem Funktionaufruf.
public int incrementAndGet() { int value = counter; value = value + 1 ; counter = value; return value; }
Greifen nun mehrere Threads auf dieselbe Variable zu kommt es mit großer Wahrscheinlichkeit zu Dateninkonsistenzen, da in der Pause zwischen zwei Anweisungen ein anderer Thread Zugriff auf die Variable bekommen kann. Die folgenden Beispiel werden dies zeigen. Datensicherheit kann man dadurch erreichen, daß man mit Hilfe von synchronized die Anweisungen zu einem dann atomaren Block zusammenfaßt. Seit Java 1.5 gibt es jedoch eine Satz von Atomicklassen, die atomare Methoden anbieten und damit synchronized überflüssig machen. Noch dazu sind diese Methoden wesentlich performanter. Diese Klassen nützen aus, daß neuere Prozessoren die Fähigkeit haben schon auf Prozessorebene mehrere Anweisungen atomar ablaufen zu laassen.
Im folgenden werden fünf Counter eingeführt und verglichen.
SimpleCounter bildet den Vorgang des Hochzählens mit eigenen Anweisungen nach.
/** */ class SimpleCounter { private int counter = 0; public void increment() { int value = counter; value = value +1 ; counter = value; } public int incrementAndGet() { int value = counter; value = value +1 ; counter = value; return counter; } public int value() { return counter; } }
IntegerCounter verwendet den ++Operator.
/** */ class IntegerCounter { private int counter = 0; public void increment() { counter++; } public int incrementAndGet() { return ++counter; } public int value() { return counter; } }
VolatileCounter verwendet als Counter eine volatile int.
/** */ class VolatileCounter { private volatile int counter = 0; public void increment() { counter++; } public int incrementAndGet() { return ++counter; } public int value() { return counter; } }
SynchronizedCounter verwendet synchronized Methoden.
/** */ class SynchronizedCounter { private int counter = 0; public synchronized void increment() { int value = counter; value = value + 1 ; counter = value; } public synchronized int incrementAndGet() { int value = counter; value = value +1 ; counter = value; return counter; } public int value() { return counter; } }
AtomicInteger.
Hier brauchen wir keine eigene Klasse. AtomicInteger kann direkt verwendet werden.
Die folgende Testmethode ist für alle fünf Varianten gleich aufgebaut. Es werden zwei Threads gestartet. Jedes run() zählt den Zähler um eins hoch und gibt den hochgezählten Wert aus. Egal welcher Thread zuerst startet, der zweite Thread sollte 2 ausgeben.
private static void xxxCounter() { final XxxCounter counter = new XxxCounter(); Runnable r = new Runnable(){ @Override public void run() { System.out.println(Thread.currentThread().getName() + " : " + counter.incrementAndGet()); }}; Thread t0 = new Thread(r); Thread t1 = new Thread(r); t0.start(); t1.start(); }
Für Xxx setze man der Reihe nach Simple, Integer, Volatile, Synchronzed oder Atomic.
SimpleCounter
meist: Thread-0 : 1 Thread-1 : 2 gelegentlich: Thread-0 : 2 Thread-1 : 1 Thread-1 : 2 Thread-1 : 1 Thread-0 : 2 Thread-0 : 1 selten: Thread-0 : 1 Thread-1 : 1 Thread-1 : 1 Thread-0 : 1
IntegerCounter
meist: Thread-0 : 1 Thread-1 : 2 gelegentlich: Thread-0 : 2 Thread-1 : 1 Thread-1 : 2 Thread-1 : 1 Thread-0 : 2 Thread-0 : 1 selten: Thread-0 : 1 Thread-1 : 1 Thread-1 : 1 Thread-0 : 1
VolatileCounter
meist: Thread-0 : 1 Thread-1 : 2 gelegentlich: Thread-0 : 2 Thread-1 : 1 Thread-1 : 2 Thread-1 : 1 Thread-0 : 2 Thread-0 : 1 sehr selten: Thread-0 : 1 Thread-1 : 1 Thread-1 : 1 Thread-0 : 1
SynchronizedCounter
Thread-0 : 1 Thread-0 : 2 Thread-1 : 1 Thread-1 : 2 Thread-1 : 2 Thread-1 : 1 Thread-0 : 2 Thread-0 : 1
Nur diese vier Fälle treten auf.
AtomicCounter
Thread-0 : 1 Thread-0 : 2 Thread-1 : 1 Thread-1 : 2 Thread-1 : 2 Thread-1 : 1 Thread-0 : 2 Thread-0 : 1
Nur diese vier Fälle treten auf.
Bereits dieser einfache Vergleich zeigt, daß in int-Variable in solchen Situationen unbrauchbare Ergebnisse liefern. Wie falsch die Ergebnisse sind, zeigt der nächste Test.
Die Testmethode hat wiederum für alle Fälle denselben Aufbau.
private static int test_xxxCounter() throws InterruptedException { final XxxCounter counter = new XxxCounter(); Runnable ru = new Runnable(){ @Override public void run() { for(int i=0; i<1000; i++) counter.increment(); }}; Thread th1 = new Thread(ru); Thread th2 = new Thread(ru); Thread th3 = new Thread(ru); th1.start(); th2.start(); th3.start(); th1.join(); th2.join(); th3.join(); System.out.println("counter simple value = " + counter.value()); return counter.value(); }
Hier steht Xxx wieder für Simple, Integer, Volatile
Die Counter werden in main mit dem Wert 0 initialisiert. Jeder Thread zählt 1000 mal hoch. main wartet, bis alle Threads zu Ende sind und gibt dann den Counter aus, d.h. der Counter sollte jedesmal auf 3000 stehn. Nur SynchronizedCounter und AtomicCounter arbeiten korrekt, bei allen anderen gibt es zum Teil erhebliche Abweichungen.
Für die beiden Counter, die korrekt arbeiten messen wir noch wie lange das Hochzählen dauert. Hier steht Xxx für Synchronized bzw. Atomic.
private static void test_xxxCounter() throws InterruptedException { LocalTime lt1 = LocalTime.now(); final XxxCounter counter = new XxxCounter(); Runnable ru = new Runnable(){ @Override public void run() { for(int i=0; i<1000; i++) counter.incrementAndGet(); }}; Thread th1 = new Thread(ru); Thread th2 = new Thread(ru); Thread th3 = new Thread(ru); th1.start(); th2.start(); th3.start(); th1.join(); th2.join(); th3.join(); LocalTime lt2 = LocalTime.now(); Duration dauer = Duration.between(lt1, lt2); System.out.println("counter synchronized value = " + counter.value()); System.out.println(dauer.toMillis() + " millis"); }
SimpleCounter
counter simple value = 2315 counter simple value = 2336 counter simple value = 2987 counter simple value = 1838 counter simple value = 3000 counter simple value = 2807
IntegerCounter
counter integer value = 2867 counter integer value = 2810 counter integer value = 2811 counter integer value = 3000 counter integer value = 2836 counter integer value = 2276
VolatileCounter
counter volatile value = = 3000 counter volatile value = = 2711 counter volatile value = = 3000 counter volatile value = = 2788 counter volatile value = = 2736 counter volatile value = = 2453
SynchronizedCounter
counter synchronized value = 3000 14 millis counter synchronized value = 3000 12 millis counter synchronized value = 3000 12 millis counter synchronized value = 3000 13 millis counter synchronized value = 3000 12 millis counter synchronized value = 3000 12 millis
AtomicCounter
counter synchronized value = 3000 3 millis counter synchronized value = 3000 2 millis counter synchronized value = 3000 2 millis counter synchronized value = 3000 2 millis counter synchronized value = 3000 1 millis counter synchronized value = 3000 2 millis