info
date: 2011-08-10 20:26:54
tags: Java programming
category: Java
Created by: Stephan Bösebeck
logged in
ADMIN
no english version available yet
Da mir das jetzt schon so oft passiert ist, und ich immer wieder wieder solche Probleme lösen muss, möchte ich hier mal kurz ein paar Tipps zum Thema Multithreadding in Java und den damit verbundenen Problemen kommen.
Allgemeines
Worum geht es eigentlich und warum gibt’s da Probleme? Multithreadding ist eine Programmiertechnik, die es möglich macht, dass in einem Programm mehrere Dinge (mindestens zwei) gleichzeitig passieren bzw. ausgeführt werden. Im Zuge der Desktopanwendungen nutzt jeder Anwendungen, die mehrere Threads nutzen, denn meistens ist die Oberfläche ein Thread, und die Anwendung "selbst" mindestens ein anderer. Das ist ja an sich kein Problem sondern eher wünschenswert... Aber es bringt auch Probleme mit sich... Schauen wir uns das mal am Beispiel von Java etwas genauer an.
Das Problem ist, dass mehrere Threads quasi gleichzeitig auf einen gemeinsamen Speicherbereich bzw. eine gemeinsame Variable zugreifen. Das kann unproblematisch sein, wenn es sich um reinen Lesezugriff handelt. Sobald einer der beteiligten Threads jedoch schreibt, können seltsame Dinge passierten.
Klassenbibliothek und Externa
Java wird gerne in EJB oder allgemeiner J2EE-Umgebungen eingesetzt und dort muss das eigene Programm nicht selten mit vielen gleichzeitigen Zugriffen (concurrent usern) umgehen können. Dummerweise wird das bei der Programmierung oft einfach "vergessen" bzw. nicht beachtet. Der beliebteste Fehler ist vermutlich, dass Teile aus der Klassenbibliothek oder aus Bibliotheken von Drittanbietern verwendet werden, die nicht threadsicher (threadsafe) sind. Gerade mit JDK1.5 wurden einige neue und vermeintlich "bessere" Klassen weingeführt, wie zum Beispiel die der StringBuilder für den StringBuffer.
Nahezu der einzige Unterschied zwischen StringBuilder und StringBuffer ist, dass der StringBuilder nicht threadsafe ist, der StringBuffer dagegen schon. Es ist also nicht ratsam, in J2EE-Umgebungen "blind" überall den StringBuilder einzusetzen, weil Java sagt, der ist flotter. Ist er auch, allerdings kann das zu sehr seltsamen Effekten bei der Requestberarbeitung führen.
Weitere Beispiele, die evtl. noch aufgelistet werden sollten sind ArrayList (nicht threadsafe) und Vector (threadsafe), HashMap (nicht threadsafe) und Hashtable (threadsafe). Klar... man sollte nun aber auch nicht blind die threadsafen Varianten verwenden, sondern sich einfach klar darüber sein, was man tut und was man in welcher situation braucht.
Singletons und deren Probleme
Schlimmer wird es noch, wenn man z.B. in einem Container eingebettet ist wie z.B. einer Servletengine oder eben einem EJB-Application-Server. Dann werden die eigenen Klassen evtl. von mehreren Threads gleichzeitig aufgerufen, selbst wenn man sich selbst da keine Gehangen darüber gemacht hat. Insbesondere, wenn man irgendwelche static-Variablen verwendet (auch schreibend) kann das sehr nach hinten los gehen. So wird eigentlich fast immer, das Singleton Pattern falsch implementiert. Hier eine Implementierung, die man auch oft liest und die in Single-threadded Environments sicherlich ok ist:
[codesyntax lang="java" lines="fancy" lines_start="1"]
public class Singleton { private static instance; private Singleton() { //initialisierung } public static Singleton get() { if (instance==null) { instance=new Singleton(); } return instance; } }
[/codesyntax]
Ich höre euch quasi schon sagen: "Was soll denn daran falsch sein, so mach ich es doch auch immer..."... Naja... wo fangen wir an.... (String genommen ist das ja auch eine Factory ;-))
Gehen wir mal davon aus, dass dieser Singleton in einer multithreadded Umgebung verwendet wird. Wenn nun mehrere Thread gleichzeitig auf get() zugreifen, kann es vorkommen, dass der Konstruktor 2x aufgerufen wird, was evtl. ja fatal ist. (ich weiß, die meisten nutzen Singleton, nicht weil das Objekt nur einmal instanziiert werden darf, sondern um einfach darauf zugreifen zu können.) Wenn im Konstruktor nun Dinge passieren, die nur ein Mal vorkommen dürfen (Files öffenen, Sockets öffnen etc), dann knallst... es ist nicht gesagt, ob der Singleton dann richtig instanziiert ist.
Gleiches gilt, wenn in dem Constructor eine Exception geworfen wird, dann ist gar nicht klar, was passieren wird und ob/welche Instanz man dann bekommt, wenn man get() aufruft.
eine "Verbesserung" wäre die Instanz gleich zu initialisieren:
[codesyntax lang="java" lines="fancy" lines_start="1"]
public class Singleton { private static instance=new Singleton(); private Singleton() { //initialisierung } public static Singleton get() { return instance; } }
[/codesyntax]
Das ist zwar schon besser, aber nicht wirklich gut. Denn: der Construktor des Singleton wird vom ClassLoader aufgerufen, was zunächst mal gut klingt, jedoch gleich wieder relativiert wird, wenn man bedenkt, dass es durchaus mehr als einen ClassLoader in einer Application (insbesondere in J2EE-Containern oder ähnlichem) geben kann. Dann kann es wiederum vorkommen, dass mehrere Instanzen des Singleton erstellt werden. Siehe dazu auch: Programming Singletons (Sun). Evtl. erinnert man sich dann ja an das Schlüsselwörtchen synchronized:
[codesyntax lang="java" lines="fancy" lines_start="1"]
public class Singleton { private static instance=null; private Singleton() { //initialisierung } public synchronized static Singleton get() { if (instance==null) { instance=new Singleton(); } return instance; } }
[/codesyntax]
An sich hätte man damit alle Probleme umgangen, jedoch ist es nicht wirklich wünschenswert, Singletons so zu implementieren. Denn das synchronized sorgt dafür, dass immer nur ein Thread auf get() zugreifen kann. Damit hat man sich einen ziemlichen Flaschenhals gebaut, der die Performance stark negativ beeinflussen kann.
Eine wesentlich bessere Lösung ist folgende:
[codesyntax lang="java" lines="fancy" lines_start="1"]
public class Singleton { private static instance=null; private Singleton() { try { //initialisierung } catch (Throwable e) {} //geeignete Fehlerbehandlung!!!!!!! } public static Singleton get() { if (instance==null) { synchronized(Singleton.class) { if (instance==null) { instance=new Singleton(); } } } return instance; } }
[/codesyntax]
Damit ist sichergestellt, dass man in den Synchronized-Block nur dann kommt, wenn wirklich eine neue Instanz erstellt werden muss. Damit leidet die Performance nicht so erheblich wie im Beispiel vorher. NUR so hat man eine Singleton-Implementierung, die wirklich threadsafe ist. (und ich bin mir selbst da nicht zu 10000% sicher...;-)
Static im Allgmeinen
Grundsätzlich sollte man es sich verkneifen in einer multithreadded Umgebung irgendwo das Wörtchen "static" zu verwenden, es sei denn, man ist sich seiner Sache sehr sicher. Die falsche Verwendung von Singletons ist nur ein Problem. Häufig werden auch Datenstrukturen statisch abgelegt um "einfacher" darauf zugreifen zu können. Das birgt natürlich Gefahren. Beispiel:
[codesyntax lang="java" lines="fancy" lines_start="1"]
public class Servlet extends HttpServlet { private static MyLogger l=new MyLogger(); ..... public void get(HttpServletRequest req, HttpServletResponse resp) { l.setSessionId(req.getSession(true).getId()); //do something l.setSessionId(null); //request finished! } }
[/codesyntax]
Aus falschem Ehrgeiz wurde hier wohl versucht, den Speicherbedarf zu reduzieren. Die Idee war es, die SessionIDs mitzuprotokollieren um bei den Logausgaben die Tätigkeiten eines Users/Browsers verfolgen zu können. Das ging aber gehörig schief, denn: Ein Servlet wird (normalerweise) nur ein Mal instanziiert aber von mehreren Thread (je nach Konfiguration des Containers) verwendet. Das führte dazu, dass man die Requests in keiner Weise verfolgen konnte, da sehr häufig mitten in den Log ausgaben (null) ausgegeben wurde... Weil Thread 1 schon in der Bearbeitung recht weit fortgeschritten ist, und Thread 2 gerade beginnt doGet() aufzurufen.
Problem mit Member-Variablen
Wenn man sich die Ausführungen vom letzten Beispiel noch mal ansieht, dann sollte klar sein, dass der selbe Fehler auch auftritt, selbst wenn de Variable nicht static ist! Der Grund: eine Instanz der Klasse "Servlet" wird von verschiedenen Threads verwendet. Somit existiert auch die Variable l (MyLogger) nur ein Mal, selbst wenn sie nicht static ist! Das kann man nur umgehen, indem man lokale Variablen erstellt, (sofern das nicht zu anderen Problemen führt - Performance z.B.).
Dummerweise erkennt man nicht wirklich oft, ob eine Klasse bzw. deren Instanz von "Außen" von mehreren Thread verwendet wird, oder nicht. Da hilft nur, RTFM!
Aber sowas kann man sich auch selbst zaubern, insbesondere wenn man seine Elemente in Datenstrukturen packt, die evtl. von verschiedenen Klassen/Threads verwendet werden wollen. Die Beispiele dafür kann man sich wohl sehr einfach vorstellen, jedoch ist es häufig nicht so einfach zu erkennen, selbst im eigene Code nicht. Es bleibt wichtig, sich immer klar darüber zu sein, was die threads in der eignen Anwendung tun und wer, wie und mit wie vielen Threads auf meine Klassen zugreift.
"Synchronized ist doch die Lösung für alle Multithread-Probleme"
Naja, das wäre ein wenig zu einfach. Mit synchronized kann man sicherlich einige Probleme lösen, allerdings könnte das sehr gut sein, dass man sich damit mehr Probleme einhandelt, als einem lieb ist. Die Performance leidet stark unter der Verwendung von synchronized, da diese Blöcke nur von einem Thread verwendet werden können. Besonders schlimm wird es, wenn man nicht das richtige Objekt zur Synchronisierung verwendet:
[codesyntax lang="java" lines="fancy" lines_start="1"]
public void doSomething() {
synchronized(Object.class) {
//do Something weird
}
}
[/codesyntax]
Hier synchronisiert man gegen das Class Objekt von der Klasse Object, weiter oben in der Hierarchie geht gar nicht... wenn man das an verschiedenen Stellen der Anwendung macht, kann jeweils nur ein Thread auf einen dieser Blöcke zugreifen, selbst wenn sie auf völlig unterschiedliche Daten zugreifen und gerne auch parallel laufen dürften. Es ist besser, auf das "kritische" Objekt zu synchronisieren, dann würden sogar verschiedene Instanzen dieser Klasse parallel laufen können:
[codesyntax lang="java" lines="fancy" lines_start="1"]
public class MyClass {
private Vector myData;
......
public void doSomething() {
synchronized(myData) {
myData.add(....);
}
}
}
[/codesyntax]
hier werden nur Threads synchronisiert, die auf das selbe Objekt zugreifen. Falls es mehrere Instanzen von MyClass gibt, dürfen auch mehrere Threads gleichzeitig die Methoden in den jeweils anderen Instanzen ausführen. Sowas passiert z.B. gerne bei der Verwendung von EJBs.
Es ist aber auch durchaus möglich, einen Deadlock mit dem Synchronized statement zu erzeugen.
[codesyntax lang="java" lines="fancy" lines_start="1"]
public void a() { synchronized(this) { b(); //do something } } public void b() { synchronized (this) { //do something! } }
[/codesyntax]
Das sieht doch auf den ersten Blick recht gut aus, jedoch bei genauerem hinsehen, kann das nicht funktionieren: Wenn in Methode a() die Methode b() aufgerufen werden soll, ist man schon innerhalb eines synchronized Blocks. in Methode B befindet sich wiederum ein Sychronized Block (auf das selbe Object) und der kann erst dann betreten werden, wenn der erste Block verlassen wird, was ja nicht passiert, solange b() nicht fertig wird... Klassischer Deadlock!
Oft ist das natürlich nicht so plakativ wie hier der Fall, sondern über 3 oder Mehr "Ecken" (Klassen bzw. Objekte)... Das ist zwar nicht unbedingt ein Multithreadding-Only-Thema, denn Deadlocks können auch singlethreadded auftauchen (s.o.), aber bei mehreren Threads sind sie um so schwieriger zu entdecken. Manchmal gibt es auch keine echten Deadlocks sondern nur Verzögerungen, wo ein Thread einfach mal 1-2 Minuten "Pause" macht, biss alle anderen irhre Synchronized-Blöcke verlassen haben.
Static für Threads: Threadlocal
Wenn man allerdings doch etwas statisches einem Thread zur Verfügung stellen will (bzw. allen Objekten, die in diesem Thread instanziiert werden), dann kann man sich mit einer neuen Klasse behelfen: Threadlocal (auch wenn das nicht wirklich die Idee für diese Klasse war):
[codesyntax lang="java" lines="fancy" lines_start="1"]
public class Test { private static ThreadLocalinstance=null; private Test() { //do something } //remember to add synchronized blocks for Singleton!!! public static Test get() { if (instance==null || instance.get()==null) { instance=new ThreadLocal (); instance.set(new Test()); } return instance.get(); } }
[/codesyntax]
Bitte nicht vergessen, hier noch die passenden synchronized-Blöcke einzufügen - der Übersichtlichkeit wegen habe ich die hier mal weggelassen.
Beans, Servlets und so weiter
Diese Dinger sind ja momentan fürchterlich in und werden überall verwendet, meistens ohne sich wirklich Gedanken dazu zu machen. Mal abgesehen, dass man in den Meisten fällen mit EJB sich nur Nachteile einkauft und die Vorteile nicht wirklich nutzt. Aber das ist ein anderes Thema...
Was die meisten nicht bedenken (wieso auch, ist ja so einfach einzusetzen), dass Beans und Servlets einigen Regeln unterliegen, die man - insbesondere im Hinblick auf Multithreadding - besser kennen sollte.
Bei den Servlets verursacht es immer wieder Schwierigkeiten das wenige Instanzen des Servlet von vielen Threads verwendet wird. D.h. in diesem Zusammenhang können auch Member-Variablen zu Problemen werden!
Bei Beans ist es ähnlich, insbesondere Stateless beans... dort ist vielen nicht bekannt, dass, selbst wenn man eine Referenz auf eine Bean hat und selbst wenn es sich um eine Lokal vorhandene Bean handelt, es nicht gesagt ist, dass man bei jedem Methodenaufruf die selbe Bean erhält - oft ist das eben nicht so. Und meistens wird dieses eigentliche Default Verhalten eben erst unter Last gezeigt.
Man kann das natürlich umgehen, in dem man einfach Stateful Session Beans verwendet, die "kleben" quasi am Client. Da darf man dann auch gerne was zwischen Methodenaufrufen speichern.
Aber auch hier ergeben sich Probleme, denn unter hoher last, werden auch ein Haufen solcher Beans erzeugt. Das kann zu Speicher- oder allgemeiner Resourcenproblemen führen. Klar, man kann die Anzahl der Beans auf ein Maximum begrenzen... Dann warten die zusätzlichen Anfragen eben hängen, bis eine Bean freigegeben wurde.
Und ach ja: bitte vergesst nicht, irgendwann der Bean zu sagen, dass sie nicht mehr benötigt wird... Eine Destroy-Methode oder so ist für Stateful Beans wirklich nötig! (Siehe Auch: EJB-Lifecycle).
Abschließende Tipps
Ganz ehrlich, ich hab schon viel zu viel Zeit damit zugebracht, lustige Fehler in Java-Programmen zu suchen, die nur auftraten, wenn Last auf dem System ist. Diese Fehler sind in nahezu 100% der Fälle auf Probleme der Trennung zwischen den Threads zurückzuführen.
So etwas ist leider nicht so einfach zu debuggen, denn es tritt ja nur unter Last auf. Manche dieser Fehler kann man mit JMeter finden und somit dann auch beheben, aber da JMeter naturgemäß nur immer wieder das selbe tut, bzw. die gleiche Anfrage an den Server stellt, kann man damit auch nicht 100% alle Fehler dieser Art finden.
Eine nahezu unverzichtbare Hilfe beim lösen dieser Probleme bieten Profiling-Tools wie z.B. JProfiler. Aber die wirklich nützlichen Tools sind leider kostenpflichtig, auch wenn sich die Anschaffung sicherlich auszahlen wird.