Probleme im Zusammenhang mit Multithreading (Concurrency)

+ andere TechDocs
+ Best Practices
+


Concurrency (Multithreading, Nebenläufigkeit) gehört zu den schwierigsten Themen der Java-Programmierung. Die möglichen Fehler sind häufig schwer zu erkennen und meistens schwer zu debuggen, da sie vielleicht nur sporadisch und nicht so einfach reproduzierbar auftreten.

Die Auswirkungen treten oft erst im Produktivbetrieb auf und können erheblich sein: Im "guten Fall" blockierte Anwendungen und Server, im schlimmeren Fall verlorene oder verfälschte Daten.

Brian Goetz zitiert in seinem Buch folgendermaßen Dion Almaer: "... most Java programs are so rife with concurrency bugs that they work only 'by accident'".

Dieses Dokument versucht einige wichtige Aspekte herauszugreifen, allerdings verkürzt. Für eine ausführliche und genaue Beschreibung sei zum Beispiel auf folgende Dokumentationen verwiesen:



Inhalt

  1. 15 verbreitete Irrtümer
  2. Erzeugen von Threads
    Thread, Runnable, ExecutorService, Unregelmäßigkeiten
  3. Fehlschlagende Synchronisationsversuche am Zähler-Beispiel
    static, volatile, synchronized, lockObject, Synchronisation
  4. Fehlschlagende Synchronisationsversuche mit Collections
    "final" und "synchronizedList" schützen nicht ausreichend, Lock mit "this" oder auf Methoden schützt nicht bei statischen Objekten, "static synchronized" genügt nicht immer
  5. Deadlock
    Dynamic lock-ordering Deadlock, Deadlock auch mit nur einem "synchronized", Deadlock auch ohne explizite Verwendung von "synchronized", Deadlock während der Klassen-Initialisierung, Programmatische Deadlock-Erkennung, Deadlock-Fehlalarm
  6. Immutable
  7. Grad der Threadsicherheit
  8. Allgemeine Hinweise für sicheres Multithreading
    Multithreading-Sicherheit, Zu wenig oder zu viel Synchronisation, Lese- und Schreiboperationen mit demselben Lockobjekt sichern, Zusammenhängende Variablen, static, final, volatile, synchronized, lockObject, Sichtbarkeit zwischen Threads, long und double sind nicht atomic, Operatoren sind nicht atomic, Snapshot-Liste statt synchronisiertem Iterator, Nicht-threadsichere Collections, Andere nicht-threadsichere Klassen, CountDownLatch und Semaphore statt wait() und notify(), Thread.sleep() statt yield(), ReentrantLock.unlock() in finally, Work Queue, Thread Pool und Task-Steuerung, FindBugs, JUnit- und Lasttests, ThreadInfo, EJB im Java EE Application Server
  9. Multithread-sichere Lazy-Initialisierung ("on Demand")
    Normale Initialisierung, Lazy Initialization holder Class Idiom, Double-checked Locking Idiom ("DCL")
  10. Multithread-sicherer Read-only-Cache ("Memoizer")
    Synchronized Memoizer, ConcurrentHashMap Memoizer, Optimierter Memoizer, Memoizer-Test
  11. Parallele Abarbeitung vieler Tasks
    TasksConcurrentExecutor, TasksConcurrentExecutor-Test
  12. Parallele Abarbeitung vieler Tasks mit dem Stream-Api ab Java 8
  13. Singleton
    Singleton bis Java 1.4, Singleton ab Java 5, Singleton mit Initialisierungsfehler im final-Attribut



15 verbreitete Irrtümer

Ich habe keine Multithreading-Probleme, weil ich keine Threads erzeuge
Auch wenn man selbst keine Threads erzeugt, sind die meisten Java-Programme multithreaded, zum Beispiel weil ein Framework (wie das AWT oder Swing) oder ein Application Server mehrere Threads startet, oder durch die Verwendung von Callbacks, RMI, Timer, ...
Ich verwende EJBs, zu denen der Java EE Application Server Multithreading-Sicherheit sicherstellt
Für EJBs in Java EE Application Servern empfiehlt Sun strenge "EJB Restrictions" (genauer beschrieben im Kapitel "21.1.2 Programming Restrictions" aus der ejb-3_0-fr-spec-ejbcore.pdf zum JSR 220). Allerdings werden diese meistens nicht konsequent eingehalten. Und selbst bei Einhaltung kann es zu Multithreading-Problemen kommen, zum Beispiel bei statusbehafteten Diensten und mit "static final"-Objekten. Häufige Schwachstellen sind statusbehaftete Servlets, "static"-Variablen, "static final"-Objekte, Singletons, Lazy Initialization, Caches, ServiceLocator etc.
Ich habe keine Multithreading-Probleme, weil ich nur threadsichere Klassen verwende
Die "Komposition" threadsicherer Klassen ist häufig nicht threadsicher, zum Beispiel wegen zusammengehörender Variablen (wie Koordinaten).
Ich habe keine Multithreading-Probleme, weil ich nirgendwo "static" verwende
Auch ohne "static" kann es Multithreading-Probleme geben, zum Beispiel in statusbehafteten Servlets oder im Beispiel InkonsistenzenOhneStatic.
Wenn ich meine "static"-Variablen als "static final" deklariere, sind sie threadsicher
Das trifft für primitive Typen zu, aber natürlich nicht für Objektreferenzen, wie das Beispiel FalschSynchronisierteCollection1 zeigt. Auch harmlos aussehende Zeilen können Multithreading-Probleme verursachen, wie zum Beispiel:
static final SimpleDateFormat DF = new SimpleDateFormat("yyyy-MM-dd");
Ich habe keine Multithreading-Probleme, weil alle Schreibzugriffe synchronisiert sind
Wenn die Leseoperationen gar nicht oder nicht mit demselben Lockobjekt synchronisiert sind, kommt es zu Inkonsistenzen, zum Beispiel wegen fehlender Sichtbarkeit.
Ich habe keine Multithreading-Probleme, weil meine Methoden "synchronized" sind
Das genügt oft nicht, zum Beispiel weil "this" das falsche "lockObject" ist oder bei statischen Variablen, siehe FalscheSynchronisation1.
Ich habe keine Multithreading-Probleme, weil ich alles per "synchronized(lockObject)" abgesichert habe
Auch das kann ungenügend sein, wenn ein ungünstiges lockObject verwendet wird, siehe FalscheSynchronisation2. Dass die Wahl des richtigen lockObjects nicht immer einfach ist, wird auch bei Collections.synchronizedMap() beschrieben.
Deadlock, Livelock und Starvation kann bei mir nicht passieren, weil ich nirgendwo "synchronized" verwende
Nicht nur zu wenig, auch zu viel Synchronisation ist schädlich: Nicht nur wegen der Performancebeeinträchtigung, sondern auch wegen der Gefahr, dass es zum Stuck Thread, Deadlock, Livelock oder zur Starvation kommt. Auch wenn Sie selbst nirgendwo "synchronized" verwenden, so verwenden Sie sehr wahrscheinlich viele synchronisierte JDK-Objekte, wie zum Beispiel StringBuffer, Vector, Stack und Hashtable. Deadlock-Beispiele finden Sie unter Dynamic lock-ordering Deadlock, DeadlockMitSynchronized, DeadlockMitStringBuffer und DeadlockClassInitialisierung.
Wenn ich "Collections.synchronized...()" verwende, brauche ich mich nicht um Threadsicherheit zu kümmern
Bereits die API-Javadoc zu Collections.synchronized...() weist darauf hin, dass zum Beispiel Iteratoren extern synchronisiert werden müssen. Bei Collections.synchronizedMap() muss sogar besonders aufgepasst werden, damit beim keySet auf das richtige Lockobjekt synchronisiert wird. Weitere Fehlermöglichkeiten zeigen FalschSynchronisierteCollection1, FalschSynchronisierteCollection2 und FalschSynchronisierteCollection3.
Meine Klasse ist immutable (und threadsicher), weil die Attribute privat sind und es nur Getter gibt
Immutable Klassen benötigen keine Synchronisation. Aber private Attribute und keine Setter-Methoden genügt nicht für Unveränderbarkeit, wie das ImmutableTest-Beispiel zeigt.
Lesen und Schreiben von Referenzen und primitiven Typen (boolean, int, long, double, ...) ist atomic
Die Java Language Specification ("JLS") garantiert, dass das Lesen und Schreiben von Referenzen und Variablen atomar ist, außer für long und double. Unsynchronisierter konkurrierender Zugriff auf eine long-Variable kann also dazu führen, dass die oberen und unteren 32 Bit von verschiedenen long-Werten stammen.
Wenn ich den Wert einer Variablen ändere, ist diese Änderung in anderen Threads sichtbar
Die Java Language Specification garantiert nicht, dass der in einem Thread in eine Variable geschriebene Wert für andere Threads sichtbar ist. Die Variable könnte zum Beispiel temporär in dem Thread-spezifischem Cache oder in einem CPU-Register gehalten werden.
Meine "Double-checked Locking"-Version ist sicher
Double-checked Locking konnte bis Java 1.4 nicht funktionieren. Erst ab Java 5 gibt es sicheres Double-checked Locking.
Bei Datenbankzugriffen sorgt das RDBMS für Multithreading-Sicherheit und "ACID"
Reale Datenbanken werden aus Performancegründen nur selten mit dem maximalen "Transaction Isolation Level" betrieben. Häufig ist "Read Committed" eingestellt. Je nach Einstellung kann es zu Concurrency-Problemen wegen Dirty Read, Non-repeatable Read und Phantom Read kommen (siehe Programmierbeispiel). Bei Verwendung von ORM-Tools (wie z.B. JPA oder Hibernate) gibt es weitere Concurrency-Überraschungen, weil der DB-seitige Transaction Isolation Level teilweise ausgehebelt wird, siehe java-hibernate.htm#First-Level-Cache. Außerdem ist bei Datenbanken grundsätzlich die Gefahr von Deadlocks groß (z.B. bei "Select for update ...").


Erzeugen von Threads

Die im Folgenden vorgestellten Programmierbeispiele können Sie auch als Zipdatei downloaden.

Es gibt verschiedene Möglichkeiten, um innerhalb eines Java-Programms Threads zu erzeugen. Einige einfache werden im Folgenden vorgestellt.

Die "Thread.sleep(1)"-Zeilen sind nur eingefügt, damit die Threads häufiger wechseln können (und damit es nicht so schnell zur Starvation kommt).

Threads per Thread-Klasse

public class ThreadsPerThreadKlasse   // (Mit noch fehlender Synchronisation, siehe unten)
{
   public static void main( String[] args )
   {
      (new MeinThread( 1 )).start();
      (new MeinThread( 2 )).start();
      (new MeinThread( 3 )).start();
   }
}

class MeinThread extends Thread
{
   static int zaehler = 0;
   int meineThreadNum;

   MeinThread( int meineThreadNum )
   {
      this.meineThreadNum = meineThreadNum;
   }

   @Override
   public void run()
   {
      while( zaehler < 1000 ) {
         System.out.println( "Thread " + meineThreadNum + ": " + ++zaehler );
         try { Thread.sleep( 1 ); } catch( InterruptedException ex ) {/*ok*/}
      }
   }
}

Threads per Runnable

public class ThreadsPerRunnable   // (Mit noch fehlender Synchronisation, siehe unten)
{
   public static void main( String[] args )
   {
      (new Thread( new MeinRunnable1( 1 ) )).start();
      (new Thread( new MeinRunnable1( 2 ) )).start();
      (new Thread( new MeinRunnable1( 3 ) )).start();
   }
}

class MeinRunnable1 implements Runnable
{
   static int zaehler = 0;
   int meineThreadNum;

   MeinRunnable1( int meineThreadNum )
   {
      this.meineThreadNum = meineThreadNum;
   }

   @Override
   public void run()
   {
      while( zaehler < 1000 ) {
         System.out.println( "Thread " + meineThreadNum + ": " + ++zaehler );
         try { Thread.sleep( 1 ); } catch( InterruptedException ex ) {/*ok*/}
      }
   }
}

Threads per ExecutorService (ab Java 5)

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadsPerExecutorService   // (Mit noch fehlender Synchronisation, siehe unten)
{
   public static void main( String[] args )
   {
      ExecutorService threadPool = Executors.newFixedThreadPool( 3 );
      threadPool.execute( new MeinRunnable2( 1 ) );
      threadPool.execute( new MeinRunnable2( 2 ) );
      threadPool.execute( new MeinRunnable2( 3 ) );
      threadPool.shutdown();
   }
}

class MeinRunnable2 implements Runnable
{
   static int zaehler = 0;
   int meineThreadNum;

   MeinRunnable2( int meineThreadNum )
   {
      this.meineThreadNum = meineThreadNum;
   }

   @Override
   public void run()
   {
      while( zaehler < 1000 ) {
         System.out.println( "Thread " + meineThreadNum + ": " + ++zaehler );
         try { Thread.sleep( 1 ); } catch( InterruptedException ex ) {/*ok*/}
      }
   }
}

Unregelmäßigkeiten (wegen fehlender Synchronisation)

Die Ausgabe von allen drei Varianten könnte beispielsweise so aussehen (eventuell erscheinen die Unregelmäßigkeiten erst bei höheren Zahlen):

Thread 1: 1
Thread 1: 3
Thread 1: 4
Thread 1: 5
Thread 1: 6
Thread 1: 7
Thread 2: 7
Thread 1: 8
Thread 2: 9
Thread 2: 12
Thread 1: 10
Thread 2: 13
Thread 3: 11
Thread 2: 15
Thread 1: 14
Thread 3: 17
...

Bemerkenswert ist Folgendes:



Fehlschlagende Synchronisationsversuche am Zähler-Beispiel

Inkonsistenzen auch ohne "static"-Variable

Die oben dargestellten Multithreading-bedingten Inkonsistenzen treten besonders gerne im Zusammenhang mit statischen Variablen auf. Aber wie das folgende Beispiel zeigt, können dieselben Inkonsistenzen auch dann auftreten, wenn die zaehler-Variable nicht static deklariert ist:

public class InkonsistenzenOhneStatic   // Synchronisationsfehler, obwohl ohne "static"
{
   int zaehler = 0;  // ohne static

   public static void main( String[] args )
   {
      (new InkonsistenzenOhneStatic()).starteThreads();
   }

   void starteThreads()
   {
      (new Thread( new MeinRunnableOS( 1, this ) )).start();
      (new Thread( new MeinRunnableOS( 2, this ) )).start();
      (new Thread( new MeinRunnableOS( 3, this ) )).start();
   }
}

class MeinRunnableOS implements Runnable
{
   int meineThreadNum;
   InkonsistenzenOhneStatic mom;

   MeinRunnableOS( int meineThreadNum, InkonsistenzenOhneStatic mom )
   {
      this.meineThreadNum = meineThreadNum;
      this.mom = mom;
   }

   @Override
   public void run()
   {
      while( mom.zaehler < 1000 ) {
         System.out.println( "Thread " + meineThreadNum + ": " + ++mom.zaehler );
         try { Thread.sleep( 1 ); } catch( InterruptedException ex ) {/*ok*/}
      }
   }
}

Ein anderes Beispiel dafür, dass ohne Verwendung von static Multithreading-Probleme möglich sind, zeigt das folgende Servlet. Die service()-Methode dieses Servlet-Objekts wird durch den Servlet-Container für viele Threads verwendet. Da "++zaehler" nicht atomar ist, kommt es zu Fehlzählungen (was z.B. für eindeutige Schlüssel katastrophal wäre).

import javax.servlet.*;

public class ServletMitZaehlFehler implements Servlet   // Synchronisationsfehler, obwohl ohne "static"
{
   private long zaehler = 0;

   public long getZaehler() { return zaehler; }

   public void service( ServletRequest requ, ServletResponse resp )
   {
      ++zaehler;
      // ...
   }
}

Weitere mögliche Fehlerquellen in Servlets sind beispielsweise die Injektion einer Stateful Session EJB per @EJB oder eines JPA-EntityManagers per @PersistenceContext.

Inkonsistenzen auch mit "volatile" und "synchronized"

Seit Java 5 gibt es ein neues Java Memory Model (siehe JSR 133). Seitdem hat der Modifizierer "volatile" eine wesentlich striktere Bedeutung und kann viele Multithreading-bedingte Probleme verhindern, weil der Inhalt dieser Variablen jedesmal aus dem Hauptspeicher geholt wird (und z.B. nicht aus dem Thread-spezifischem Cache oder aus CPU-Registern). Aber in diesem Beispiel hilft "volatile" nicht (z.B. weil "++zaehler" eine nicht-atomare Zuweisung ist).

Um "++zaehler" nicht unterbrechbar auszuführen, könnte man versuchen, die run()-Methode per "synchronized" zu synchronisieren. Aber wie die Ausgabe des folgenden Beispiels zeigt, funktioniert auch das nicht, weil die verschiedenen Threads run()-Methoden aus verschiedenen Instanzen verwenden.

public class FalscheSynchronisation1  // Synchronisationsfehler trotz "volatile" und "synchronized"
{
   public static void main( String[] args )
   {
      (new Thread( new MeinRunnableFS1( 1 ) )).start();
      (new Thread( new MeinRunnableFS1( 2 ) )).start();
      (new Thread( new MeinRunnableFS1( 3 ) )).start();
   }
}

class MeinRunnableFS1 implements Runnable
{
   static volatile int zaehler = 0;  // volatile nuetzt hier nicht
   int meineThreadNum;

   MeinRunnableFS1( int meineThreadNum )
   {
      this.meineThreadNum = meineThreadNum;
   }

   @Override
   public synchronized void run()    // synchronized nuetzt hier nicht
   {
      while( zaehler < 1000 ) {
         System.out.println( "Thread " + meineThreadNum + ": " + ++zaehler );
         try { Thread.sleep( 1 ); } catch( InterruptedException ex ) {/*ok*/}
      }
   }
}

Inkonsistenzen auch mit "synchronized(lockObject)"

Auch die (fehlerhafte) Synchronisation über ein "lockObject" kann unnütz sein, wie die Ausgabe des folgenden Beispiels zeigt:

public class FalscheSynchronisation2   // Synchronisationsfehler trotz "synchronized( lockObject )"
{
   public static void main( String[] args )
   {
      (new Thread( new MeinRunnableFS2( 1 ) )).start();
      (new Thread( new MeinRunnableFS2( 2 ) )).start();
      (new Thread( new MeinRunnableFS2( 3 ) )).start();
   }
}

class MeinRunnableFS2 implements Runnable
{
   final Object lockObject = new Object(); // lockObject als Instanzvariable
   static int zaehler = 0;
   int meineThreadNum;

   MeinRunnableFS2( int meineThreadNum )
   {
      this.meineThreadNum = meineThreadNum;
   }

   @Override
   public void run()
   {
      synchronized( lockObject ) {         // locking nuetzt so nicht
         while( zaehler < 1000 ) {
            System.out.println( "Thread " + meineThreadNum + ": " + ++zaehler );
            try { Thread.sleep( 1 ); } catch( InterruptedException ex ) {/*ok*/}
         }
      }
   }
}

"synchronized(lockObject)" kann Multithreading verhindern

Damit die Blockade auf alle Threads wirkt, könnte man das "lockObject" als "static" deklarieren. Dies führt jedoch im folgenden Beispiel dazu, dass die Zählschleife nur noch von einem einzigen Thread durchlaufen wird:

public class FalscheSynchronisation3   // Falsche Synchronisation verhindert Multithreading
{
   public static void main( String[] args )
   {
      (new Thread( new MeinRunnableFS3( 1 ) )).start();
      (new Thread( new MeinRunnableFS3( 2 ) )).start();
      (new Thread( new MeinRunnableFS3( 3 ) )).start();
   }
}

class MeinRunnableFS3 implements Runnable
{
   static final Object lockObject = new Object(); // static lockObject
   static int zaehler = 0;
   int meineThreadNum;

   MeinRunnableFS3( int meineThreadNum )
   {
      this.meineThreadNum = meineThreadNum;
   }

   @Override
   public void run()
   {
      synchronized( lockObject ) {                // locking verhindert Multithreading
         while( zaehler < 1000 ) {
            System.out.println( "Thread " + meineThreadNum + ": " + ++zaehler );
            try { Thread.sleep( 1 ); } catch( InterruptedException ex ) {/*ok*/}
         }
      }
   }
}

Funktionierende Synchronisation

Das Beispiel kann mit "static" deklariertem lockObject und eingegrenztem Locking-Bereich so synchronisiert werden, dass es kontinuierlich hochzählt. Bitte beachten Sie, dass dies nur für bestimmte Situationen eine geeignete Lösung ist und andere Situationen anders behandelt werden müssen.

public class FunktionierendeSynchronisation   // Funktionierende Synchronisation
{
   public static void main( String[] args )
   {
      (new Thread( new MeinRunnableOK( 1 ) )).start();
      (new Thread( new MeinRunnableOK( 2 ) )).start();
      (new Thread( new MeinRunnableOK( 3 ) )).start();
   }
}

class MeinRunnableOK implements Runnable
{
   private static final Object lockObject = new Object();
   static int zaehler = 0;
   int meineThreadNum;

   MeinRunnableOK( int meineThreadNum )
   {
      this.meineThreadNum = meineThreadNum;
   }

   @Override
   public void run()
   {
      while( zaehler < 1000 ) {
         synchronized( lockObject ) {
            System.out.println( "Thread " + meineThreadNum + ": " + ++zaehler );
         }
         try { Thread.sleep( 1 ); } catch( InterruptedException ex ) {/*ok*/}
      }
   }
}

Bitte beachten Sie: Erfolgreich synchronisiert ist lediglich das kontinuierliche Hochzählen. Die Zählgrenze 1000 ist nicht innerhalb der Synchronisation und wird deshalb hin und wieder überschritten.

Ab Java 5 gibt es eine einfachere und bessere Lösung durch Verwendung von "AtomicLong" (siehe unten). Damit könnte auch erreicht werden, das die Zählgrenze 1000 nicht überschritten wird.



Fehlschlagende Synchronisationsversuche mit Collections

"final" und "synchronizedList" schützen nicht ausreichend

Manchmal wird angenommen, dass Zugriffe auf als "final" oder "static final" deklarierte Objekte nicht synchronisiert werden müssen. Das folgende Beispiel zeigt das Gegenteil: Die einzelnen Threads werden durch IndexOutOfBoundsException abgebrochen, bis nur noch ein Thread übrig bleibt.

import java.util.*;

public class FalschSynchronisierteCollection1   // Synchronisationsfehler auch mit "static final"
{
   public static void main( String[] args )
   {
      (new Thread( new MeinRunnableCol1() )).start();
      (new Thread( new MeinRunnableCol1() )).start();
      (new Thread( new MeinRunnableCol1() )).start();
   }
}

class MeinRunnableCol1 implements Runnable
{
   // "final" und "synchronizedList" nuetzen hier nicht:
   static final List<String> meinAttr = Collections.synchronizedList( new ArrayList<String>() );

   public void run()
   {
      for( int i=0; i<100; i++ ) {
         meinAttr.add( "" + (new Random()).nextLong() );
         meinAttr.add( "" + (new Random()).nextLong() );
         meinAttr.remove( meinAttr.size() - 1 );
         meinAttr.remove( meinAttr.size() - 1 );
         System.out.println( Thread.currentThread().getName() +
                             ": meinAttr.size = " + meinAttr.size() );
         while( meinAttr.size() > 0 ) meinAttr.remove( meinAttr.size() - 1 );
         try { Thread.sleep( 1 ); } catch( InterruptedException ex ) {/*ok*/}
      }
   }
}

Lock mit "this" oder auf Methoden schützt nicht bei statischen Objekten

Statische Objekte können nicht durch nicht-statische synchronized Methoden gesichert werden:

import java.util.*;

public class FalschSynchronisierteCollection2   // Synchronisationsfehler trotz "synchronized"
{
   public static void main( String[] args )
   {
      (new Thread( new MeinRunnableCol2() )).start();
      (new Thread( new MeinRunnableCol2() )).start();
      (new Thread( new MeinRunnableCol2() )).start();
   }
}

class MeinRunnableCol2 implements Runnable
{
   static volatile List<String> meinAttr = null;  // volatile nuetzt hier nicht

   synchronized List<String> getMeinAttr()        // synchronized nuetzt hier nicht
   {
      if( meinAttr == null ) {
         meinAttr = Collections.synchronizedList( new ArrayList<String>() );
         for( int i=0; i<1000; i++ ) {
            meinAttr.add( "s" + i );
         }
      }
      return meinAttr;
   }

   public void run()
   {
      // Fehler: Size ist manchmal kleiner und manchmal groesser als 1000:
      System.out.println( Thread.currentThread().getName() +
                          ": meinAttr.size = " + getMeinAttr().size() );
   }
}

"static synchronized" genügt nicht immer

Auch "static synchronized" genügt nicht, wenn vor Beendigung der Initialisierung bereits andere Threads die zu füllende Collection "sehen":

import java.util.*;

public class FalschSynchronisierteCollection3 // Synchronisationsfehler trotz "static synchronized"
{
   public static void main( String[] args )
   {
      (new Thread( new MeinRunnableCol3() )).start();
      (new Thread( new MeinRunnableCol3() )).start();
      (new Thread( new MeinRunnableCol3() )).start();
   }
}

class MeinRunnableCol3 implements Runnable
{
   static volatile List<String> meinAttr = null;  // volatile nuetzt hier nicht

   static synchronized List<String> getMeinAttr() // static synchronized genuegt hier nicht
   {
      if( meinAttr == null ) {
         meinAttr = Collections.synchronizedList( new ArrayList<String>() );
         for( int i=0; i<1000; i++ ) {
            meinAttr.add( "s" + i );
         }
      }
      return meinAttr;
   }

   public void run()
   {
      if( meinAttr == null ) {
         meinAttr = getMeinAttr();
      }
      // Fehler: Size ist manchmal kleiner als 1000:
      System.out.println( Thread.currentThread().getName() +
                          ": meinAttr.size = " + meinAttr.size() );
   }
}

Weiter unten finden Sie Beschreibungen, wie "Lazy-Initialisierung" threadsicher realisiert werden kann.



Deadlock

"Dynamic lock-ordering Deadlock"

Die meisten Deadlocks entstehen durch doppeltes Locking, wobei A auf B und gleichzeitig B auf A wartet. Ein klassisches Beispiel hierfür ist die Übertragung eines Geldbetrags von einem auf ein anderes Konto. Während der Überprüfung und Übertragung darf kein anderer Thread die Konten ändern, weshalb beide Konten in der uebertrageBetrag()-Methode per synchronized für andere Zugriffe gesperrt werden. Wenn ein Thread versucht, von Konto A auf Konto B zu überweisen, und ein zweiter Thread in die entgegengesetzte Richtung überweisen will, kommt es zum Deadlock.

Entscheidend für einen "Dynamic-lock-ordering-Deadlock" ist:

public class DeadlockKonto
{
   public static void main( String[] args )
   {
      Konto kontoA = new Konto(), kontoB = new Konto();
      (new Thread( new MeinRunnableKto( kontoA, kontoB ) )).start();
      (new Thread( new MeinRunnableKto( kontoB, kontoA ) )).start();
   }
}

class MeinRunnableKto implements Runnable
{
   Konto vonKonto, nachKonto;

   MeinRunnableKto( Konto vonKonto, Konto nachKonto )
   {
      this.vonKonto  = vonKonto;
      this.nachKonto = nachKonto;
   }

   public void run()
   {
      int i = 0;
      while( ++i < 10000 ) {
         uebertrageBetrag( i );
         try { Thread.sleep( 1 ); } catch( InterruptedException ex ) {/*ok*/}
      }
   }

   boolean uebertrageBetrag( int betrag )
   {
      synchronized( vonKonto ) {
         synchronized( nachKonto ) {
            System.out.println( Thread.currentThread().getName() + ": " + betrag );
            if( vonKonto.kontoStand < betrag ) return false;
            vonKonto.einauszahlen( -betrag );
            nachKonto.einauszahlen( betrag );
            return true;
         }
      }
   }
}

class Konto
{
   int kontoStand;

   void einauszahlen( int betrag ) { kontoStand += betrag; }
}

Ob und wie schnell der Deadlock eintritt, hängt von der verwendeten Hardware und der CPU-Anzahl ab.

Sehen Sie sich den Thread-Status in JConsole an: Starten Sie JConsole (jconsole.exe aus dem JDK-bin-Verzeichnis, z.B. C:\Program Files\Java\jdk1.6\bin), verbinden Sie bei laufendem DeadlockKonto-Programm mit dem Local Process "DeadlockKonto", klicken Sie oben auf den Tabulatorreiter "Threads" und wählen Sie unten links die Threads "Thread-0" und "Thread-1". Rechts erscheint der Stacktrace und zum Beispiel:

Name: Thread-0
State: BLOCKED on Konto@165b7e owned by: Thread-1

Wenn Sie auf den "Detect Deadlock"-Button klicken, werden Ihnen die beiden Threads Thread-0 und Thread-1 angezeigt.

Auch im Kommandozeilenfenster können Sie sich den Deadlock anzeigen lassen: Betätigen Sie "Strg + Pause" (bzw. "Ctrl + Break" oder unter Linux "Ctrl + \"), um ungefähr Folgendes zu erhalten:

Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock monitor 0x021b7c34 (object 0x2804c268, a Konto),
  which is held by "Thread-0"
"Thread-0":
  waiting to lock monitor 0x021b7004 (object 0x2804c278, a Konto),
  which is held by "Thread-1"

Brechen Sie das Programm anschließend mit "Strg + C" ab.

Java EE Application Server melden bei Deadlocks häufig "Stuck Thread".

Deadlock auch mit nur einem "synchronized"

Während im letzten Beispiel die doppelte Synchronisierung offensichtlich war, ist sie im folgenden Beispiel etwas schwieriger zu erkennen: Es scheint nur ein einziges synchronized zu geben. Trotzdem entsteht schnell ein Deadlock, weil die synchronisierte bearbeite()-Methode rekursiv zweimal aufgerufen wird.

public class DeadlockMitSynchronized
{
   public static void main( String[] args )
   {
      Item xA = new Item(), xB = new Item();
      (new Thread( new MeinRunnableDl( xA, xB ) )).start();
      (new Thread( new MeinRunnableDl( xB, xA ) )).start();
   }
}

class MeinRunnableDl implements Runnable
{
   Item x1, x2;

   MeinRunnableDl( Item x1, Item x2 )
   {
      this.x1 = x1;
      this.x2 = x2;
   }

   public void run()
   {
      int i = 0;
      while( ++i < 1000 ) {
         x1.bearbeite( x2, i );
      }
   }
}

class Item
{
   synchronized void bearbeite( Item x, int i )
   {
      System.out.println( Thread.currentThread().getName() + ": " + i );
      if( x != null ) x.bearbeite( null, i );
   }
}

Lassen Sie sich den Deadlock wieder mit JConsole oder über "Strg + Pause" anzeigen, bevor Sie mit "Strg + C" abbrechen.

Deadlock auch ohne explizite Verwendung von "synchronized"

In diesem Beispiel ist die doppelte Synchronisierung ebenfalls etwas schwieriger zu erkennen, es scheint gar kein synchronized zu geben: Die Synchronisierung ist im StringBuffer enthalten und die Blockade entsteht in der Zeile "sb1.insert( sb1.length() / 2, sb2 );".

In der main()-Methode wird nach 3 Sekunden der Status der Threads abgefragt. In der Regel ist er dann bereits auf "BLOCKED".

public class DeadlockMitStringBuffer
{
   public static void main( String[] args )
   {
      StringBuffer sbA = new StringBuffer(), sbB = new StringBuffer();
      Thread t1 = new Thread( new MeinRunnableDSB( sbA, sbB ) );
      Thread t2 = new Thread( new MeinRunnableDSB( sbB, sbA ) );
      t1.start();
      t2.start();
      try { Thread.sleep( 3000 ); } catch( InterruptedException ex ) {/*ok*/}
      System.out.println( t1.getName() + ": isAlive=" + t1.isAlive() + " State=" + t1.getState() );
      System.out.println( t2.getName() + ": isAlive=" + t2.isAlive() + " State=" + t2.getState() );
      System.exit( 0 );
   }
}

class MeinRunnableDSB implements Runnable
{
   StringBuffer sb1, sb2;

   MeinRunnableDSB( StringBuffer sb1, StringBuffer sb2 )
   {
      this.sb1 = sb1;
      this.sb2 = sb2;
   }

   public void run()
   {
      int i = 0;
      while( ++i < 100 ) {
         sb2.append( "42" );
         sb1.insert( sb1.length() / 2, sb2 );
         System.out.println( Thread.currentThread().getName() + ": i=" + i +
               ", sb1=" + sb1.length() + ", sb2=" + sb2.length() );
         try { Thread.sleep( 1 ); } catch( InterruptedException ex ) {/*ok*/}
      }
   }
}

Das Ergebnis kann zum Beispiel so aussehen (eventuell müssen Sie das Programm mehrmals starten):

Thread-0: i=1, sb1=4,  sb2=6
Thread-1: i=1, sb1=6,  sb2=4
Thread-1: i=2, sb1=12, sb2=6
Thread-0: i=2, sb1=20, sb2=14
Thread-1: i=3, sb1=38, sb2=60
Thread-0: i=3, sb1=60, sb2=38
Thread-0: isAlive=true State=BLOCKED
Thread-1: isAlive=true State=BLOCKED

Deadlock während der Klassen-Initialisierung

Auch dieses Beispiel endet im Deadlock, obwohl nirgends dass Schlüsselwort "synchronized" verwendet wird. Es simuliert eine "Lazy Initialization", bei der im parallel laufenden Thread t lediglich das Attribut initialized auf true gesetzt wird. Der Haupt-Thread wartet mit t.join() auf die Beendigung dieses Threads.

public class DeadlockClassInitialisierung
{
   private static boolean initialized = false;

   static {
      Thread t = new Thread( new Runnable() {
         public void run() {
            System.out.println( "'t.run()'-Beginn" );
            initialized = true;
            System.out.println( "'t.run()'-Ende" );
         }
      });
      t.start();
      try {
         System.out.println( "Vor 't.join()'" );
         t.join();
         System.out.println( "Nach 't.join()'" );
      } catch( InterruptedException ex ) {
         throw new AssertionError( ex );
      }
   }

   public static void main( String[] args )
   {
      System.out.println( initialized );
   }
}

Der Deadlock entsteht, weil der Thread t für die Änderung des Attributs initialized auf die fertige Initialisierung der Klasse, die dieses Attribut enthält, wartet. Der Thread t wartet also auf die Beendigung des static-Klasseninitialisierers, der wiederum mit t.join() auf die Beendigung des Threads wartet.

Das Beispiel ist (etwas abgewandelt) dem Buch Java Puzzlers von Joshua Bloch und Neal Gafter entnommen (Puzzle 85). Dort finden Sie eine ausführlichere und genauere Erläuterung.

Programmatische Deadlock-Erkennung

Seit Java 5 gibt es die Möglichkeit, über ThreadMXBean Deadlocks programmatisch aufzuspüren. Allerdings gibt es dabei einige wichtige Details zu beachten:

Die Klasse zur Deadlock-Erkennung wird der Einfachheit halber im Folgenden als Runnable implementiert:

import java.lang.management.*;

public class DeadlockDetector implements Runnable
{
   // Fuer Tests auf 1 Sekunde, sonst besser laengere Perioden:
   private static final long PERIOD_TIME_SECONDS = 1;
   private final ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();

   @Override
   public void run()
   {
      while( true ) {
         long[] ids = ( threadMXBean.isSynchronizerUsageSupported() )
                      ? threadMXBean.findDeadlockedThreads()
                      : threadMXBean.findMonitorDeadlockedThreads();
         if( ids != null && ids.length > 0 ) {
            for( long id : ids ) {
               ThreadInfo threadInfo = threadMXBean.getThreadInfo( id );
               System.out.println( threadInfo );
            }
         }
         try { Thread.sleep( PERIOD_TIME_SECONDS * 1000 ); } catch( InterruptedException ex ) {/*ok*/}
      }
   }
}

Um das Beispiel einfach zu halten, werden im Deadlock-Fall lediglich Meldungen ausgegeben. Dr. Heinz M. Kabutz zeigt im Java Specialists' Newsletter Issue 130, wie dies professionell um Listener erweitert werden kann.

Folgendes Beispiel zeigt, wie die Deadlock-Erkennung mit DeadlockDetector durch Hinzufügen einer Zeile auf das obige DeadlockKonto-Beispiel (und analog auf die anderen Beispiele) angewendet werden kann:

public class DeadlockDetectionKonto
{
   public static void main( String[] args )
   {
      (new Thread( new DeadlockDetector() )).start();

      Konto kontoA = new Konto(), kontoB = new Konto();
      (new Thread( new MeinRunnableKto( kontoA, kontoB ) )).start();
      (new Thread( new MeinRunnableKto( kontoB, kontoA ) )).start();
   }
}

Die Ausgabe kann zum Beispiel so aussehen:

Thread-1: 1
Thread-2: 1
Thread-2: 2
Thread-1: 2
Thread-1: 3
Thread-2: 3
Thread-1: 4
Thread-2: 4
Thread-2: 5
Thread-1: 5
Thread-1: 6
Thread-2: 6
Thread-1: 7
Thread-2: 7
"Thread-2" Id=10 BLOCKED on Konto@530daa owned by "Thread-1" Id=9
"Thread-1" Id=9 BLOCKED on Konto@a62fc3 owned by "Thread-2" Id=10
"Thread-2" Id=10 BLOCKED on Konto@530daa owned by "Thread-1" Id=9
"Thread-1" Id=9 BLOCKED on Konto@a62fc3 owned by "Thread-2" Id=10
...

Deadlock-Fehlalarm

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.*;

public class DeadlockDetectionFehlalarm
{
   public static void main( String[] args )
   {
      (new Thread( new DeadlockDetector() )).start();

      // "Ownable Synchronizer" (ab Java 5):
      final Lock lockA = new ReentrantLock();
      final Lock lockB = new ReentrantLock();
      (new Thread( new MeinRunnableMitJava5Lock( lockA, lockB ) )).start();
      (new MeinRunnableMitJava5Lock( lockB, lockA )).run();

      System.out.println( "Fertig." );
      System.exit( 0 );
   }
}

class MeinRunnableMitJava5Lock implements Runnable
{
   Lock lock1, lock2;

   MeinRunnableMitJava5Lock( Lock lock1, Lock lock2 )
   {
      this.lock1 = lock1;
      this.lock2 = lock2;
   }

   public void run()
   {
      lock1.lock();
      try {
         Thread.sleep( 500 );
         if( lock2.tryLock( 10, TimeUnit.SECONDS ) )
            lock2.unlock();
      } catch( InterruptedException ex ) {
         ex.printStackTrace();
      } finally {
         lock1.unlock();
      }
   }
}

Über Thread.start() wird MeinRunnableMitJava5Lock in einem eigenen neuen Thread gestartet. MeinRunnableMitJava5Lock.run() wird dagegen im main-Thread ausgeführt.

Anders als beim vorherigen Beispiel ist die Verklemmung nur temporär (für die Dauer von lock2.tryLock()) und anschließend beendet sich das Programm korrekt (und meldet "Fertig."). Es muss nicht durch "Strg+C" abgebrochen werden. Statt "BLOCKED" wird hier "TIMED_WAITING" gemeldet. Die Ausgabe kann zum Beispiel so aussehen:

"Thread-1" Id=9 TIMED_WAITING on java.util.concurrent.locks.ReentrantLock$NonfairSync@8813f2 owned by "main" Id=1
"main" Id=1 TIMED_WAITING on java.util.concurrent.locks.ReentrantLock$NonfairSync@1d58aae owned by "Thread-1" Id=9

"Thread-1" Id=9 TIMED_WAITING on java.util.concurrent.locks.ReentrantLock$NonfairSync@8813f2 owned by "main" Id=1
"main" Id=1 TIMED_WAITING on java.util.concurrent.locks.ReentrantLock$NonfairSync@1d58aae owned by "Thread-1" Id=9

...

Fertig.

Deadlock über Datenbankzugriffe

Außer den genannten Software-Deadlock gibt es noch viele weitere Möglichkeiten für Deadlocks, zum Beispiel über gemeinsam genutzte Ressourcen wie Datenbanktabellen.



Immutable

In Bezug auf Multithreading-Sicherheit sind "immutable" (= unveränderbare) Klassen (wie z.B. String, Long, BigInteger) zu bevorzugen, da für sie keine Synchronisation erforderlich ist.

Damit eine Klasse immutable ist, genügt es allerding nicht, die Attribute private zu deklarieren und keine Setter-Methoden anzubieten, sondern es sind weitere Vorkehrungen notwendig, wie folgendes (etwas abgewandeltes) Beispiel von Joshua Bloch zeigt: Bei Verwendung von Period1 kann das Objekt p sowohl durch end.setTime() als auch durch p.getStart().setTime() nachträglich verändert werden.

Anders als Period1 ist Period2 tatsächlich immutable. Dies wird dadurch erreicht, dass sowohl im Konstruktor als auch in den Getter-Methoden nicht Referenzen gespeichert oder übergeben werden, sondern Kopien erzeugt werden. Dabei sollte nicht die Date.clone()-Methode verwendet werden, weil Date nicht als final deklariert ist und somit ein von Date abgeleitetes Objekt übergeben werden könnte, welches zusätzliche unerwünschte Methoden enthalten könnte.

import java.util.Date;

public class ImmutableTest
{
   public static void main( String[] args )
   {
      final Date start = new Date();
      final Date end   = new Date();
      final Period1 p  = new Period1( start, end ); // Period1 durch Period2 ersetzen
      System.out.println( p );

      end.setTime( 1234567890 );
      System.out.println( p );

      p.getStart().setTime( 12345678900L );
      System.out.println( p );
   }
}

final class Period1 // Mutable
{
   private final Date start;
   private final Date end;

   public Period1( Date start, Date end )
   {
      if( start.compareTo( end ) > 0 )
         throw new IllegalArgumentException( start + " after " + end );
      this.start = start;
      this.end   = end;
   }

   public Date getStart() { return start; }
   public Date getEnd()   { return end;   }

   @Override
   public String toString() { return start + " - " + end; }
}

final class Period2 // Immutable
{
   private final Date start;
   private final Date end;

   public Period2( Date start, Date end )
   {
      if( start.compareTo( end ) > 0 )
         throw new IllegalArgumentException( start + " after " + end );
      this.start = new Date( start.getTime() );
      this.end   = new Date( end.getTime() );
   }

   public Date getStart() { return new Date( start.getTime() ); }
   public Date getEnd()   { return new Date( end.getTime() );   }

   @Override
   public String toString() { return start + " - " + end; }
}


Grad der Threadsicherheit

Joshua Bloch unterscheidet zwischen fünf Graden der Threadsicherheit:



Allgemeine Hinweise für sicheres Multithreading

Multithreading-Sicherheit

Auch wenn man selbst keine Threads erzeugt, sind die meisten Java-Programme multithreaded, zum Beispiel weil ein Framework (wie das AWT oder Swing) oder ein Application Server mehrere Threads startet, oder durch die Verwendung von Callbacks, RMI, Timer, ...

Multithreading-Sicherheit bedeutet im Wesentlichen, dass der Zugriff auf veränderbare und von mehreren Threads verwendete Objekte gemanaged wird ("shared mutable data"). Diese Objekte sind oft Java-Objekte, aber können auch ganz andere Objekte sein, zum Beispiel Datenbanktabellen.

Besondere Vorsicht gilt bei statischen Variablen, bei "static final"-Objekten, bei an Threads übergebenen Objekten, bei Collections und bei verzögerter Lazy-Initialisierung.

Zu wenig oder zu viel Synchronisation

Es darf weder zu wenig noch zu viel synchronisiert werden:
- Bei zu wenig Synchronisation kommt es zu schwerwiegenden und schwer zu ermittelnden Fehlern.
- Bei zu viel Synchronisation kommt es zu Performancebeeinträchtigung und eventuell zum Stuck Thread, Deadlock, Livelock oder zur Starvation.

Die synchronisierten Code-Abschnitte sollten so kurz wie möglich sein.

Aus synchronisierten Code-Abschnitten heraus sollten keine fremden Methoden ("alien Methods") aufgerufen werden, deren Thread-Verhalten nicht genau bekannt ist.

Langwierige Methodenaufrufe (z.B. Datenbankabfragen) sollten nach Möglichkeit aus dem Lockblock herausgehalten werden.

Lese- und Schreiboperationen mit demselben Lockobjekt sichern

Es müssen nicht nur Schreiboperationen, sondern auch Leseoperationen eines Objekts synchronisiert werden, und zwar mit demselben Lockobjekt. Sonst kommt es zu Inkonsistenzen, zum Beispiel wegen fehlender Sichtbarkeit.

Zusammenhängende Variablen

Wenn mehrere Variablen/Objekte im Zusammenhang stehen (z.B. Koordinaten), macht es keinen Sinn die Objekte einzeln threadsicher zu machen. Stattdessen können sie als nicht-threadsichere Variablen definiert werden und in allen Zugriffsmethoden müssen sie in gemeinsamen Lockblöcken synchronisiert werden.

static, final

Eine Vermeidung von static-Variablen oder das Hinzufügen des final-Modifizierers genügt nicht für Threadsicherheit, wie die Beispiele "InkonsistenzenOhneStatic" und "FalschSynchronisierteCollection1" zeigen.

volatile, synchronized, lockObject

Die Modifizierer "volatile" und "synchronized" nützen nur wenn sie korrekt eingesetzt werden. Einige Fehler zeigen oben die Kapitel Fehlschlagende Synchronisationsversuche.

Sichtbarkeit zwischen Threads

Die Java Language Specification ("JLS") garantiert nicht, dass der in einem Thread in eine Variable geschriebene Wert für andere Threads sichtbar ist. Die Variable könnte zum Beispiel temporär in dem Thread-spezifischem Cache oder in einem CPU-Register gehalten werden.

Es ist also nicht sicher, dass über den Ausdruck "while( !stopRequested ) { ... }" innerhalb eines Threads durch Setzen von "stopRequested=true" aus einem anderen Thread heraus die while-Schleife gestoppt wird (es könnte sogar sein, dass die Abbruchbedingung vom Optimierer aus der while-Schleife heraus optimiert wird und vorher einmalig ausgeführt wird).

Abhilfe: Bis Java 1.4 mussten die Zugriffe auf "stopRequested" synchronisiert werden (z.B. über dedizierte synchronized Methoden). Ab Java 5 genügt es, "stopRequested" als "volatile" zu markieren.

long und double sind nicht atomic

Die Java Language Specification garantiert, dass das Lesen und Schreiben von Referenzen und Variablen atomar ist, außer für long und double. Unsynchronisierter konkurrierender Zugriff auf long-Variablen kann also dazu führen, dass die oberen und unteren 32 Bit von verschiedenen long-Werten stammen.

Abhilfe: Bis Java 1.4 "synchronized" Zugriffsmethoden, ab Java 5 "AtomicLong".

Operatoren sind nicht atomic

Obwohl nextSerialNumber in den folgenden Zeilen als int atomar definiert wird (und im Beispiel unsinnigerweise zusätzlich als volatile gekennzeichnet ist), ist die folgende generateSerialNumber()-Methode nicht threadsicher, weil der ++-Operator nicht atomar ist:

private static volatile int nextSerialNumber = 0;

public static int generateSerialNumber() { return nextSerialNumber++; }

Abhilfe: Bis Java 1.4 "synchronized" Methoden, ab Java 5 "AtomicLong", zum Beispiel so:

private static final AtomicLong nextSerialNumber = new AtomicLong();

public static long generateSerialNumber() { return nextSerialNumber.getAndIncrement(); }

Snapshot-Liste statt synchronisiertem Iterator über Liste

Statt einen Iterator-Block zu synchronisieren, kann es in bestimmten Fällen günstiger sein, über eine Schnappschussliste zu iterieren. Dadurch kann der synchronisierte Block reduziert werden und es kann eventuell verhindert werden, dass aus dem synchronisierten Block heraus fremde Methoden ("alien Methods") mit unbekanntem Thread-Verhalten aufgerufen werden müssen:

List<E> snapshotList;
synchronized( mutableList ) {
   snapshotList = new ArrayList<E>( mutableList );
}
for( E e : snapshotList ) {
   alienMethod( e );
}

Nicht-threadsichere Collections

Die "alten Collections" (z.B. Vector, Stack, Hashtable) sind threadsicher synchronisiert.

Die ab Java 1.2 "neuen Collections" (z.B. ArrayList, HashMap) sind nicht threadsicher. Über Collections.synchronized...() können weitgehend synchronisierte Wrapper generiert werden, aber die Iteratoren erfordern externe Synchronisation.

Bevorzugen Sie die seit Java 5 im Package java.util.concurrent angebotenen neuen vollständig threadsicheren und performanten Collection-Klassen, zum Beispiel ConcurrentHashMap und CopyOnWriteArrayList.

Andere nicht-threadsichere Klassen

Es gibt viele weitere nicht-threadsichere Klassen. Studieren Sie hierzu die jeweilige API-Javadoc. Benutzen Sie zum Beispiel statt java.text.SimpleDateFormat besser org.apache.commons.lang.time.FastDateFormat.

CountDownLatch und Semaphore statt wait() und notify()

Vermeiden Sie die fehlerträchtigen Methoden "wait()" und "notify()" für "Guarded Blocks". Verwenden Sie stattdessen "CountDownLatch" und "Semaphore".

Thread.sleep() statt yield()

Verlassen Sie sich nicht auf eine Wirkung von "Thread.yield()". Verwenden Sie stattdessen "Thread.sleep()" oder "TimeUnit.MILLISECONDS.sleep()".

ReentrantLock.unlock() in finally

Wenn Sie nicht "intrinsic" Locks, sondern "ReentrantLock" verwenden, müssen Sie nach dem "lock()"-Kommando einen "try"-Block verwenden, damit Sie "unlock()" im "finally"-Block ausführen können. Ansonsten würde der Lock bei Exceptions bestehen bleiben.

Work Queue, Thread Pool und Task-Steuerung

Programmieren Sie Work Queues, Thread Pools und Task-Steuerungen nicht selbst. Verwenden Sie das Executor Framework aus dem java.util.concurrent-Package. Verwenden Sie den ExecutorService und für zeitgesteuerte Tasks den ScheduledThreadPoolExecutor.

FindBugs, JUnit- und Lasttests, ThreadInfo

Lassen Sie Ihre Software von FindBugs überprüfen. FindBugs kann u.a. auch bestimmte Multithreading-Probleme erkennen.

Testen Sie nicht nur die Fachlichkeit, sondern erstellen Sie auch JUnit-Tests zur Überprüfung der Multithreading-Sicherheit.

Bei Server-Anwendungen: Führen Sie ausgiebige Lasttests mit vielen gleichzeitigen simulierten Benutzern durch. Führen Sie diese Lasttests insbesondere direkt nach wiederholten Server-Neustarts durch, um Lazy-Initialisierungsfehler zu finden.

Wenn Sie "Thread contention Monitoring" aktivieren, können Sie einige Multithreading-relevanten Kennwerte über die ThreadInfo der ThreadMXBean per JMX monitoren.

EJB im Java EE Application Server

Für EJBs in Java EE Application Servern gelten besondere "EJB Restrictions" (die allerdings selten alle konsequent eingehalten werden), siehe: http://java.sun.com/blueprints/qanda/ejb_tier/restrictions.html.



Multithread-sichere Lazy-Initialisierung ("on Demand")

"Lazy-Initialisierung" ("Initialisierung on Demand") bedeutet, dass Variablen nicht sofort geladen werden, sondern erst dann, wenn sie benötigt werden.

Normalerweise sollte die unproblematische unverzögerte "normale Initialisierung" bevorzugt werden (sowohl für Instanzvariablen als auch für statische Felder):

// "Normale Initialisierung" (mit oder ohne "static"):
private [static] final MeinAttrTyp meinAttr = berechneMeinenAttributWert();

Lazy-Initialisierung ist problematischer und sollte nur verwendet werden, wenn die Vorteile benötigt werden, zum Beispiel um eine Initialisierungs-Zirkularität zu durchbrechen oder weil die Initialisierung zeitaufwändig berechnet werden muss und nicht immer benötigt wird.

Falls Lazy-Initialisierung realisiert werden soll, erfolgt dies threadsicher am einfachsten über eine synchronized Methode:

private MeinAttrTyp meinAttr;

// "Lazy-Initialisierung" ueber synchronized Methode:
synchronized MeinAttrTyp getMeinAttr()
{
   if( meinAttr == null ) meinAttr = berechneMeinenAttributWert();
   return meinAttr;
}

Für den Fall, dass die Variable sehr oft gelesen wird und die Synchronisationszeit gespart werden soll, stellt Joshua Bloch die beiden folgenden performanteren Varianten vor.

Für statisches Feld: "Lazy Initialization holder Class Idiom"

public class MeineLihcKlasse
{
   // "Lazy Initialization holder Class Idiom" fuer statisches Feld:
   private static class FieldHolder
   {
      static final MeinAttrTyp meinAttr = berechneMeinenAttributWert();
   }
   public static MeinAttrTyp getMeinAttr() { return FieldHolder.meinAttr; }
}

Für Instanzvariable: "Double-checked Locking Idiom" ("DCL") (nur ab Java 5)

public class MeineDclKlasse
{
   private volatile MeinAttrTyp meinAttr;

   // "Double-checked Locking Idiom" fuer Instanzvariable:
   public MeinAttrTyp getMeinAttr() {
      MeinAttrTyp tmp = meinAttr;
      if( tmp == null ) {
         synchronized( this ) {
            tmp = meinAttr;
            if( tmp == null ) {
               meinAttr = tmp = berechneMeinenAttributWert();
            }
         }
      }
      return tmp;
   }
}

Achtung: "Double-checked Locking" hat bis Java 1.4 zu Fehlern geführt, wie unter 'http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html' gezeigt wird.

Erst ab Java 5 mit dem neuen Java Memory Model (siehe JSR 133) und der strikteren Bedeutung des "volatile"-Modifizierers ist das gezeigte Double-checked Locking sicher.



Multithread-sicherer Read-only-Cache ("Memoizer")

Häufig müssen zeitaufwändige Abfragen (z.B. Datenbankabfragen) oder Berechnungen (z.B. mathematische) für wiederholte Lesezugriffe zwischengespeichert werden ("Caching"). Voraussetzung ist, dass dieselben Input-Argumente stets zum selben Ergebnis führen (Idempotenz), so dass die originale Abfrage- bzw. Berechnungsoperation nur einmal ausgeführt werden muss.

Um zu generalisieren wird im Folgenden angenommen, dass die Abfrage- bzw. Berechnungsoperation über das interface Computable<A,V> abstrahiert werden kann, welches die Input-Argumente in der Klasse "A" erhält, das Ergebnis über die Methode "compute" ermittelt und ein Objekt der Klasse "V" returniert.

Eine einfache und übliche Realisierung des Read-only-Caches (auch "Memoizer" genannt) könnte so aussehen (eine Einbettung in eine Anwendung wird weiter unten in MemoizerTest gezeigt):

import java.util.*;

public class SynchronizedMemoizer<A,V> implements Computable<A,V>
{
   private final Map<A,V> cache = new HashMap<A,V>();
   private final Computable<A,V> c;

   public SynchronizedMemoizer( Computable<A,V> c )
   {
      this.c = c;
   }

   public synchronized V compute( A arg ) throws InterruptedException
   {
      V result = cache.get( arg );
      if( result == null ) {
         result = c.compute( arg );
         cache.put( arg, result );
      }
      return result;
   }
}

Wichtig ist der "synchronized"-Modifier, welcher konkurrierende Zugriffe auf die Cache-HashMap synchronisiert.

Dieser Read-only-Cache funktioniert korrekt und ist in vielen Fällen ausreichend. Allerdings hat er gewisse Nachteile:


Falls es egal ist, wenn in Einzelfällen für dieselben Input-Argumente die Berechnung wiederholt ausgeführt wird, können Sie mit einer ConcurrentHashMap den "synchronized"-Modifier vermeiden und, noch wichtiger, eine Beschleunigung durch Parallelität erreichen:

import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentHashMapMemoizer<A,V> implements Computable<A,V>
{
   private final ConcurrentHashMap<A,V> cache = new ConcurrentHashMap<A,V>();
   private final Computable<A,V> c;

   public ConcurrentHashMapMemoizer( Computable<A,V> c )
   {
      this.c = c;
   }

   public V compute( A arg ) throws InterruptedException
   {
      V result = cache.get( arg );
      if( result == null ) {
         result = c.compute( arg );
         cache.putIfAbsent( arg, result );
      }
      return result;
   }
}

Brian Goetz und Tim Peierls zeigen, wie ein "Memoizer" aussehen kann, der alle genannten Nachteile vermeidet:

import java.util.concurrent.*;

/**
 * Nach einer Vorlage von:
 * @author Brian Goetz und Tim Peierls
 */
public class Memoizer<A,V> implements Computable<A,V>
{
   private final ConcurrentMap<A,Future<V>> cache = new ConcurrentHashMap<A,Future<V>>();
   private final Computable<A,V> c;

   public Memoizer( Computable<A,V> c )
   {
      this.c = c;
   }

   public V compute( final A arg )
   {
      while( true ) {
         Future<V> f = cache.get( arg );
         if( f == null ) {
            Callable<V> eval = new Callable<V>() {
               public V call() throws InterruptedException {
                  return c.compute( arg );
               }
            };
            FutureTask<V> ft = new FutureTask<V>( eval );
            f = cache.putIfAbsent( arg, ft );
            if( f == null ) {
               f = ft;
               ft.run();
            }
         }
         try {
            return f.get();
         } catch( CancellationException ex ) {
            cache.remove( arg, f );
         } catch( Exception ex ) {
            cache.remove( arg, f );
            throw new RuntimeException( ex.getCause() );
         }
      }
   }
}

interface Computable<A,V>
{
   V compute( A arg ) throws InterruptedException;
}

Das folgende Testprogramm ermöglicht beispielhafte Performancemessungen zu den vier Varianten a) ohne Cache, b) mit "SynchronizedMemoizer", c) mit "ConcurrentHashMapMemoizer" und d) mit "optimiertem Memoizer" (die Ergebnisse variieren abhängig von den Parametern):

import java.util.Random;
import java.util.concurrent.TimeUnit;

public class MemoizerTest
{
   public static void main( String[] args )
   {
      Computable<String,Double> c = new LangwierigeBerechnung();
      starteThreads( "Ohne Cache",                 c );
      starteThreads( "Synchronized Memoizer",      new SynchronizedMemoizer<String,Double>( c ) );
      starteThreads( "ConcurrentHashMap Memoizer", new ConcurrentHashMapMemoizer<String,Double>( c ) );
      starteThreads( "Optimierter Memoizer",       new Memoizer<String,Double>( c ) );
   }

   static void starteThreads( String s, Computable<String,Double> c )
   {
      for( int i=0; i<5; i++ ) { // eventuell die Anzahl der Threads aendern
         new Thread( (new MemoizerRunnable( s, c )) ).start();
      }
   }
}

class LangwierigeBerechnung implements Computable<String,Double>
{
   public Double compute( String arg ) throws InterruptedException
   {
      TimeUnit.MILLISECONDS.sleep( 10 ); // simuliert z.B. Datenbankabfrage
      return new Double( arg );
   }
}

class MemoizerRunnable implements Runnable
{
   static final int[]        ARGARR = { 1, 42, 7, 1629, 4711, 1984, 999, 2, 3, 11 };
   String                    s;
   Computable<String,Double> c;

   MemoizerRunnable( String s, Computable<String,Double> c )
   {
      this.s = s;
      this.c = c;
   }

   public void run()
   {
      long startZeit = System.nanoTime();
      int j = (new Random()).nextInt( 10 );
      for( int i=0; i<20; i++ ) { // eventuell die Lese-Anzahl aendern
         try {
            TimeUnit.MILLISECONDS.sleep( 1 );
            c.compute( "" + ARGARR[(i + j) % ARGARR.length] );
         } catch( InterruptedException ex ) {
            throw new RuntimeException( ex );
         }
      }
      System.out.println( s + ": " + ((System.nanoTime() - startZeit) / 1000 / 1000.) + " ms" );
   }
}

Die Ausgabe könnte zum Beispiel so aussehen (gekürzt):

Optimierter Memoizer:        60 ms
ConcurrentHashMap Memoizer:  60 ms
Synchronized Memoizer:      115 ms
Ohne Cache:                 225 ms

Die "LangwierigeBerechnung" ist auf 10 Millisekunden eingestellt und der "MemoizerRunnable" fügt pro Schleife eine weitere Millisekunde hinzu (damit sich die Threads besser abwechseln). Deshalb benötigt die Variante ohne Cache für 20 Berechnungen etwas mehr als 20 * 11 = 220 Millisekunden. Der "Synchronized Memoizer" muss jede der 10 verschiedenen Abfragen nur einmal berechnen lassen, weitere Abfragen werden aus dem Cache bedient. Er benötigt deshalb etwas mehr als 10 * 11 = 110 Millisekunden. Der "ConcurrentHashMap Memoizer" und der "Optimierte Memoizer" bieten erhöhte Parallelität und benötigen oft nur etwa die Hälfte der Zeit.

Variieren Sie folgende Parameter, um die Effekte zu beobachten: Zeitdauer in der Klasse "LangwierigeBerechnung", Anzahl der Threads in "starteThreads()", Anzahl der Anfragen in "run()" und die Anzahl der verschiedenen Argumente in "ARGARR".


Der "Synchronized Memoizer" hat zwar die schlechteste Performance, aber einen Vorteil: Falls Sie die Cache-Größe begrenzen müssen und hierzu einen LRU-Cache implementieren wollen, können Sie das sehr einfach, indem Sie in SynchronizedMemoizer den Ausdruck "new HashMap<A,V>()" ersetzen durch "new LruLinkedHashMap<A,V>( 20 )" und folgende Klasse hinzufügen:

import java.util.*;

// LRU-Cache ("Least recently used")
public final class LruLinkedHashMap<A,V> extends LinkedHashMap<A,V>
{
   private final int maxEntries;

   public LruLinkedHashMap( final int maxEntries )
   {
      super( 16, 0.75f, true );
      this.maxEntries = maxEntries;
   }

   @Override
   protected boolean removeEldestEntry( Map.Entry<A,V> lru )
   {
      return size() > maxEntries;
   }
}


Parallele Abarbeitung vieler Tasks

Mit dem ExecutorService ist es relativ einfach, viele Aufgaben (Tasks) parallel abzuarbeiten, und dabei eine vorgegebene Anzahl von Threads optimal auszunutzen. Der ExecutorService bietet eine interne Queue, so dass Tausende von Tasks übergeben werden können, die dann optimal auf beispielsweise 8 Threads verteilt werden.

Voraussetzung ist, dass Sie Ihre Tasks unabhängig voneinander und in beliebiger Reihenfolge ausführen können.

Die folgende Java-Klasse TasksConcurrentExecutor bietet sich an, wenn Sie eine Liste der auszuführenden Tasks übergeben wollen und eine Liste der Ergebnisse erhalten wollen. Um TasksConcurrentExecutor verwenden zu können, erstellen Sie ein das Interface Computable implementierendes Objekt, worin Sie die auszuführende Arbeitsmethode compute() definieren. Die Tasks definieren Sie über die Klasse TaskDescription, der Sie das Computable-Objekt und das jeweilige Input-Argument-Objekt übergeben, und in der Sie nach der Ausführung auch das jeweilige Result-Objekt finden. Diese Tasks sammeln Sie in einer Liste und starten die parallele Ausführung über:

(new TasksConcurrentExecutor<InputType,ResultType>( taskList, threadCount, timeoutSeconds )).run();

Ein konkretes Beispiel hierzu finden Sie in der nachfolgend vorgestellten Testklasse TasksConcurrentExecutorTest.

Hier der TasksConcurrentExecutor:

import java.util.List;
import java.util.concurrent.*;

/**
 * Parallele Abarbeitung vieler Tasks.
 * Aufruf ueber:
 * (new TasksConcurrentExecutor<InputType,ResultType>( taskList, threadCount, timeoutSeconds )).run();
 * Ergebnisse finden Sie in den Result-Objekten in den Tasks in der taskList.
 */
public class TasksConcurrentExecutor<A,R> implements Runnable
{
   private List<TaskDescription<A,R>> taskList;
   private int                        threadCount;
   private long                       timeoutSeconds;

   public TasksConcurrentExecutor( List<TaskDescription<A,R>> taskList, int threadCount, long timeoutSeconds )
   {
      this.taskList       = taskList;
      this.threadCount    = threadCount;
      this.timeoutSeconds = timeoutSeconds;
   }

   @Override public void run()
   {
      ExecutorService threadPool = Executors.newFixedThreadPool( threadCount );
      for( TaskDescription<A,R> td : taskList ) {
         threadPool.execute( new TaskRunnable<A,R>( td ) );
      }
      threadPool.shutdown();
      try {
         if( !threadPool.awaitTermination( timeoutSeconds, TimeUnit.SECONDS ) ) {
            threadPool.shutdownNow();
            throw new RuntimeException( "Timeout (" + timeoutSeconds + " Sekunden) ueberschritten." );
         }
      } catch( InterruptedException ex ) {
         throw new RuntimeException( ex );
      }
   }
}

class TaskRunnable<A,R> implements Runnable
{
   private TaskDescription<A,R> td;

   public TaskRunnable( TaskDescription<A,R> td )
   {
      this.td = td;
   }

   @Override public void run()
   {
      // System.out.println( Thread.currentThread().getName() + ": arg=" + td.arg );
      td.result = td.computable.compute( td.arg );
      // System.out.println( Thread.currentThread().getName() + ": result=" + td.result );
   }
}

/** Task-Beschreibung (Arbeitsmethode, Input-Argument und Ergebnis) */
class TaskDescription<A,R>
{
   Computable<A,R> computable;
   A               arg;
   R               result;

   public TaskDescription( Computable<A,R> computable, A arg )
   {
      this.computable = computable;
      this.arg        = arg;
   }
}

/** Interface fuer die Objekte mit der Task-Arbeitsmethode */
interface Computable<A,R>
{
   R compute( A arg );
}

Mit folgender simplen Test-Klasse TasksConcurrentExecutorTest.java können Sie obigen TasksConcurrentExecutor testen (vielleicht ersetzen Sie die compute()-Methode durch etwas sinnvolleres):

import java.util.*;

/** Test fuer @see TasksConcurrentExecutor */
public class TasksConcurrentExecutorTest
{
   public static void main( String[] args )
   {
      // Die Beispiel-Tasks haben ein Integer-Input-Argument und ein String-Result:
      Computable<Integer,String> testComputable = new Computable<Integer,String>() {
         @Override public String compute( Integer arg ) {
            // Hier die auszufuehrende Task-Arbeitsmethode implementieren ...:
            try { Thread.sleep( arg.intValue() * 200 ); } catch( InterruptedException ex ) {/*ok*/}
            return "" + arg;
         }
      };
      int threadCount = ( args.length > 0 ) ? Integer.parseInt( args[0] ) :  8;
      int taskCount   = ( args.length > 1 ) ? Integer.parseInt( args[1] ) : 20;
      List<TaskDescription<Integer,String>> taskList = new ArrayList<TaskDescription<Integer,String>>();
      // Erzeuge die auszufuehrenden Task-Objekte und sammle in der taskList:
      for( int i = 0; i < taskCount; i++ )
         taskList.add( new TaskDescription<Integer,String>( testComputable, new Integer( (new Random()).nextInt( 10 ) ) ) );
      System.out.println( "Fuehre " + taskList.size() + " Tasks in " + threadCount + " Threads aus:" );
      long startZeit = System.nanoTime();

      // Fuehre die Tasks parallelisiert aus:
      (new TasksConcurrentExecutor<Integer,String>( taskList, threadCount, 3600 )).run();

      double dauer = ((System.nanoTime() - startZeit) / 1000000 / 1000.);
      for( TaskDescription<Integer,String> td : taskList ) {
         System.out.println( Thread.currentThread().getName() + ": arg=" + td.arg + ", result=" + td.result );
      }
      System.out.println( "Dauer: " + dauer + " s" );
   }
}

Führen Sie im Kommandozeilenfenster für beispielsweise 20 Tasks aus:

javac *.java  
java TasksConcurrentExecutorTest 1 20 --> ca. 16 Sekunden bei 1 Thread
java TasksConcurrentExecutorTest 8 20 --> ca.   3 Sekunden bei 8 Threads

Während bei der im Beispiel verwendeten simplen Thread.sleep()-Task-Arbeitsmethode viele Threads kein Problem sind, sollte bei reellen Task-Arbeitsmethoden die Zahl der Threads die Anzahl der CPU-Kerne nur dann überschreiten, wenn die Task-Arbeitsmethoden teilweise Wartezeiten enthalten (z.B. warten auf Datei- oder Datenbankzugriffe). Verfolgen Sie die Auslastung der CPU-Kerne mit JConsole, um die optimale Thread-Anzahl zu finden.



Parallele Abarbeitung vieler Tasks mit dem Stream-Api ab Java 8

Seit Java 8 gibt es weitere stark vereinfachende Möglichkeiten zur parallelen Verarbeitung. Sie hierzu: Funktionale Programmierung, Lambda-Ausdrücke, Stream-API, Bulk Operations on Collections, Filter-Map-Reduce.



Singleton

Ein Singleton ist eine Klasse, von der nicht mehrere Instanzen gleichzeitig erzeugt werden können. Dies kann zum Beispiel für einen Cache genutzt werden.

Bezüglich Singletons im EJB-Umfeld beachten Sie bitte: http://www.roseindia.net/javatutorials/J2EE_singleton_pattern.shtml und http://java.sun.com/developer/technicalArticles/Programming/singletons.

Singleton bis Java 1.4

Bis Java 1.4 war der übliche Weg zur Realisierung eines Singletons, die Klasse mit einem privaten Konstruktor und einem öffentlichen Member auszustatten.

Beispiel für ein Singleton mit einem "public static final INSTANCE"-Feld:

public class MeinSingleton1
{
   public static final MeinSingleton1 INSTANCE = new MeinSingleton1();

   private MeinSingleton1() { /* ... */ }
}

Beispiel für ein Singleton mit einer "public static getInstance()"-Factory-Methode:

public class MeinSingleton2
{
   private static final MeinSingleton2 INSTANCE = new MeinSingleton2();

   private MeinSingleton2() { /* ... */ }

   public static MeinSingleton2 getInstance() { return INSTANCE; }
}

Bitte beachten Sie, dass bei diesem einfachen Ansatz die Singularität nur sichergestellt ist, solange Sie die Klasse nicht als "serializable" markieren. Andernfalls müssten Sie alle Attribute als "transient" kennzeichnen und eine geeignete "readResolve()"-Methode ergänzen.

Singleton ab Java 5

Ab Java 5 gibt es eine bessere Alternative: Ein "enum" mit einem Element:

public enum MeinSingleton3
{
   INSTANCE;

   public void zusaetzlicheMethoden() { /* ... */ }
}

Diese Singleton-Version ist serialisierbar und robust gegen Reflection-Attaken.

Enum-Typen und darin deklarierte Attribute und Methoden sind implizit immer statisch.

Singleton mit Initialisierungsfehler im final-Attribut

Das folgende Beispiel zeigt, dass als final deklarierte Attribute ausgelesen werden können, bevor sie korrekt initialisiert wurden. Das final-Attribut y wird mit dem Wert "0" statt mit dem aktuellen Jahr ausgelesen, weil die Initialisierungsreihenfolge falsch gewählt wurde (YEAR muss vor INSTANCE initialisiert werden, damit YEAR im Konstruktoraufruf "INSTANCE = new SingletonInitFehler()" gesetzt ist).

import java.util.Calendar;

public class SingletonInitFehler
{
   public  static final SingletonInitFehler INSTANCE = new SingletonInitFehler();
   private static final int YEAR = Calendar.getInstance().get( Calendar.YEAR );
   private final int y;

   private SingletonInitFehler()
   {
      y = YEAR;
   }

   public static void main( String[] args )
   {
      System.out.println( INSTANCE.y );
   }
}



Weitere Themen: andere TechDocs | Best Practices
© 2008-2009 Torsten Horn, Aachen