J and I and Me
2009-09-11
  EJB 3.1 Singletons considered harmful
Bevor wir zum Problem vorstoßen, zunächst einige Worte zu Singletons und Pooling. EJB hat vor 3.1 nur ein Modell mit Pooling von Bean angeboten. Der Grund für diese Entscheidung ist, dass man sich so nicht mit Multi-Threading in seiner Anwendung beschäftigen muss. Jeder Thread bekommt seine Instanz aus dem Pool, daher wird jede Instanz nur von einem Thread genutzt. Spring hingegen nutzt Singletons - von jeder Spring Bean gibt es typischerweise nur eine Instanz. Spring bietet auch die Option für Pooling - aber dieses Feature würde ich für den Preis des am wenigsten genutzten Features in Spring nominieren. Ich selber habe es bisher nur einmal produktiv verwendet - und nicht etwa, um Thread-Problemen zu entgehen, sondern um Ressourcen zu poolen. Die Nutzung von Singletons stellt bezüglich den Threads nämlich meistens kein Problem dar - weil die Beans fast immer Thread-Safe sind. Sie erhalten die Daten als Parameter, haben keinen eigenen Zustand und Ressourcen wie die Datenbankverbindung wird von Spring automatisch an den Thread gekoppelt. Dieses Modell wird in vielen Anwendungen genutzt und hat sich als sehr skalierbar erwiesen.

EJB 3.1 bietet nun als Neuerung auch ein Singelton-Modell an. Obwohl es aber mittlerweile unstrittig ist, dass Pooling praktisch nie wirklich benötigt wird, bleibt das Pooling der Default. Singletons sind vor allem dafür gedacht, um Datenstrukturen zu implementieren, die man nur einmal haben möchte. Dazu gehören zum Beispiel Caches. Es macht kaum Sinn, mehrere Instanzen eines Caches vorzuhalten. Man sollte lieber für alle Zugriffe dieselbe Instanz nutzen, weil man sonst mehrere Cache befüllen muss. Ein einfaches Singleton kann zum Beispiel folgendermaßen aussehen:

@Singleton
public class MyCache {

private Map cache = new ConcurrentHashMap();

public Object get(String key) {
return cache.get(key);
}

public void store(String key, Object value) {
cache.put(key, value);
}

}

Sieht jemand das Problem? Nun, wenn man diese Implementierung so nutzt, wird man ein erhebliches Skalierungsproblem haben. Überrascht? Lesen Sie weiter.

EJB 3.1 Singletons haben ein interessantes Feature. In der EJB-3.1-Spezifikation ist das Problem der Nebenläufigkeit gelöst worden. Dieses Thema dort anzusprechen ist gleich aus mehreren Gründen spannend:

Das erinnert an eines der größten Probleme von EJB in Version 1.x und 2.x. Damals wurde mit Container Managed Persistence das Persistenz-Problem gelöst. Dabei wurden die bekannten Ansätze von O/R-Mappern ignoriert - so wie man jetzt die vorhandenen Nebenläufigkeits-APIs ignoriert. Und man hat eine Lösung in EJB implementiert, so dass man sie nur in einem Application Server nutzen kann - obwohl Persistenz genau wie heute Nebenläufigkeit ein viel allgemeineres Problem ist.

Wie funktioniert nun die Nebenläufigkeit bei EJB 3.1? Der Default ist Container Managed Concurrency. Alternativ kann man durch Bean Managed Concurrency die üblich Java-Bordmittel nutzen. Bei Container Managed Concurrency kann eine Methode mit @Lock(READ) oder @Lock(WRITE) markieren. Nur eine Methode mit einem WRITE-Lock kann zu einem Zeitpunkt aufgerufen werden. Keine andere Methode - weder mit READ- noch mit WRITE-Lock - darf dann in einem anderen Thread ausgeführt werden. Parallele Abarbeitung von Methoden mit READ-Locks ist hingegen erlaubt. Die Nebenläufigkeit an Methoden fest zu machen ist alleine schon Problem genug. Der Schlüssel zu einem skalierbaren System ist, dass man Locks so kurz wie möglich hält.

Ich hatte vor einiger Zeit einen Consulting-Einsatz wegen eines Skalierungsproblems. Das habe ich gelöst, indem ich diesen Code:

...
synchronized(cache) {
if (cache.get(key)==null) {
cache.put(aValue);
}
}
...


durch solchen Code ersetzt habe:

...
if (cache.get(key)==null) {
synchronized(cache) {
if (cache.get(key)==null) {
cache.put(aValue);
}
}
}
...


Dadurch wird der Cache nur noch gelockt, wenn er den Wert noch nicht enthält. Da der Cache irgendwann gefüllt ist, wird der Cache dann praktisch gar nicht mehr gelockt und man kann der Code dann mit wesentlich besserer Nebenläufigkeit betrieben werden. Wie man hier sieht, kann das Verschieben des Locking um eine Zeile schon einen erheblichen Unterschied machen. Wenn man solche Sachen mit den EJB-3.1-Annotationen machen will, wird man nicht weiterkommen, da sie nur auf Methoden wirken.

Schlimmer wird es nun dadurch, dass der Default ist, dass jede Methode ohne Annotationen so behandelt wird, als wäre sie mit @Lock(WRITE) versehen. Das bedeutet, dass in dem gesamten EJB-3.1-Singleton - wenn man nicht anderes definiert - nur ein Thread zur Zeit aktiv sein kann. Der oben mit EJB 3.1 implementierte Cache kann also nur von einem Thread parallel genutzt werden. Das wird in der Realität bei vielen parallelen Threads zu erheblichen Problemen führen. Da @Lock(WRITE) der Default ist, sieht man gar nicht, was passiert, und muss man sehr genau verstehen, was hinter den Kulissen vor sich geht. Wer also einfach so mit EJB 3.1 los programmiert, wird in diese Falle tappen.

Besonders ironisch ist natürlich, dass der oben definierte Cache durch die ConcurrentHashMap sowieso von mehreren Threads parallel genutzt werden kann. EJB 3.1 schüzt uns also ganz umsonst von der ach-so-komplizierten Welt der Nebenläufigkeit.

Labels: , ,

  09:00
Bookmark and Share
Comments:
Hallo,

das Beispiel hat das Problem, dass es so nicht thread-safe sein wird. "Double-Checked-Locking", der Compiler wird dort einiges weg"optimieren". http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html

Nunja, die eigentliche Idee wird trotzdem klar.
 
Hallo,
Danke für den Kommentar. Allerdings geht es in dem Artikel um die Erzeugung von Objekten, hier geht es ja um einen Methoden-Aufruf. Von daher würde ich denken, dass der Code in Ordnung sein sollte.
Gruß,
Eberhard Wolff
 
Es ist ja nicht ganz klar, was hinter dem "cache" steckt. Wenn das eine ConcurrentMap ist, dann ist Ebehard's code ok. Eine plain HashMap waere fatal.

Allerdings schade ist, dass in dieser Variante requests fuer verschiedene, nicht vorhandene Cache-Keys sequentiell bearbeitet werden. Das geht besser.

Statt das allerdings selber zu bauen, gibt es hier eine solide Implementierung, die sich um per-key Locking und die Concurrency kuemmert:

http://code.google.com/p/google-collections/source/browse/trunk/src/com/google/common/collect/MapMaker.java?r=80&spec=svn96#270

Matthias
 
Ich denke immer öfter, dass in Java-APIs, besonders denen von Java EE, unnötig viele Sachen neu erfunden werden, statt schon vorhandene grundlegende Lösungen zu verwenden oder zu entwickeln. Das führt zu unnötiger Komplexität und dazu, dass man die Programmiersprache mit ihren grundlegenden Funktionen und APIs gar nicht mehr nutzt, sondern sich nur noch mit speziellen APIs herum schlägt.

Da gefällt mir der Ansatz von Spring sehr gut, wo mit wenigen aber mächtigen Konzepten wie DI und AOP sehr viele Probleme auf immer gleiche Weise gelöst werden.

Ich wünsche mir nur, dass Spring selbst mal modernisiert wird und die XML-Zeit endgültig überwunden wird. Die Java-Konfiguration von Spring ist leider nicht zufriedenstellend, denn an bestimmten Stellen kommt man an XML einfach nicht vorbei und da auch alle Beispiele in XML gehalten sind, ist es oft einfacher, bei XML zu bleiben. Allein schon die Tatsache, dass man ein Werkzeug wie die STS braucht, um den Überblick zu behalten, deutet auf ein grundlegendes Problem hin, nämlich nicht typsichere XML-Konfigration. In dieser Hinsicht finde ich JSR 299 CDI (bzw. die Implementierung Weld) oder Google Guice besser. Spring 4 sollte die besten Ideen von Weld und Spring 3 kombinieren: DI mit Annotationen (ja, ich weiß, JSR 330 gibt es in Spring 3) und Java-Konfiguration, aber AOP auch mit AspectJ ermöglichen. Und das ganze am besten noch (optional) mit Scala :-)
 
Hallo Christian,

vielen Dank für den Kommentar. Mit Hilfe der @Configuration Annotation von Spring 3.0 kann man XML auf ein absolutes Minimum reduzieren, siehe http://static.springsource.org/spring/docs/3.0.x/spring-framework-reference/html/beans.html#beans-java-basic-concepts . Spring lässt einem da eben die Wahl - im Gegensatz zu Java EE 6.
 
Hallo

Ich bin per Zufall über diesen Blog gestolpert.

Der Code hat mindestens ein Probleme: er ist nicht korrekt, da die Nebenläufigkeit nicht korrekt behandelt wird. Wird man dies im Betrieb merken? Wohl kaum.

BTW: "Double-Checked-Locking" tritt nicht nur bei der Erzeugung auf. Es ist ein generelles Problem, der Code entspricht aber nicht diesem (Anti Pattern).

Grüsse
Jörg
 
Kommentar veröffentlichen

<< Home
J for Java | I for Internet, iMac, iPod and iPad | Me for me

ARCHIVES
Juni 2005 / Juli 2005 / August 2005 / September 2005 / Oktober 2005 / November 2005 / Dezember 2005 / Januar 2006 / Februar 2006 / März 2006 / April 2006 / Mai 2006 / Juni 2006 / Juli 2006 / August 2006 / September 2006 / Oktober 2006 / November 2006 / Dezember 2006 / Januar 2007 / Februar 2007 / März 2007 / April 2007 / Mai 2007 / Juni 2007 / Juli 2007 / August 2007 / September 2007 / Oktober 2007 / November 2007 / Dezember 2007 / Januar 2008 / April 2008 / Mai 2008 / Juni 2008 / August 2008 / September 2008 / November 2008 / Januar 2009 / Februar 2009 / März 2009 / April 2009 / Mai 2009 / Juni 2009 / Juli 2009 / August 2009 / September 2009 / Oktober 2009 / November 2009 / Dezember 2009 / Januar 2010 / Februar 2010 / März 2010 / April 2010 / Mai 2010 / Juli 2010 / August 2010 / Oktober 2010 / Januar 2011 / Februar 2011 / März 2011 / April 2011 / Mai 2011 / Juni 2011 / August 2011 / September 2011 / November 2011 / Februar 2012 / April 2012 / Mai 2012 / April 2013 / Mai 2013 / Juni 2013 / Januar 2015 / Juli 2015 / Februar 2016 /

Links

Twitter
Google +
Slideshare
Prezi
XING
LinkedIn
Das Spring Buch


Feeds

Feedburner


Impressum
Betreiber und Kontakt:
Eberhard Wolff
Leobschützer Strasse 22
13125 Berlin
E-Mail-Adresse: eberhard.wolff@gmail.com

Verantwortlich für journalistisch-redaktionelle Inhalte:
Eberhard Wolff