GEDOPLAN
Jakarta EE (Java EE)

Virtual Threads in Jakarta Restful Webservices

Jakarta EE (Java EE)
zebra 4979654 1280 jpg

Mit der Java Version 21 wurden die virtuellen Threads nun endlich offiziell als Feature fertiggestellt und können zur Programmierung einer nebenläufigen Anwendung verwendet werden. Virtuelle Threads sind wesentlich leichtgewichtiger als die bisher zur Verfügung stehenden Platform-Threads. Doch wie kann man in Enterprise Anwendungen von ihnen profitieren und sie sinnvoll einsetzen? Ich möchte euch in diesem Beitrag anhand einer Quarkus Rest-Anwendung die Möglichkeiten aufzeigen.

Daten aus unterschiedlichen Quellen nutzen

Um ein besseres Verständnis für die Nutzung von Nebenläufigkeit zu bekommen, möchte ich zunächst ein einfaches Szenario skizzieren. Wir haben eine Liste von Kunden, auf die wir direkt zugreifen können bspw. über einen Datenbank-Aufruf. In einem weiteren Schritt möchten wir an dieses Kunden-Objekt noch weitere Informationen hängen, die in einem anderen System zur Verfügung gestellt werden. Hierfür kann ich einen einfachen Rest-Client-Aufruf nutzen, der diese Daten dem Kunden-Objekt hinzufügt.

Im untenstehenden Beispiel werden die Basisdaten des Kunden einfach über den Konstruktor erzeugt.

@Data
public class Customer {
  private Long number;
  private String name;
  private BigDecimal sales;

  public Customer(Long number) {
    this.number = number;
    this.name = "Customer" + number;
  }
}

Im Rest-Client simuliere ich dann einen langsamen Aufruf über ein einfaches Thread.sleep(delay). Wobei die delay Variable (Standard 200ms) über einen Property-Eintrag verändert werden kann.

public void getDetails(Customer customer) {
  var random = Math.random() * 1000;
  customer.setSales(BigDecimal.valueOf(random));
  try {
    Thread.sleep(delay);
  } catch (InterruptedException e) {
    throw new RuntimeException(e);
  }
}

Um nun in Quarkus virtuelle Threads nutzen zu können verwenden wir die Annotation @RunOnVirtualThread. Diese Annotation kann jedoch nicht an einer beliebigen Stelle eingesetzt werden. Daher nutzen wir in unserem Beispiel die Methode des Rest-Endpunkts dafür.

@GET
@Path("virtual")
@RunOnVirtualThread
@SneakyThrows
public List<Customer> getAllCustomersVirtual() {
  ...
}

Das Erzeugen der virutellen Threads und der nebenläufige Aufruf des RestClients erfolgt in einem separatem Service. Dazu lässt sich der ExecutorService bequem per Inject nutzen.

@Inject
@VirtualThreads
ExecutorService executorService;

public void getDetailsVirtual(Customer customer) {
  executorService.execute(() -> customerRestClient.getDetails(customer));
}

Aufruf der Rest-Endpunkte

Wenn wir den Endpunkt nun klassisch aufrufen, bekommen wir folgende Log-Ausgabe:

2024-06-03 12:07:17,130 INFO  [de.ged.sho.vth.CustomerResource] (executor-thread-1) executor-thread-1
2024-06-03 12:07:17,135 INFO  [de.ged.sho.vth.CustomerRestClient] (executor-thread-1) Starte Customer(number=1, name=Customer1, sales=657.0995634934278)
2024-06-03 12:07:17,336 INFO  [de.ged.sho.vth.CustomerRestClient] (executor-thread-1) Beende Customer(number=1, name=Customer1, sales=657.0995634934278)
2024-06-03 12:07:17,337 INFO  [de.ged.sho.vth.CustomerRestClient] (executor-thread-1) Starte Customer(number=2, name=Customer2, sales=103.30044405089455)
2024-06-03 12:07:17,538 INFO  [de.ged.sho.vth.CustomerRestClient] (executor-thread-1) Beende Customer(number=2, name=Customer2, sales=103.30044405089455)
2024-06-03 12:07:17,539 INFO  [de.ged.sho.vth.CustomerRestClient] (executor-thread-1) Starte Customer(number=3, name=Customer3, sales=111.8827699955257)
2024-06-03 12:07:17,739 INFO  [de.ged.sho.vth.CustomerRestClient] (executor-thread-1) Beende Customer(number=3, name=Customer3, sales=111.8827699955257)
2024-06-03 12:07:17,740 INFO  [de.ged.sho.vth.CustomerRestClient] (executor-thread-1) Starte Customer(number=4, name=Customer4, sales=521.2613134303721)
2024-06-03 12:07:17,941 INFO  [de.ged.sho.vth.CustomerRestClient] (executor-thread-1) Beende Customer(number=4, name=Customer4, sales=521.2613134303721)

Man kann erkennen, dass ein Executor Thread für die gesamte Ausführung der Aufrufe zuständig ist. Der Aufruf erfolgt dabei synchron hintereinander, so dass bei 4 Kunden der Vorgang 800+ms dauert.

Verwenden wir für den Abruf der Details nun virtuelle Threads, bekommen wir folgende Log-Ausgabe:

2024-06-03 12:07:48,809 INFO  [de.ged.sho.vth.CustomerResource] (quarkus-virtual-thread-0) quarkus-virtual-thread-0
2024-06-03 12:07:48,811 INFO  [de.ged.sho.vth.CustomerRestClient] (quarkus-virtual-thread-1) Starte Customer(number=1, name=Customer1, sales=778.4656731823097)
2024-06-03 12:07:48,811 INFO  [de.ged.sho.vth.CustomerRestClient] (quarkus-virtual-thread-3) Starte Customer(number=3, name=Customer3, sales=338.8230603545126)
2024-06-03 12:07:48,811 INFO  [de.ged.sho.vth.CustomerRestClient] (quarkus-virtual-thread-2) Starte Customer(number=2, name=Customer2, sales=235.9506199995366)
2024-06-03 12:07:48,811 INFO  [de.ged.sho.vth.CustomerRestClient] (quarkus-virtual-thread-4) Starte Customer(number=4, name=Customer4, sales=329.02252118005805)
2024-06-03 12:07:49,012 INFO  [de.ged.sho.vth.CustomerRestClient] (quarkus-virtual-thread-1) Beende Customer(number=1, name=Customer1, sales=778.4656731823097)
2024-06-03 12:07:49,012 INFO  [de.ged.sho.vth.CustomerRestClient] (quarkus-virtual-thread-2) Beende Customer(number=2, name=Customer2, sales=235.9506199995366)
2024-06-03 12:07:49,012 INFO  [de.ged.sho.vth.CustomerRestClient] (quarkus-virtual-thread-3) Beende Customer(number=3, name=Customer3, sales=338.8230603545126)
2024-06-03 12:07:49,013 INFO  [de.ged.sho.vth.CustomerRestClient] (quarkus-virtual-thread-4) Beende Customer(number=4, name=Customer4, sales=329.02252118005805)

Bereits der Endpunkt wird innerhalb eines virutellen Threads gestartet. Darüber hinaus werden nun 4 virutelle Threads für das Sammeln der Detailinformationen verwendet. Damit ergibt sich nun eine Laufzeit von 200+ms.

Fazit

Durch den Einsatz von virtuellen Threads lassen sich in einigen Fällen Aufrufe parallelisieren und damit eine bessere Laufzeit erzielen. Der Vorteil bei der Verwendung von Quarkus ist die Möglichkeit sowohl blocking, als auch non-blocking in Form von virtuellen Threads in einer Anwendung nutzen zu können. Es kann also je nach Anwendungsfall entschieden werden, welche Variante für die Aufrufe sinnvoll ist. Der Quellcode für das Beispiel befindet sich wie immer auf Github.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Bitte füllen Sie dieses Feld aus.
Bitte füllen Sie dieses Feld aus.
Bitte gib eine gültige E-Mail-Adresse ein.
Sie müssen den Bedingungen zustimmen, um fortzufahren.

Autor

Diesen Artikel teilen

LinkedIn
Xing

Gibt es noch Fragen?

Fragen beantworten wir sehr gerne! Schreibe uns einfach per Kontaktformular.

Kurse

weitere Blogbeiträge

Work Life Balance. Jobs bei Gedoplan

We are looking for you!

Lust bei GEDOPLAN mitzuarbeiten? Wir suchen immer Verstärkung – egal ob Entwickler, Dozent, Trainerberater oder für unser IT-Marketing! Schau doch einfach mal auf unsere Jobseiten! Wir freuen uns auf Dich!

Work Life Balance. Jobs bei Gedoplan

We are looking for you!

Lust bei GEDOPLAN mitzuarbeiten? Wir suchen immer Verstärkung – egal ob Entwickler, Dozent, Trainerberater oder für unser IT-Marketing! Schau doch einfach mal auf unsere Jobseiten! Wir freuen uns auf Dich!