Advanced   Java   Services AtomicInteger


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.


Ein Vergleich veschiedener Counter

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.





Vergleich einzelner Aufrufe

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.


Vergleich der Ergebnisse bei mehreren Aufrufen

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.


Vergleich: Drei Threads zählen jeweils 1000-mal hoch

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");
}

Vergleich der Ergebnisse bei jeweils sechs Abläufen

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