Um den nebenläufigen Zugriff auf einen „kritischen Abschnitt“ im Code zu kontrollieren, bietet Java SE verschiedene Möglichkeiten: synchronized
-Blöcke und -Methoden sowie die Klassen des Packages java.util.concurrent
. Doch wie verhält sich dies mit EJBs? Hier lohnt sich ein genauerer Blick.
Seit EJB 3.1 gibt es die Möglichkeit, mit Singleton Session Beans zu arbeiten. Per Default kontrolliert der EJB-Container dann den nebenläufigen Zugriff auf die einzige Instanz einer solchen Bean, und das zunächst sehr restriktiv: standardmäßig kann immer nur ein Thread auf die Bean zugreifen. Methodenaufrufe werden also sequentialisiert, was natürlich bei einer stark frequentierten Singleton Session Bean durchaus zu Performanz-Einbußen führen kann. Dies lässt sich aber dadurch regeln, dass die Bean-Klasse oder die einzelnen Methoden selbst mit der Annotation @Lock
versehen werden.
@Singleton @ConcurrencyManagement(CONTAINER) public class MySingletonBean { @Lock(READ) public Object readState() { // ... } @Lock(WRITE) public void writeState(Object state) { // ... } }
Methoden, die mit @Lock(READ)
markiert sind, können nun nebenläufig ausgeführt werden, Methoden mit @Lock(WRITE)
erlauben dagegen nur einen Thread zu einem Zeitpunkt. Während der Ausführung einer Methode mit WRITE-Lock sind dann auch alle anderen Methoden gesperrt. Eine Kombination der @Lock
-Annotation an der Bean-Klasse und an einzelnen Methoden ist möglich. Für nicht extra annotierte Methoden wird dann die Einstellung der Klasse verwendet, und Annotationen an einzelnen Methoden überschreiben diesen Default. Wer eine noch feingranularere Kontrolle über die Nebenläufigkeit benötigt, kann eine Singleton Session Bean auch als @ConcurrencyManagement(BEAN)
deklarieren und hat dann alle Möglichkeiten zur Verfügung, die Java SE bietet, d.h. synchronized
-Blöcke und -Methoden sowie die Klassen des Packages java.util.concurrent
.
So weit, so gut. Doch was ist, wenn ich keine Singleton Session Beans verwenden kann, z.B. weil ich noch mit EJB 3.0 arbeiten muss? Für Stateless Session Beans (oder auch Message-Driven Beans) hält der EJB-Container einen Pool von Instanzen vor und jeder Thread bekommt seine eigene Instanz aus diesem Pool. Somit greifen auch die üblichen Java SE-Standards nicht. Nun, die Lösung ist eigentlich denkbar einfach: Ich baue mir einfach ein gemeinsam verwendetes Singleton-Lock oder Semaphor und verwende dabei die Möglichkeiten, die mir das Java SE Package java.util.concurrent
bietet:
import java.util.concurrent.ReadWriteLock; import java.util.concurrent.ReentrantReadWriteLock; public class SingletonReadWriteLock { private static final ReadWriteLock LOCK_INSTANCE = new ReentrantReadWriteLock(); private SingletonReadWriteLock() { // geschützer Konstruktor, da Singleton! } public static ReadWriteLock getInstance() { return LOCK_INSTANCE; } }
Zugegeben, diese Implementation entspricht nicht hundertprozentig dem Singleton-Pattern, aber durch diese einfache, verkürzte Form sparen wir uns hier, nochmal alle Methoden des Lock-Interfaces zu implementieren, nur um diese dann an die Instanz des Locks zu delegieren.
Um alle Instanzen einer Stateless Session Bean zu synchronisieren, kann ein solches gemeinsames Lock-Singleton dann wie folgt verwendet werden:
@Stateless public class MyStatelessBean { public Object readState() { SingletonReadWriteLock.getInstance().readLock().lock(); // ... SingletonReadWriteLock.getInstance().readLock().unlock(); } public void writeState(Object state) { SingletonReadWriteLock.getInstance().writeLock().lock(); // ... SingletonReadWriteLock.getInstance().writeLock().unlock(); } }
Dieser Ansatz erlaubt es auch, mehrere Session Beans unterschiedlichen Typs miteinander zu synchronisieren. Voraussetzung dafür ist, dass das Singleton-Lock in einem von allen betroffenen Beans gemeinsam verwendeten Classloader verfügbar ist, z.B. in einem gemeinsamen EAR oder dem globalen Classloader des EJB-Servers (wenn z.B. Classloader-Isolation für EARs verwendet wird). An seine Grenzen stößt dieser Ansatz natürlich in einer geclusterten Umgebung oder wenn die zu synchronisierenden Beans in einer verteilten Umgebung laufen, d.h. auf mehrere Applikationsserver verteilt sind. In diesen Fällen müsste man dann ein Locking über eine externe, gemeinsam genutzte Ressource implementieren, z.B. über eine Datenbank.