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:
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).
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*/} } } }
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*/} } } }
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*/} } } }
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:
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.
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*/} } } }
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*/} } } } }
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*/} } } } }
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, dass die Zählgrenze 1000 nicht überschritten wird.
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>() ); @Override 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*/} } } }
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; } @Override public void run() { // Fehler: Size ist manchmal kleiner und manchmal groesser als 1000: System.out.println( Thread.currentThread().getName() + ": meinAttr.size = " + getMeinAttr().size() ); } }
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; } @Override 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.
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; } @Override 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".
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; } @Override 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.
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; } @Override 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
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 { protected static boolean initialized = false; static { Thread t = new Thread( new Runnable() { @Override 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.
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 ...
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; } @Override 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.
Außer den genannten Software-Deadlock gibt es noch viele weitere Möglichkeiten für Deadlocks, zum Beispiel über gemeinsam genutzte Ressourcen wie Datenbanktabellen.
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; } }
Joshua Bloch unterscheidet zwischen fünf Graden der Threadsicherheit:
Immutable (unveränderbar)
Für immutable Klassen ist keine Synchronisation erforderlich.
Beispiele: String, Long, BigInteger.
Bedingungslos threadsicher
Diese Klassen beinhalten vollständige interne Synchronisationsmechanismen.
Beispiele: ConcurrentHashMap, CopyOnWriteArrayList.
Bedingt threadsicher
Bei diesen Klassen sind einige Methoden threadsicher, aber andere nicht.
Beispiele: Die von Collections.synchronized...() returnierten weitgehend synchronisierten Wrapper, deren Iteratoren allerdings externe Synchronisation erfordern.
Nicht threadsicher
Im Multithreading-Betrieb ist externe Synchronisation erforderlich.
Beispiele: ArrayList, HashMap.
Nicht threadsicherbare Klassen
Threadsicherheit kann nicht hergestellt werden (z.B. wegen interner statischer Variablen).
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.
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.
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.
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.
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.
Die Modifizierer "volatile" und "synchronized" nützen nur wenn sie korrekt eingesetzt werden. Einige Fehler zeigen oben die Kapitel Fehlschlagende Synchronisationsversuche.
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.
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".
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(); }
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 );
}
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.
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.
Vermeiden Sie die fehlerträchtigen Methoden "wait()" und "notify()" für "Guarded Blocks". Verwenden Sie stattdessen "CountDownLatch" und "Semaphore".
Verlassen Sie sich nicht auf eine Wirkung von "Thread.yield()". Verwenden Sie stattdessen "Thread.sleep()" oder "TimeUnit.MILLISECONDS.sleep()".
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.
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. Beachten Sie den Unterschied zwischen scheduleAtFixedRate() und scheduleWithFixedDelay(). Beachten Sie, dass Sie Exceptions, die von dem ausführenden Runnable geworfen werden, unbedingt im Runnable behandeln müssen, weil sonst die Ausführung des Scheduled-Executors beendet wird. Falls Sie cron-ähnliche Ausdrücke verwenden wollen, sehen Sie sich den EJB Timer Service und die @Schedule-Annotation an.
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.
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.
"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.
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; } }
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.
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; } @Override 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; } @Override 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>>(); protected final Computable<A,V> c; public Memoizer( Computable<A,V> c ) { this.c = c; } @Override public V compute( final A arg ) { while( true ) { Future<V> f = cache.get( arg ); if( f == null ) { Callable<V> eval = new Callable<V>() { @Override 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> { @Override 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; } @Override 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 static final long serialVersionUID = 1L; 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; } }
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 )).start();
Ein konkretes Beispiel hierzu finden Sie in der nachfolgend vorgestellten Testklasse TasksConcurrentExecutorTest.
Hier die Klasse zur parallele Abarbeitung vieler Tasks: TasksConcurrentExecutor.java
import java.util.List; import java.util.concurrent.*; /** * Parallele Abarbeitung vieler Tasks. * Aufruf ueber: * (new TasksConcurrentExecutor<InputType,ResultType>( taskList, threadCount, timeoutSeconds )).start(); * Die taskList muss die TaskDescription-Objekte enthalten. * Die Ergebnisse befinden sich anschliessend in den Result-Objekten in den Tasks in der taskList. */ public class TasksConcurrentExecutor<A,R> { 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; } public void start() { ExecutorService threadPool = Executors.newFixedThreadPool( threadCount ); for( TaskDescription<A,R> td : taskList ) { threadPool.execute( new TaskRunnable( 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 ); } } public class TaskRunnable implements Runnable { 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 ); } } }
Die Task-Beschreibungs-Klasse: TaskDescription.java
/** Task-Beschreibung (Arbeitsmethode, Input-Argument und Ergebnis) */ public class TaskDescription<A,R> { Computable<A,R> computable; public A arg; public R result; public TaskDescription( Computable<A,R> computable, A arg ) { this.computable = computable; this.arg = arg; } /** Interface fuer die Objekte mit der Task-Arbeitsmethode */ public interface Computable<A,R> { R compute( A arg ); } }
Mit folgender Test-Klasse TasksConcurrentExecutorTest.java können Sie obigen TasksConcurrentExecutor testen (vielleicht ersetzen Sie die compute()-Methode durch etwas sinnvolleres):
import java.util.*; import java.util.concurrent.TimeUnit; import org.junit.*; /** Test fuer @see TasksConcurrentExecutor */ public class TasksConcurrentExecutorTest { /** Teste die Parallelitaet */ @Test public void testTasksConcurrentExecutorParallel() { testTasksConcurrentExecutorParallelIntern( 1, 20, 340 ); testTasksConcurrentExecutorParallelIntern( 8, 20, 60 ); } private static void testTasksConcurrentExecutorParallelIntern( int threadCount, int taskCount, int maxDauerMs ) { // Die Beispiel-Tasks haben ein Integer-Input-Argument und ein String-Result: TaskDescription.Computable<Integer,String> testComputable = new TaskDescription.Computable<Integer,String>() { @Override public String compute( Integer arg ) { // Hier die auszufuehrende Task-Arbeitsmethode implementieren ...: try { TimeUnit.MILLISECONDS.sleep( arg.intValue() ); } catch( InterruptedException ex ) { /* ok */ } return "" + arg; } }; 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 ) ) ) ); // Fuehre die Tasks parallelisiert aus: long startZeit = System.nanoTime(); (new TasksConcurrentExecutor<Integer,String>( taskList, threadCount, 1 )).start(); long dauerMs = ((System.nanoTime() - startZeit) / 1000000); Assert.assertTrue( taskList.size() + " Tasks in " + threadCount + " Threads: Dauer ist " + dauerMs + " ms, " + "erlaubt sind maximal " + maxDauerMs + " ms.", dauerMs <= maxDauerMs ); } /** Teste die Timeout-Funktionalitaet */ @Test public void testTasksConcurrentExecutorTimeout() { TaskDescription.Computable<Integer,String> testComputable = new TaskDescription.Computable<Integer,String>() { @Override public String compute( Integer arg ) { try { TimeUnit.MILLISECONDS.sleep( arg.intValue() ); } catch( InterruptedException ex ) { /* ok */ } return "" + arg; } }; List<TaskDescription<Integer,String>> taskList = new ArrayList<TaskDescription<Integer,String>>(); // Test mit 100 Millisekunden (kleiner als Timeout von 1 Sekunde): TaskDescription<Integer,String> td = new TaskDescription<Integer,String>( testComputable, Integer.valueOf( 100 ) ); taskList.add( td ); (new TasksConcurrentExecutor<Integer,String>( taskList, 1, 1 )).start(); Assert.assertEquals( "100", td.result ); // Test mit 1100 Millisekunden (groesser als Timeout von 1 Sekunde): taskList.clear(); taskList.add( new TaskDescription<Integer,String>( testComputable, Integer.valueOf( 1100 ) ) ); try { (new TasksConcurrentExecutor<Integer,String>( taskList, 1, 1 )).start(); Assert.fail( "Nach Timeout-Ueberschreitung muss Exception geworfen werden." ); } catch( RuntimeException ex ) { Assert.assertEquals( "Timeout (1 Sekunden) ueberschritten.", ex.getMessage() ); } } }
Führen Sie den JUnit-Test entweder in Eclipse aus, oder downloaden Sie junit-4.12.jar und hamcrest-core-1.3.jar, und führen Sie aus:
javac -cp .;junit-4.12.jar *.java
java -cp .;junit-4.12.jar;hamcrest-core-1.3.jar org.junit.runner.JUnitCore TasksConcurrentExecutorTest
Während bei der im Beispiel verwendeten simplen TimeUnit.MILLISECONDS.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.
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.
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.
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.
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.
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 ); } }