Java EE Concurrency API

Hallo allerseits!

Und hier gönnen wir uns Brötchen und starten den zweiten Stream des Kurses "Java Enterprise Developer" . Der ständige Schöpfer und Lehrer des Kurses - Vitaly Ivanov - hat sogar einen Artikel zu diesem Thema geschrieben, der Ihnen hoffentlich nützlich erscheinen wird :)

Also lass uns gehen :)

In diesem Artikel wird die JavaEE Concurrency Specification API ( JSR 236 ) erläutert, die den Standard für parallele Aufgaben in einem JavaEE-Container unter Verwendung des Konzepts verwalteter Ressourcen definiert. Die Veröffentlichung der siebten Version von JavaEE ermöglichte die Ausführung paralleler Aufgaben in Enterprise-Containern und bot dem Entwickler praktische Tools und Dienstprogramme für die Arbeit mit Multitasking. Bis zu diesem Zeitpunkt war das gesamte Multitasking der spezifischen Implementierung des verwendeten Anwendungsservers überlassen, der unabhängig über die Optimierung der Aufgaben entscheidet. Verstöße gegen dieses Prinzip wurden als schlechte Praxis beim Aufbau der Architektur von Unternehmensanwendungen angesehen. Infolgedessen wurde dem Entwickler nicht empfohlen, neue Threads zu erstellen, und manchmal war ein solches Verhalten auf Containerebene verboten.


Die Enterprise-Bean darf nicht versuchen, Threads zu verwalten. Die Enterprise-Bean darf nicht versuchen, einen Thread zu starten, zu stoppen, anzuhalten oder fortzusetzen oder die Priorität oder den Namen eines Threads zu ändern. Die Enterprise-Bean darf nicht versuchen, Thread-Gruppen zu verwalten.

(Kostenlose Übersetzung des Autors: EJBs sollten nicht versuchen, Threads zu verwalten, dh versuchen, ihre Ausführung zu starten, zu stoppen, anzuhalten und wiederherzustellen oder die Priorität oder den Namen eines Threads zu ändern. Außerdem sollten EJBs nicht versuchen, Thread-Gruppen zu verwalten.)

Das Verbot der Erstellung eigener Threads in JavaEE-Containern ist zwar recht problematisch, bei diesem Ansatz können die Hintergrunddienste des Containers jedoch nicht die Richtigkeit ihrer Arbeit garantieren. Beispielsweise kann das Schließen einer Transaktion nach Abschluss der EJB-Methode möglicherweise nicht ordnungsgemäß funktionieren, wenn Aufgaben in einem neuen Thread mithilfe von Threads-Vererbungen (oder ausführbaren Implementierungen) von JavaSE gestartet wurden. Die Verwendung der von der Executor-API bereitgestellten grundlegenden Schnittstellentypen wie ExecutorService und ScheduledExecutorService würde beim Erstellen mit den statischen Methoden der Executors-Klasse zu potenziellen Fehlern führen und die Ausführung von Containerdiensten stören.

Von den in der JavaEE-Spezifikation empfohlenen Tools für die asynchrone Ausführung von Aufgaben musste der Entwickler asynchrone zustandslose / Statefull-EJBs und / oder Message Driven Beans verwenden, deren Funktionen für einen bestimmten Aufgabenbereich ausreichen und deren Verwaltung zunächst vollständig und vollständig vom Anwendungsserver gesteuert wird. nämlich ein EJB-Container.

Wie bereits erwähnt, sind dank JSR 236 Container-verwaltete Ressourcen erschienen, die Unterstützung für Multithreading und asynchrone Taskausführung implementieren und die Funktionen des Pakets java.util.concurrent von JavaSE erweitern. Für den JavaEE-Stack befinden sich die verwalteten Ressourcenklassen im Paket javax.enterprise.concurrent , und der Zugriff auf die Objekte dieser Klassen erfolgt über die Ressourceninjektion mithilfe der Annotation @Resource oder über den JNDI-Kontext (insbesondere InitialContext). Gleichzeitig wurde die Möglichkeit hinzugefügt, Future / ScheduledFuture / CompletableFuture-Objekte zu verwenden, die in Umgebungen mit mehreren Threads in JavaEE-Anwendungen bekannt sind.

Es gibt also genügend Texte und wir wollen uns die verwalteten Ressourcen, die in der Spezifikation bereitgestellt werden, aus praktischer Sicht genauer ansehen, und zwar im Zusammenhang mit der Verwendung der Anwendung im Anwendungscode sowie unter dem Gesichtspunkt der Ressourcenkonfiguration am Beispiel des Glassfish 5-Anwendungsservers.

Nun, die ManagedExecutorService-Klasse war die erste, die in Betracht gezogen wurde. Sie erweitert (bereits aus dem Namen bekannt) die Funktionen des bekannten JavaSE ExecutorService und ist für die asynchrone Ausführung von Aufgaben in der JavaEE-Umgebung ausgelegt.

Um nicht nur diesen Typ von ExecutorService auf dem Glassfish-Anwendungsserver zu konfigurieren, sollten Sie auf die Konfigurationsdatei domain.xml verweisen, deren Speicherort durch das Verzeichnis $ {GLASSFISH_HOME} / domain / <Domänenname> / config bestimmt wird. Ein Fragment dieser Datei ist unten dargestellt:

 <domain application-root="${com.sun.aas.instanceRoot}/applications" version="25" log-root="${com.sun.aas.instanceRoot}/logs"> <resources> <context-service object-type="system-all" jndi-name="concurrent/__defaultContextService" /> <managed-executor-service object-type="system-all" jndi-name="concurrent/__defaultManagedExecutorService" /> <managed-scheduled-executor-service object-type="system-all" jndi-name="concurrent/__defaultManagedScheduledExecutorService" /> <managed-thread-factory object-type="system-all" jndi-name="concurrent/__defaultManagedThreadFactory" /> </resources> <servers> <server config-ref="server-config" name="server"> <resource-ref ref="concurrent/__defaultContextService" /> <resource-ref ref="concurrent/__defaultManagedExecutorService" /> <resource-ref ref="concurrent/__defaultManagedScheduledExecutorService" /> <resource-ref ref="concurrent/__defaultManagedThreadFactory" /> </server> </servers> </domain> 

Gehen Sie in die Benutzeroberfläche des Glassfish 5-Administrationsbereichs und konfigurieren Sie

ManagedExecutorService lautet wie folgt:



Dieser Abschnitt ermöglicht das Erstellen neuer Ressourcen desselben Typs, das Verwalten vorhandener Ressourcen, das Löschen sowie das Sperren und Entsperren.

Für Fans der Konsolenverwaltung in Glassfish wird ein leistungsstarkes Dienstprogramm asadmin vorgestellt, mit dem Sie mithilfe des darin enthaltenen Befehls create-manage create-managed-executor-service neue ManagedExecutorService-Ressourcen erstellen können:



Im Anwendungscode ist es bequemer, die Ressourceninjektion zu verwenden, um einen Verweis auf das vom ManagedExecutorService erstellte Objekt abzurufen. Sie können jedoch auch die JNDI-Tools verwenden, wie unten gezeigt:

 @Resource(lookup = "concurrent/OtusExecutorService") ManagedExecutorService executor; InitialContext context = new InitialContext(); ManagedExecutorService managedExecutorServiceWithContext = (ManagedExecutorService) context.lookup( "concurrent/OtusExecutorService"); 

Ich möchte den Leser darauf aufmerksam machen, dass der Lookup-Parameter für die @Resource Annotation optional ist. Wenn er vom Entwickler nicht im Anwendungscode definiert wird, __default der Container Standardressourcen mit dem Präfix __default in ihrem __default . In diesem Fall wird der Code für den Entwickler noch präziser:

 @Resource ManagedExecutorService executor; 

Nachdem Sie mit den Methoden execute execute() und submit execute() einen Verweis auf dieses Objekt erhalten haben, können Sie Aufgaben ausführen, die die Runnable- oder Callable-Schnittstelle im Container implementieren.

An einem Beispiel möchte ich festhalten, dass unter den vielen möglichen Fällen die interessantesten Aufgaben in einer verteilten JavaEE-Umgebung ausgeführt werden und es wichtig ist, Transaktionsunterstützung in einer Multithread-Umgebung bereitzustellen. Wie Sie wissen, hat JavaEE die JTA-Spezifikation (Java Transaction API) entwickelt, mit der Sie die Grenzen einer Transaktion bestimmen können, indem Sie sie explizit mit der Methode begin() und mit commit() -Methoden enden, Änderungen rollback() oder rollback() , mit der die ausgeführten Aktionen zurückgesetzt werden.

Stellen Sie sich eine Beispielaufgabe vor, die eine Nachricht aus einer Liste von einhundert Elementen nach Index in einer Benutzertransaktion zurückgibt:

 public class TransactionSupportCallableTask implements Callable<String> { private int messageIndex; public TransactionSupportCallableTask(int messageId) { this. messageIndex = messageId; } public String call() { UserTransaction tx = lookupUserTransaction(); String message = null; try { tx.begin(); message = getMessage(messageIndex); tx.commit(); } catch (Exception e) { e.printStackTrace(); try { tx.rollback(); } catch (Exception e1) { e1.printStackTrace(); } } return message; } private void getMessage(int index) { … } private UserTransaction lookupUserTransaction () { … } } 

Der Code für das Servlet, das eine Nachricht aus der Liste an einem zufällig ausgewählten Index anzeigt:

 @WebServlet("/task") public class ManagedExecutorServiceServlet extends HttpServlet { @Resource(lookup = "concurrent/OtusExecutorService") ManagedExecutorService executor; protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { Future<String> futureResult = executor.submit(new TransactionSupportCallableTask(Random.nextInt(100))); while (!futureResult.isDone()) { try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } } try { response.getWriter().write("Callable task has received message with following content '" + futureResult.get() + "'"); } catch(Exception e) { e.printStackTrace(); } } } 

Als nächstes ist die Ressource ManagedScheduledExecutorService zu berücksichtigen, deren Hauptzweck darin besteht, Aufgaben zu planen, die in regelmäßigen Abständen wiederholt werden oder eine verzögerte Ausführung erfordern.



Unter dem Gesichtspunkt der Konfiguration dieser Ressource über die GlassFish-Administratorkonsole wurden im Vergleich zum vorherigen Typ keine besonderen Änderungen festgestellt:



Um schnell eine Ressource vom Typ asadmin create-managed-scheduled-executor-service , verfügt asadmin über den Befehl create-managed-scheduled-executor-service



Im Anwendungscode verwenden wir weiterhin die Ressourceninjektion:

 @Resource(lookup = "concurrent/OtusScheduledExecutorService") ManagedScheduledExecutorService scheduledExecutor; 

Die Hauptmethoden zum Ausführen von Aufgaben für diesen ExecutorService-Typ sind schedule() , das Aufgaben vom Typ Runnable oder Callable als Eingabe empfängt, und scheduleAtFixedRate() , das zusätzlich die anfängliche Verzögerung in der Aufgabe bestimmt und das Wiederholungsintervall in TimeUnit (Sekunden, Minuten usw.) festlegt. .).

Der vorherige Fall kann wie folgt umgeschrieben werden:

 @WebServlet("/scheduledTask") public class ManagedScheduledExecutorServiceServlet extends HttpServlet { @Resource(lookup = "concurrent/OtusScheduledExecutorService") ManagedScheduledExecutorService scheduledExecutor; protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { ScheduledFuture<String> futureResult = scheduledExecutor.schedule( new TransactionSupportCallableTask(Random.nextInt(100)), 5, TimeUnit.SECONDS); while (!futureResult.isDone()) { try { Thread.sleep(50); // Wait } catch (InterruptedException e) { e.printStackTrace(); } } try { response.getWriter().write("Callable task received message with following content '" + futureResult.get() + "'"); } catch ( Exception e) { e.printStackTrace(); } } } 

Die Concurrency-API für die Enterpise-Umgebung bietet außerdem die Möglichkeit, kontrollierte Flows zu erstellen. Für diese Aufgaben sollten Sie die Funktionen einer verwalteten Thread-Factory verwenden, die ihre Funktionalität über die gleichnamige ManagedThreadFactory-Klasse implementiert und auf die auch über den JNDI-Dienst zugegriffen wird:

 @Resource ManagedThreadFactory factory; 

Das Administrationsfenster für die Glassfish-Konsole sieht „altmodisch“ aus:



Mithilfe einer verwalteten Thread-Factory können Sie dem Container nicht nur Flusssteuerungsmechanismen zur Verfügung stellen, sondern auch die Eigenschaften der generierten Threads initialisieren: Namen festlegen und Prioritäten setzen, was in Zukunft die Suche nach Problemen beim Parsen eines Thread-Dumps erheblich vereinfachen kann, indem die Ausführungsreihenfolge zuvor benannter Threads leicht erkannt wird.

In unserem Fall definieren wir die Klasse des Streams, die Informationen zu einem Freund anzeigt, mit dem diese Aufgabe untrennbar mit der Konsole verbunden ist:

 public class SimpleThreadTask implements Runnable { private String friend; public SimpleThreadTask(String friend){ this.friend = friend; } @Override public void run() { System.out.println("Hello, " + friend); } } 

Lassen Sie das Servlet den Thread starten und melden Sie dies der Ausgabe:

 @WebServlet("/thread") public class ManagedThreadFactoryServlet extends HttpServlet { @Resource ManagedThreadFactory factory; protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { Thread thread = factory.newThread(new SimpleThreadTask("Otus")); thread.setName("ManagedThreadFromPool"); thread.setPriority(7); thread.start(); response.getWriter().write("Custom thread has been running."); } } 

In Bezug auf die letzte Funktion von JavaEE im Bereich Multithreading - Context Services sollte beachtet werden, dass diese Services dynamische kontextbezogene Proxy-Objekte erstellen. Wir alle kennen die Funktionen dynamischer Proxys aus JavaSE ( java.lang.reflect.Proxy ), mit denen Sie eine dynamische Implementierung der erforderlichen Schnittstellen generieren können, deren Funktionen aktiv zum Erstellen von Datenbankverbindungen und zum Transaktionsmanagement verwendet werden und die für alle Arten von AOP-Interceptors usw. verwendet werden. Darüber hinaus wird für Proxys, die über JavaEE-Kontextdienste erstellt wurden, angenommen, dass sie im Rahmen eines gemeinsamen JNDI-Kontexts, Sicherheitskontexts und einer gemeinsamen Containerklasse arbeiten können.

Verwenden Sie zum Verbinden des Dienstes einfach den folgenden Code:

 @Resource ContextService service; 

Unter dem Gesichtspunkt der Verwaltung und Konfiguration dieser Ressource ist alles äußerst vertraut und den bereits betrachteten Typen ähnlich:



Unten sehen Sie ein Beispiel für einen Thread, der eine Proxy-Aufgabe im Kontext eines Containers startet:

 public class SampleProxyTask implements Runnable { @Override public void run() { //  Subject subject = Subject.getSubject(AccessController.getContext()); logInfo(subject.getPrincipals()); //    calculateSmth(); } private void calculateSmth() { … } private void logInfo(Set<Principal> subject) { … } } 

Zustandslose EJB-Bean zum Erstellen kontextbezogener Proxys:

 @Stateless public class ContextServiceBean { @Resource ContextService service; @Resource ManagedExecutorService executor; public void perform(Runnable task) { Runnable proxy = service.createContextualProxy(task, Runnable.class); executor.submit(proxy); } } 

Und schließlich der Code für das Servlet, das die Aufgabe ausführt:

 @WebServlet("/context") public class ContextServiceServlet extends HttpServlet { @Inject ContextServiceBean contextServiceBean; protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { contextServiceBean.perform(new SampleProxyTask()); } } 

Dies beendet tatsächlich die Fähigkeiten des JavaEE-Entwicklers, in einer Multithread-Umgebung zu arbeiten. Dank ihnen unterliegen alle Prozesse und Dienste, die im Container stattfinden, der strengen Kontrolle des Servers, koordinieren ihre Arbeit und verletzen nicht die übliche Ausführungsreihenfolge. Für die Ziele des Enterprise-Entwicklers reichen diese Funktionen häufig aus, und in der achten Version hat sich diese API nicht geändert.

DAS ENDE

Wie immer warten wir auf Fragen und Kommentare und besuchen Vitaly für eine offene Lektion , in der er auch Fragen stellen und das Thema „CDI in Aktion @“ anhören / daran teilnehmen kann.

Source: https://habr.com/ru/post/de421809/


All Articles