Java Standard: Threads
Einleitung
[Bearbeiten]Nebenläufigkeit (concurrency) ist die Fähigkeit eines Systems, zwei oder auch mehrere Aufgaben (scheinbar) gleichzeitig auszuführen. In Java kann die Ausführungsparallelität innerhalb eines Programmes mittels Threads (lightweight processes) erzwungen werden. Laufen mehrere Threads parallel, so spricht man auch von Multithreading.
Threads erzeugen und starten
[Bearbeiten]Threads sind Bestandteil des Java-Standardpackages java.lang.
Methode 1: Die Thread-Klasse
[Bearbeiten]Die Klasse Thread
implementiert die Schnittstelle Runnable
.
Prinzipielle Vorgehensweise:
- Eine Klasse, abgeleitet von
Thread
, erstellen - Die
Thread
-Methodepublic void run ()
überschreiben - Instanz(en) der
Thread
-Subklasse bilden - Die
Thread
-Instanz(en) mittelspublic void start()
starten
Beispiel:
public class SimpleThread extends Thread
{
public void run()
{
for (int i = 0; i<=100; i++)
{
System.out.println(getName() + ": " + i);
try
{
sleep(50);
}
catch(InterruptedException ie)
{
// ...
}
}
}
}
public class App
{
public static void main(String[] args)
{
SimpleThread thread1 = new SimpleThread();
SimpleThread thread2 = new SimpleThread();
thread1.start();
thread2.start();
}
}
oder
public class SimpleThread extends Thread
{
public SimpleThread(String id)
{
start();
}
public void run()
{
for (int i = 0; i<=100; i++)
{
System.out.println(getName() + ": " + i);
try
{
sleep(50);
}
catch(InterruptedException ie)
{
// ...
}
}
}
}
public class App
{
public static void main(String[] args)
{
new SimpleThread("1");
new SimpleThread("2");
}
}
Methode 2: Das Runnable-Interface
[Bearbeiten]Prinzipielle Vorgehensweise:
- Eine
Runnable
implementierende Klasse erstellen - Die
Runnable
-Methodepublic void run ()
überschreiben - Instanz(en) der
Runnable
implementierenden Klasse bilden Thread
-Instanz(en) erstellen. Als Parameter wird eineRunnable
-Instanz übergeben.- Die
Thread
-Instanz(en) mittelspublic void start()
starten
public class SimpleRunnable implements Runnable
{
public void run()
{
for (int i = 0; i<=100; i++)
{
System.out.println(Thread.currentThread().getName() + ": " + i);
try
{
Thread.sleep(50);
}
catch(InterruptedException ie)
{
// ...
}
}
}
}
public class App
{
public static void main(String[] args)
{
Runnable r1 = new SimpleRunnable();
Runnable r2 = new SimpleRunnable();
new Thread(r1).start();
new Thread(r2).start();
}
}
Der main-Thread
[Bearbeiten]Jede Java-Applikation besitzt zumindest einen Thread, den main-Thread.
public class App
{
public static void main(String[] args)
{
Thread t = Thread.currentThread();
System.out.println("Name = " + t.getName());
System.out.println("Id = " + t.getId());
System.out.println("Priorität = " + t.getPriority());
System.out.println("Zustand = " + t.getState());
}
}
zeigt
Name = main Id = 1 Priorität = 5 Zustand = RUNNABLE
Thread-Zustände
[Bearbeiten]Threads können in verschiedenen Zuständen vorliegen:
Thread.State | Erläuterung |
---|---|
NEW |
erzeugt, aber noch nicht gestartet |
RUNNABLE |
lauffähig; wird in der Java Virtual Machine (JVM) ausgeführt oder wartet auf die Freigabe von Ressourcen |
BLOCKED |
geblockt; wartet auf einen Monitor-Lock (siehe Synchronisation) |
WAITING |
wartet; das ist der Fall wenn eine der folgenden Methoden aufgerufen wurde:
|
TIME_WAITING |
wartet eine definierte Zeitspanne; das ist der Fall wenn eine der folgenden Methoden aufgerufen wurde:
|
TERMINATED |
beendet; eine einmal beendete Thread-Instanz kann nicht mehr erneut gestartet werden |
Abfragen kann man den Thread-Zustand mit der bereits in vorhergehenden Abschnitt verwendeten Funktion
Thread.State.getState()
.
Threads beenden
[Bearbeiten]Das stop-Chainsaw Massacre
[Bearbeiten]Die Klasse Thread implementiert die Funktion void stop() zur manuellen Beendigung eines Threads. Diese Funktion ist problembehaftet, daher als deprecated gekennzeichnet und soll nicht benutzt werden.
Der run-Suizid
[Bearbeiten]Implementiert man in der run()-Methode keine Endlosschleife, dann löst sich das Problem durch Zeitablauf von selbst.
public class SimpleThread extends Thread
{
public void run()
{
for(int i = 0; i<=100; i++)
{
System.out.println(getState());
}
}
}
public class App
{
public static void main(String[] args)
{
SimpleThread t = new SimpleThread();
t.start();
try
{
Thread.sleep(1000);
}
catch(InterruptedException ie)
{
}
System.out.println(t.getState());
}
}
ergibt
... RUNNABLE RUNNABLE TERMINATED
Flag-Methode
[Bearbeiten]public class SimpleThread extends Thread
{
volatile boolean running = false; // Flag
public void stopIt()
{
running = false;
}
public void run()
{
running = true;
while(running == true)
{
System.out.println(getState());
}
}
}
public class App
{
public static void main(String[] args)
{
SimpleThread t = new SimpleThread();
t.start();
try
{
Thread.sleep(1000);
}
catch(InterruptedException ie)
{
// ...
}
t.stopIt();
}
}
Referenz-Methode
[Bearbeiten]Eine Variante der Flag-Methode ist hier angegeben. Sie benutzt kein Extra-Flag sondern stattdessen die Referenz zum Thread.
public class Applet implements Runnable
{
Thread thread;
public void start()
{
thread = new Thread(this);
thread.start();
}
public void stop()
{
thread = null;
}
public void run()
{
Thread myThread = Thread.currentThread();
while (thread == myThread)
{
// tu etwas
}
}
}
Interrupt
[Bearbeiten]Mit Hilfe der Thread-Methoden
void interrupt()
und
boolean isInterrupted()
können wir auch einen Thread beenden.
public class SimpleThread extends Thread
{
public void run()
{
while(isInterrupted() == false)
{
System.out.println(getState());
}
}
}
public class App
{
public static void main(String[] args)
{
Thread t = new SimpleThread();
t.start();
try
{
Thread.sleep(1000);
}
catch(InterruptedException ie)
{
// ...
}
t.interrupt();
}
}
Threads anhalten
[Bearbeiten]Ein Thread kann mit den Funktionen
static void sleep(long millis)
static void sleep(long millis, int nanos)
für millis Millisekunden (+ nanos Nanosekunden) angehalten werden. Diese Funktion kennen wir schon aus früheren Codebeispielen.
Zusätzlich kann anderen wartenden Threads der Vortritt gewährt werden.
static void yield()
zum temporären Pausieren des momentan ausgeführten Threads, um andere Threads ausführen zu können.
public class SimpleThread extends Thread
{
public void run()
{
for(int i = 0; i<=1000; i++)
{
System.out.println(getName() + ": " + i);
if(i%2 == 0)
{
yield();
}
}
}
}
public class App
{
public static void main(String[] args)
{
new SimpleThread().start();
new SimpleThread().start();
new SimpleThread().start();
}
}
ergibt
... Thread 0: 9 Thread 0: 10 Thread 1: 9 Thread 1: 10 Thread 2: 9 Thread 2: 10 ...
Auf das Ende eines Threads warten
[Bearbeiten]void join()
Warte auf das Ende eines Threads
void join(long millis)
void join(long millis, int nanos)
Warte längstens millis Millisekunden (+ nanos Nanosekunden) auf das Ende eines Threads. Übergibt man als Parameter 0, so bedeutet dies, dass beliebig lange auf das Ende des Threads gewartet wird.
public class SimpleThread extends Thread
{
public void run()
{
for(int i = 0; i<=1000; i++)
{
System.out.println(getState());
}
}
}
public class App
{
public static void main(String[] args)
{
SimpleThread t = new SimpleThread();
t.start();
try
{
t.join();
}
catch(InterruptedException ie)
{
// ..
}
System.out.println(t.getState());
}
}
ergibt
... RUNNABLE RUNNABLE TERMINATED
Thread-Priorität
[Bearbeiten]Durch Anwendung der Thread-Methode
void setPriority(int newPriority)
kann die Priorität eines Threads im Bereich von 1 (Thread.MIN_PRIORITY) bis 10 (Thread.MAX_PRIORITY) geändert werden. Ein Thread erhält zuerst immer die Priorität des Threads, in dem er gestartet wurde. Der main-Thread weist standardmäßig die Priorität 5 auf. Die konkrete Umsetzung der zugewiesenen Priorität hängt dabei sehr stark vom jeweiligen Betriebssystem ab.
Die Abfrage der momentanen Thread-Priorität geschieht mittels der Thread-Methode
int getPriority()
Scheduler
[Bearbeiten]Die Ausführungsplanung zum Umschalten zwischen aktiven Threads und Prozessen nennt man Scheduling. Mögliche Scheduling-Strategien:
- Prioritätssteuerung (Preemption): Es wird immer der Thread mit der höchsten Priorität ausgeführt
- Zeitsteuerung (Time-Slicing): Der Scheduler weist den einzelnen Threads Zeitabschnitte zu, während der sie zur Ausführung gelangen
- Prioritäts- und Zeitsteuerung kombiniert
Dämonen
[Bearbeiten]Ein Dämon ist ein Thread der im Hintergrund ausgeführt wird. Mit der Thread-Methode
void setDaemon(boolean on)
kann zwischen den Thread-Typen Dämon-Thread (on = true) und konventionell im Vordergrund laufendem Thread (off = false) umgeschaltet werden.
Abfragen kann man den Thread-Typ mittels
boolean isDaemon()
Nicht jeder Thread eignet sich zum Dämon-Thread. Es gilt folgende Regel: Eine Java-VM beendet sich, wenn keine Nicht-Dämon-Threads mehr laufen.
Ein prominenter Dämon ist übrigens der Garbage Collector - es würde auch wenig Sinn ergeben, wenn er weiter arbeiten würde, nachdem ein Programm zu Ende ist.
Threadgruppen
[Bearbeiten]Threads kann man auch gruppieren. Beim Start einer Applikation wird automatische eine main-Threadgruppe angelegt. Der main-Thread ist Teil dieser Gruppe. All Threads sind automatisch Bestandteil einer Threadgruppe.
public class SimpleThread extends Thread
{
SimpleThread(ThreadGroup tg, String name)
{
super(tg, name);
}
public void run()
{
try
{
sleep(5000);
}
catch(InterruptedException ie)
{
// ...
}
}
}
public class App
{
public static void main(String[] args)
{
ThreadGroup tg = new ThreadGroup("Testgruppe");
Thread t1 = new SimpleThread(tg, "t1");
Thread t2 = new SimpleThread(tg, "t2");
t1.start();
t2.start();
Thread array[] = new Thread[tg.activeCount()];
tg.enumerate(array);
for(Thread t: array)
{
System.out.println(t.getName() + " ist Gruppenmitglied von " + tg.getName() );
}
}
}
Threads synchronisieren
[Bearbeiten]Race Conditions
[Bearbeiten]Definition (aus Wikipedia, der freien Enzyklopädie vom 27.08.2005):
Als Race Condition (zu deutsch Wettlaufsituation oder Wettkampfbedingung) bezeichnen Programmierer Konstellationen, in denen das Ergebnis einer Operation vom zeitlichen Verhalten bestimmter Einzeloperationen abhängt. Unbeabsichtigte Race Conditions sind ein häufiger Grund für schwer auffindbare Programmfehler, so genannte Heisenbugs. ...
Genau solche Race Conditions können auch bei Threads vorkommen. Greifen mehrere Threads auf bestimmte Ressourcen (Dateien, Variablen, Datenbanken, Drucker, etc.) zu, so kann sich der Programmierer nicht darauf verlassen, dass die Threads dies immer in einer bestimmten Reihenfolge und kollisionsfrei tun. Das hängt auch von Faktoren ab, die der Programmierer nicht beeinflussen kann, zum Beispiel der Scheduling-Strategie oder der konkreten Umsetzung der gewünschten Thread-Priorität.
Deshalb muss eine Programmiersprache Mechanismen bereitstellen, um derartige Probleme zu lösen. Eine Methode wird als thread-sicher bezeichnet, wenn sie bedenkenlos von Threads aufgerufen werden kann.
Atomare Operationen
[Bearbeiten]Definition (aus Wikipedia, der freien Enzyklopädie vom 27.08.2005):
Eine Atomare Operation [...] bezeichnet eine Operation im Computer, welche durch keine andere Operation unterbrochen werden kann. Atomare Operationen sind wichtig beim Synchronisieren von Daten. ...
Auch diesen Aspekt muss man beim Programmieren mit Threads beachten (Stichwort: Transaktionen bei Datenbanken).
Selbst bei einfachen Variablen kann man sich nicht auf atomares Verhalten verlassen. Um sicherzustellen, dass Objekt- oder Klassenvariablen vor jedem Zugriff auf den aktuellen Stand gebracht werden verwendet man das Schlüsselwort volatile (flüchtig, launisch, unbeständig).
volatile long l;
Das Schlüsselwort synchronized
[Bearbeiten]Greifen mehrere Threads auf dieselben Ressourcen zu, so kann es zwecks Problemvermeidung notwendig sein Threads zu synchronisieren.
Eine Methode können wir durch das Schlüsselwort synchronized kennzeichnen
synchronized void xxx()
{
// ...
}
Die VM wird diese Methode nun bei Bedarf automatisch sperren und entsperren.
Auch einzelne Code-Blöcke können synchronisiert werden
synchronized(objekt)
{
// ...
}
Monitore
[Bearbeiten]Die JVM definiert für Synchronisationszwecke sogenannte Monitore. Jedes Objekt mit synchronisiertem Code ist in Java ein Monitor. Dieser Monitor besitzt einen Monitor-Lock (Lock, Sperre) und führt eine Warteliste von Threads die ausgesperrt wurden. Beendet ein Thread eine synchronized-Methode oder einen synchronized-Block, so wird die Sperre aufgehoben und der nächste Thread kommt zum Zug. Der Aufruf von sleep() oder yield() hebt eine solche Sperre allerdings nicht auf.
Deadlocks
[Bearbeiten]Ein Problem bei Synchronisation können sogenannte Deadlocks (Verklemmungen) darstellen. Dabei sperren sich zwei oder mehrere Threads gleichzeitig von benötigten Ressourcen aus. Thread A wartet darauf, dass Thread B eine Ressource freigibt. Gleichzeitig wartet aber Thread B, dass Thread A seine gesperrte Ressource freigibt. Setzt man im Vorfeld keine geeigneten Maßnahmen, dann werden die beiden Threads ewig warten und mit den Threads auch der genervte und ratlose Programmbenutzer.
Zur Erkennung von Deadlock-Situationen können Deadlock-Detection-Utilities hilfreich sein. Aktuelle Java-Releases und auch IDEs wie zum Beispiel Eclipse 3.1 bieten derartige Möglichkeiten.
Das wait-notify-Konzept
[Bearbeiten]Mit Hilfe der Object-Methode
public final void wait()
können Threads in einen Wartezustand versetzt werden. Sie geben dann den Monitor frei.
Besitzt ein Thread den Monitor eines Objektes, so kann er durch
void notify()
oder
void notifyAll()
wartende Threads benachrichtigen und aus dem Wartezustand erlösen.
Concurrent-Programming ab Java 5.0
[Bearbeiten]Ab Java 5.0 werden in den Packages
- java.util.concurrent
- java.util.concurrent.atomic
- java.util.concurrent.locks
zusätzlich zur konventionellen Thread-Programmierung weitere Möglichkeiten für nebenläufiges Programmieren bereitgestellt. Nachfolgend werden einführend in diese Thematik ganz kurz ein paar Klassen und Möglichkeiten dieser Pakete angesprochen.
Callable
[Bearbeiten]Das Interface Callable<> dient ähnlichen Zwecken wie das Interface Runnable, ist aber ein bisschen flexibler. Außerdem ist anstelle der run-Methode die call-Methode zu implementieren .
import java.util.concurrent.*;
public class CallableThread implements Callable<Integer>
{
private int i;
CallableThread(int i)
{
this.i = i;
}
public Integer call()
{
for (int j=0; j<=1000; j++)
{
System.out.println("Thread " + i + ":" + j);
}
return i;
}
}
Executors
[Bearbeiten]Die Klasse Executors enthält Fabriks- und Hilfsmethoden für
- Callable
- Executor
- ExecutorService
- ScheduledExecutorService
- ThreadFactory
Future und FutureTask
[Bearbeiten]Die Schnittstelle Future<> implementiert Runnable. Die Klasse FutureTask<> implementiert Future<>.
Thread-Pools
[Bearbeiten]Zwecks optimaler Performance kann die Kreierung von Thread-Pools sinnvoll sein. Thread-Pools fassen Threads zu gemanagten Kollektionen zusammen.
import java.util.concurrent.*;
public class App
{
public static void main(String[] args)
{
ExecutorService es = Executors.newCachedThreadPool();
FutureTask<Integer> f1 = new FutureTask<Integer>(new CallableThread(1));
FutureTask<Integer> f2 = new FutureTask<Integer>(new CallableThread(2));
es.execute(f1);
es.execute(f2);
}
}
Zeitgesteuerte Task-Ausführung
[Bearbeiten]TimerTask implementiert Runnable und kann ein- oder mehrmalig durch einen Timer ausgeführt werden.
import java.util.*;
public class Task extends TimerTask
{
public void run()
{
System.out.println("Hallo!");
}
}
public class App
{
public static void main(String[] args)
{
Timer timer = new Timer();
timer.schedule(new Task(), 1000, 2000);
}
}
Die schedule()-Methode gibt es mit unterschiedlichen Signaturen. Im Beispiel wurde eine Initialverzögerung (delay) von 1000ms und eine Wiederholung (period) alle 2000ms gewählt.