Von Elyndra Valen, Senior Entwicklerin bei Java Fleet Systems Consulting
Mit Einblicken von Jamal Hassan (Backend Dev) und Nova Trent (Junior Dev)
Schwierigkeit: 🟡 Mittel
Lesezeit: 45 Minuten
Voraussetzungen: Java Grundlagen, Lambdas
Kurs: Java Erweiterte Techniken – Tag 8 von 10
📖 Java Erweiterte Techniken – Alle Tage
📍 Du bist hier: Tag 8
⚡ Das Wichtigste in 30 Sekunden
Teil 1 – Annotations: Metadaten für deinen Code – von @Override bis eigene Annotations.
Teil 2 – Multithreading: Mehrere Dinge gleichzeitig tun – Threads erstellen und starten.
Heute lernst du:
- ✅ Built-in Annotations:
@Override,@Deprecated,@SuppressWarnings - ✅ Eigene Annotations erstellen
- ✅ Threads erstellen mit
ThreadundRunnable - ✅ Thread-Lebenszyklus verstehen
- ✅
ExecutorServicefür Thread-Pools - ✅
CallableundFuturefür Rückgabewerte
👋 Elyndra: „Zwei Themen, ein Tag“
Hi! 👋
Heute packen wir zwei fundamentale Themen in einen Tag: Annotations und Multithreading Basics. Beide sind essentiell für modernes Java.
Warum zusammen? Weil Annotations oft bei Multithreading zum Einsatz kommen – denk an @Async in Spring oder Thread-Safety-Annotations.
Teil 1: Annotations
🖼️ Annotations Übersicht

Abbildung 1: Die wichtigsten Java-Annotations
🟢 GRUNDLAGEN: Built-in Annotations
@Override – Der Lebensretter
public class Tier {
public void sprechen() {
System.out.println("...");
}
}
public class Hund extends Tier {
@Override
public void sprechen() { // Compiler prüft: Existiert diese Methode in Tier?
System.out.println("Wuff!");
}
@Override
public void sprecken() { // COMPILER ERROR! Tippfehler erkannt!
System.out.println("Wuff!");
}
}
Ohne @Override: Tippfehler → Neue Methode statt Überschreibung → Bug!
Mit @Override: Compiler erkennt den Fehler sofort.
@Deprecated – Markiert veralteten Code
public class AlteAPI {
/**
* @deprecated Verwende stattdessen {@link #neueMethode()}
*/
@Deprecated(since = "2.0", forRemoval = true)
public void alteMethode() {
// Alter Code
}
public void neueMethode() {
// Neuer, besserer Code
}
}
// Verwendung:
AlteAPI api = new AlteAPI();
api.alteMethode(); // Compiler-Warnung!
Parameter:
since– Ab welcher Version deprecatedforRemoval– Wird in Zukunft entfernt?
@SuppressWarnings – Warnungen unterdrücken
@SuppressWarnings("unchecked")
public void unsichereOperation() {
List rawList = new ArrayList(); // Raw Type Warnung unterdrückt
rawList.add("text");
}
@SuppressWarnings({"deprecation", "unused"})
public void mehrereWarnungen() {
// Mehrere Warnungstypen unterdrückt
}
Häufige Werte:
"unchecked"– Unchecked Generics"deprecation"– Deprecated API"unused"– Unbenutzte Variablen"rawtypes"– Raw Types"serial"– Fehlende serialVersionUID
⚠️ Vorsicht: Warnungen zu unterdrücken ist oft ein Code-Smell. Besser: Problem beheben!
@FunctionalInterface – Für Lambda-Kompatibilität
@FunctionalInterface
public interface Berechnung {
int berechne(int a, int b);
// Default-Methoden erlaubt
default void info() {
System.out.println("Eine Berechnung");
}
// Statische Methoden erlaubt
static Berechnung addition() {
return (a, b) -> a + b;
}
// FEHLER wenn zweite abstrakte Methode:
// int andereMethode(); // Compiler Error!
}
@SafeVarargs – Für generische Varargs
public class VarargsDemo {
@SafeVarargs // Unterdrückt Heap Pollution Warnung
public static <T> List<T> asList(T... elements) {
return Arrays.asList(elements);
}
@SafeVarargs
public final <T> void verarbeite(T... items) {
for (T item : items) {
System.out.println(item);
}
}
}
🟡 PROFESSIONALS: Eigene Annotations
Annotation definieren
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME) // Wann verfügbar?
@Target(ElementType.METHOD) // Wo anwendbar?
@Documented // In Javadoc aufnehmen?
public @interface LogExecutionTime {
String value() default ""; // Parameter mit Default
boolean enabled() default true;
}
@Retention – Lebensdauer:
| Policy | Beschreibung |
|---|---|
SOURCE | Nur im Quellcode, nicht im Bytecode |
CLASS | Im Bytecode, nicht zur Laufzeit (Default) |
RUNTIME | Zur Laufzeit per Reflection lesbar |
@Target – Wo anwendbar:
| Element | Beschreibung |
|---|---|
TYPE | Klasse, Interface, Enum |
FIELD | Felder |
METHOD | Methoden |
PARAMETER | Methodenparameter |
CONSTRUCTOR | Konstruktoren |
LOCAL_VARIABLE | Lokale Variablen |
ANNOTATION_TYPE | Andere Annotations |
TYPE_USE | Überall wo Typen stehen (Java 8+) |
Annotation verwenden
public class BerichtService {
@LogExecutionTime("Bericht generieren")
public void generiereMonatsbericht() {
// Langsame Operation
}
@LogExecutionTime(value = "Export", enabled = false)
public void exportiereDaten() {
// Logging deaktiviert
}
}
Annotation per Reflection auslesen
public class AnnotationProcessor {
public static void verarbeite(Object obj) throws Exception {
Class<?> clazz = obj.getClass();
for (Method method : clazz.getDeclaredMethods()) {
if (method.isAnnotationPresent(LogExecutionTime.class)) {
LogExecutionTime annotation = method.getAnnotation(LogExecutionTime.class);
if (annotation.enabled()) {
System.out.println("Logging für: " + annotation.value());
long start = System.currentTimeMillis();
method.invoke(obj);
long duration = System.currentTimeMillis() - start;
System.out.println("Dauer: " + duration + "ms");
}
}
}
}
}
Praxis-Beispiel: Validierung
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface NotEmpty {
String message() default "Feld darf nicht leer sein";
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Range {
int min() default 0;
int max() default Integer.MAX_VALUE;
String message() default "Wert außerhalb des Bereichs";
}
// Verwendung:
public class Person {
@NotEmpty(message = "Name ist erforderlich")
private String name;
@Range(min = 0, max = 150, message = "Ungültiges Alter")
private int alter;
}
Teil 2: Multithreading Basics
🖼️ Thread-Lebenszyklus

Abbildung 2: Die Zustände eines Threads
🟢 GRUNDLAGEN: Threads erstellen
Methode 1: Thread-Klasse erweitern
public class MeinThread extends Thread {
private String name;
public MeinThread(String name) {
this.name = name;
}
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println(name + ": Zähle " + i);
try {
Thread.sleep(500); // 500ms pausieren
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
System.out.println(name + ": Fertig!");
}
}
// Verwendung:
MeinThread t1 = new MeinThread("Thread-A");
MeinThread t2 = new MeinThread("Thread-B");
t1.start(); // NICHT run() aufrufen!
t2.start();
Methode 2: Runnable implementieren (bevorzugt!)
public class MeineAufgabe implements Runnable {
private String name;
public MeineAufgabe(String name) {
this.name = name;
}
@Override
public void run() {
System.out.println(name + " läuft in: " + Thread.currentThread().getName());
}
}
// Verwendung:
Runnable aufgabe = new MeineAufgabe("Aufgabe-1");
Thread thread = new Thread(aufgabe);
thread.start();
// Oder mit Lambda:
Thread thread2 = new Thread(() -> {
System.out.println("Lambda-Thread läuft!");
});
thread2.start();
Warum Runnable bevorzugen?
- Klasse kann noch von anderer Klasse erben
- Bessere Trennung von Aufgabe und Ausführung
- Kompatibel mit ExecutorService
Thread-Methoden
Thread thread = new Thread(() -> {
// Aufgabe
});
// Thread starten
thread.start();
// Auf Thread warten
thread.join(); // Blockiert bis Thread fertig
thread.join(1000); // Max 1 Sekunde warten
// Thread-Informationen
thread.getName(); // "Thread-0"
thread.getId(); // 12
thread.getState(); // NEW, RUNNABLE, BLOCKED, WAITING, TERMINATED
thread.isAlive(); // true/false
thread.isDaemon(); // Daemon-Thread?
// Aktueller Thread
Thread current = Thread.currentThread();
System.out.println("Ich bin: " + current.getName());
// Thread pausieren
Thread.sleep(1000); // 1 Sekunde
// Thread unterbrechen (kooperativ!)
thread.interrupt();
Thread-Zustände
Thread thread = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
System.out.println(thread.getState()); // NEW
thread.start();
System.out.println(thread.getState()); // RUNNABLE
Thread.sleep(100);
System.out.println(thread.getState()); // TIMED_WAITING
thread.join();
System.out.println(thread.getState()); // TERMINATED
🟡 PROFESSIONALS: ExecutorService
Threads manuell zu erstellen ist wie jedes Mal ein neues Auto zu kaufen statt eines zu mieten. Thread-Pools sind effizienter!
ExecutorService Grundlagen
import java.util.concurrent.*;
// Thread-Pool mit fester Größe
ExecutorService executor = Executors.newFixedThreadPool(4);
// Aufgaben einreichen
executor.execute(() -> System.out.println("Aufgabe 1"));
executor.execute(() -> System.out.println("Aufgabe 2"));
executor.execute(() -> System.out.println("Aufgabe 3"));
// WICHTIG: Pool ordentlich beenden!
executor.shutdown(); // Keine neuen Aufgaben, bestehende abarbeiten
executor.awaitTermination(60, TimeUnit.SECONDS);
// Oder sofort stoppen:
// executor.shutdownNow();
Verschiedene Pool-Typen
// Feste Anzahl Threads ExecutorService fixed = Executors.newFixedThreadPool(4); // Dynamisch wachsender Pool ExecutorService cached = Executors.newCachedThreadPool(); // Ein einzelner Thread (Queue-Verarbeitung) ExecutorService single = Executors.newSingleThreadExecutor(); // Für zeitgesteuerte Aufgaben ScheduledExecutorService scheduled = Executors.newScheduledThreadPool(2); // Virtual Threads (Java 21+) ExecutorService virtual = Executors.newVirtualThreadPerTaskExecutor();
Callable und Future – Mit Rückgabewert
// Callable = Runnable mit Rückgabewert
Callable<Integer> berechnung = () -> {
Thread.sleep(1000);
return 42;
};
ExecutorService executor = Executors.newFixedThreadPool(2);
// Future = Versprechen auf ein Ergebnis
Future<Integer> future = executor.submit(berechnung);
// Andere Arbeit machen...
System.out.println("Warte auf Ergebnis...");
// Ergebnis abholen (blockiert!)
Integer ergebnis = future.get(); // Wartet bis fertig
System.out.println("Ergebnis: " + ergebnis);
// Mit Timeout:
Integer ergebnis2 = future.get(5, TimeUnit.SECONDS);
// Status prüfen:
future.isDone(); // Fertig?
future.isCancelled(); // Abgebrochen?
future.cancel(true); // Abbrechen (true = interrupt)
executor.shutdown();
Mehrere Futures verarbeiten
ExecutorService executor = Executors.newFixedThreadPool(4);
List<Callable<String>> aufgaben = List.of(
() -> { Thread.sleep(1000); return "Ergebnis A"; },
() -> { Thread.sleep(500); return "Ergebnis B"; },
() -> { Thread.sleep(1500); return "Ergebnis C"; }
);
// Alle ausführen und auf alle warten
List<Future<String>> futures = executor.invokeAll(aufgaben);
for (Future<String> f : futures) {
System.out.println(f.get());
}
// Oder: Erstes Ergebnis nehmen
String erstesErgebnis = executor.invokeAny(aufgaben);
System.out.println("Erstes: " + erstesErgebnis);
executor.shutdown();
Scheduled Executor – Zeitgesteuert
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
// Einmalig nach Verzögerung
scheduler.schedule(
() -> System.out.println("Nach 2 Sekunden!"),
2, TimeUnit.SECONDS
);
// Periodisch (feste Rate)
scheduler.scheduleAtFixedRate(
() -> System.out.println("Alle 3 Sekunden"),
0, // Initial delay
3, // Period
TimeUnit.SECONDS
);
// Periodisch (feste Verzögerung zwischen Ende und Start)
scheduler.scheduleWithFixedDelay(
() -> System.out.println("3 Sekunden nach Ende"),
0, // Initial delay
3, // Delay
TimeUnit.SECONDS
);
// Nach 10 Sekunden stoppen
scheduler.schedule(() -> scheduler.shutdown(), 10, TimeUnit.SECONDS);
🔵 BONUS: Virtual Threads (Java 21+)
// Klassischer Thread (OS-Thread, teuer)
Thread traditional = new Thread(() -> doWork());
traditional.start();
// Virtual Thread (leichtgewichtig, günstig)
Thread virtual = Thread.ofVirtual().start(() -> doWork());
// Oder mit Builder
Thread.Builder builder = Thread.ofVirtual().name("worker-", 0);
Thread vt1 = builder.start(() -> task1());
Thread vt2 = builder.start(() -> task2());
// ExecutorService für Virtual Threads
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 10_000).forEach(i -> {
executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1));
return i;
});
});
} // Auto-shutdown durch try-with-resources
Wann Virtual Threads?
- I/O-lastige Aufgaben (HTTP-Requests, DB-Queries)
- Viele gleichzeitige, kurze Aufgaben
- NICHT für CPU-intensive Berechnungen
💬 Real Talk: Thread-Fallen vermeiden
Java Fleet Büro, Freitag 15:00. Nova hat Probleme.
Nova: „Elyndra, mein Thread macht nichts! Ich rufe run() auf und es passiert nichts Paralleles.“
Thread t = new Thread(() -> System.out.println("Parallel?"));
t.run(); // FALSCH!
Elyndra: „Klassiker! run() führt den Code im aktuellen Thread aus. Du musst start() aufrufen!“
t.start(); // RICHTIG - startet neuen Thread
Jamal: „Und vergiss nicht: start() kann nur einmal aufgerufen werden!“
t.start(); t.start(); // IllegalThreadStateException!
Nova: „Okay, und warum hängt mein Programm manchmal?“
Elyndra: „Zeig mal…“
Future<String> future = executor.submit(() -> longOperation()); String result = future.get(); // Blockiert für immer wenn Operation hängt!
Elyndra: „Immer mit Timeout arbeiten!“
try {
String result = future.get(30, TimeUnit.SECONDS);
} catch (TimeoutException e) {
future.cancel(true);
// Fallback
}
Jamal: „Und NIEMALS den ExecutorService vergessen zu shutdown-en!“
// ❌ MEMORY LEAK!
ExecutorService executor = Executors.newFixedThreadPool(4);
executor.submit(task);
// Programm läuft ewig weiter...
// ✅ RICHTIG
ExecutorService executor = Executors.newFixedThreadPool(4);
try {
executor.submit(task);
} finally {
executor.shutdown();
}
// ✅ Oder mit try-with-resources (Java 19+)
try (var executor = Executors.newFixedThreadPool(4)) {
executor.submit(task);
}
❓ FAQ
Frage 1: Thread vs. Runnable – was nehmen?
Runnable ist fast immer besser:
- Klasse kann noch von anderer Klasse erben
- Kompatibel mit ExecutorService
- Bessere Testbarkeit
Thread-Klasse erweitern nur wenn du wirklich Thread-Verhalten überschreiben musst.
Frage 2: Wie viele Threads sollte ich verwenden?
Faustregel:
- CPU-intensive Aufgaben:
Runtime.getRuntime().availableProcessors() - I/O-intensive Aufgaben: Mehr Threads möglich (2x bis 10x CPUs)
int cpus = Runtime.getRuntime().availableProcessors(); ExecutorService executor = Executors.newFixedThreadPool(cpus);
Frage 3: Wann Annotation-Processing vs. Reflection?
| Annotation Processing | Reflection |
|---|---|
| Compile-Zeit | Laufzeit |
| Keine Performance-Kosten | Langsamer |
| Generiert Code | Liest Metadaten |
| Lombok, MapStruct | Spring, JUnit |
Frage 4: Was ist Thread-Safety?
Ein Code ist thread-safe wenn er von mehreren Threads gleichzeitig verwendet werden kann ohne Bugs zu verursachen.
// NICHT thread-safe:
private int counter = 0;
public void increment() { counter++; }
// Thread-safe:
private AtomicInteger counter = new AtomicInteger(0);
public void increment() { counter.incrementAndGet(); }
Mehr dazu in Tag 9: Synchronisation!
Frage 5: Bernd sagt, Multithreading sei „unnötig kompliziert“?
seufz Ja, es ist komplex. Aber:
- Moderne CPUs haben viele Kerne
- Ohne Threads → nur ein Kern wird genutzt
- Server müssen viele Requests gleichzeitig verarbeiten
- UI muss reaktiv bleiben während Hintergrundarbeit läuft
🔍 „behind the code“ oder „in my feels“? Die echten Geschichten findest du, wenn du weißt wo du suchen musst…
🎁 Cheat Sheet
🟢 Annotations
@Override // Methode überschreiben
@Deprecated // Veraltet markieren
@SuppressWarnings // Warnungen unterdrücken
@FunctionalInterface // Für Lambdas
// Eigene Annotation:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MeineAnnotation {
String value() default "";
}
🟡 Threads erstellen
// Mit Runnable (bevorzugt) Thread t = new Thread(() -> doWork()); t.start(); // Thread-Methoden t.join(); // Warten t.interrupt(); // Unterbrechen Thread.sleep(1000); // Pausieren Thread.currentThread() // Aktueller Thread
🔵 ExecutorService
// Pool erstellen ExecutorService exec = Executors.newFixedThreadPool(4); // Aufgaben einreichen exec.execute(runnable); // Ohne Rückgabe Future<T> f = exec.submit(callable); // Mit Rückgabe // Ergebnis holen T result = f.get(); // Blockiert T result = f.get(5, TimeUnit.SECONDS); // Mit Timeout // Pool beenden exec.shutdown(); exec.awaitTermination(60, TimeUnit.SECONDS);
🎨 Challenge für dich!
🟢 Level 1 – Einsteiger
- [ ] Erstelle eine
@AuthorAnnotation mit name und date - [ ] Starte 3 Threads die jeweils von 1-10 zählen
- [ ] Verwende Thread.sleep() um Verzögerungen einzubauen
Geschätzte Zeit: 30-40 Minuten
🟡 Level 2 – Fortgeschritten
- [ ] Erstelle eine
@ValidateAnnotation und einen Validator - [ ] Implementiere einen Thread-Pool der 10 Aufgaben verarbeitet
- [ ] Nutze Callable/Future um Ergebnisse zu sammeln
Geschätzte Zeit: 45-60 Minuten
🔵 Level 3 – Profi
- [ ] Baue einen einfachen Annotation-Processor für Logging
- [ ] Implementiere einen Producer-Consumer mit ExecutorService
- [ ] Vergleiche Performance: Sequential vs. Parallel
Geschätzte Zeit: 60-90 Minuten
📦 Downloads
| Projekt | Für wen? | Download |
|---|---|---|
| tag08-annotations-threads-starter.zip | 🟢 Mit TODOs | ⬇️ Download |
| tag08-annotations-threads-complete.zip | 🟡 Musterlösung | ⬇️ Download |
🔗 Weiterführende Links
🇩🇪 Deutsch
| Ressource | Beschreibung |
|---|---|
| Rheinwerk: Annotations | Umfassendes Kapitel |
🇬🇧 Englisch
| Ressource | Beschreibung | Level |
|---|---|---|
| Oracle: Annotations | Offizielle Doku | 🟢 |
| Oracle: Concurrency | Thread-Tutorial | 🟡 |
| Baeldung: ExecutorService | Praxisbeispiele | 🟡 |
👋 Geschafft! 🎉
Was du heute gelernt hast:
✅ Built-in Annotations verstehen und nutzen
✅ Eigene Annotations erstellen und per Reflection auslesen
✅ Threads erstellen mit Thread und Runnable
✅ Thread-Lebenszyklus und wichtige Methoden
✅ ExecutorService für Thread-Pools
✅ Callable und Future für Rückgabewerte
Fragen? elyndra.valen@java-developer.online
📖 Weiter geht’s!
← Vorheriger Tag: Tag 7: File I/O
→ Nächster Tag: Tag 9: Multithreading – Synchronisation
Tags: #Java #Annotations #Multithreading #Threads #ExecutorService #Concurrency #Tutorial
📚 Das könnte dich auch interessieren
© 2025 Java Fleet Systems Consulting | java-developer.online

