Praktikum aus Softwareentwicklung 2, Stunde 5 Lehrziele/Inhalt 1. Threads Threads Threads sind parallele, oder auf Rechnern mit nur einer CPU quasi-parallele, Programmabläufe in Java. Sie können beispielsweise benutzt werden, um mehrere Anforderungen auf einem Server abzuarbeiten, Hintergrundtätigkeiten wie Animationen durchzuführen oder lang-laufende Aufgaben vom GUI-Thread zu entkoppeln. Basisklassen Java bietet folgende Basisklassen zum Umgang mit Threads: java.lang.thread Thread-Objekte bilden Threads ab; und bieten programmatischen Zugriff darauf, zb: starten (start), unterbrechen (interrupt), abgeben der Kontrolle (yield) und setzen der Priorität (setpriority). Hat statische Hilfsmethoden, mit denen man auf den aktuellen Thread zugreifen kann. Auf Betriebssystemen die Threads unterstützen werden diese genutzt. java.lang.runable Aufgaben die in einem Thread ausgeführt werden sollen müssen in Objekte gekapselt werden. Diese Objekte müssen das Interface Runnable implementieren. Pro Thread kann eine Aufgabe im Konstruktor übergeben werden. java.lang.object Implementiert einen Monitor, d.h. jedes Objekt kann zur Thread-Synchronisation genutzt werden. java.lang. InterruptedException Wird geworfen wenn ein Thread schläft oder wartet und von außen unterbrochen wird. Anlegen eines Threads Die Klasse Thread verwaltet Threads in Java. Will man einen Thread in Java starten muss man ein Objekt dieser Klasse anlegen und darauf die Methode start aufrufen. Ein Thread-Objekt kann nur einmal gestartet werden, sobald er seine Aufgabe abgearbeitet hat ist er tot und kann nicht mehr verwendet werden. Das Interface Runnable ist die Schnittstelle für Aufgaben. Runnable enthält nur die Methode void run(). Benötigt man Parameter oder einen Rückgabewert, dann muss man diese als Felder im Objekt ablegen. Markus Löberbauer 2010 Seite 12
Beispiel: Anlegen eines Threads der die Zahlen von 1 bis 100 ausgibt. Definieren der Aufgabe als Runnable: public class CounterTask implements Runnable { for (int i = 1; i <= 100; ++i) { System.out.println(i); Anlegen und starten des Threads: Thread counterthread = new Thread(new CounterTask()); counterthread.start(); Die Klasse Thread kann auch erweitert werden, wenn man eine spezielle Art von Threads braucht, zb Threads die Zeitmessungen machen oder Threads die Ereignisse auslösen. Diese Erweiterbarkeit kann auch verwendet werden, um einen Thread mit einer Aufgabe zu versehen. Allerdings ist diese Art der Erweiterung im objektorientierten Sinn falsch. Und aus diesem Grund in anderen Programmiersprachen, wie beispielsweise C#, unmöglich. Negativ-Beispiel: Anlegen einen Threads der die Zahlen von 1 bis 100 ausgibt, als Thread-Ableitung. public class CounterThread extends Thread { for (int i = 1; i <= 100; ++i) { System.out.println(i); CounterThread counterthread = new CounterThread(); counterthread.start(); Unterbrechen eines Threads Es gibt Threads die ihre Aufgabe so lange ausführen bis sie von außen unterbrochen werden. Zum Beispiel Server-Threads die Client-Anfrage abarbeiten. Einen Thread kann man zuverlässig und sicher abbrechen lassen, indem man in der Verarbeitungs-Schleife Thread.interrupted() prüft oder ein als volatile markiertes Feld ausliest. Reagiert der Thread auf Thread.interrupted(), dann kann der Thread von außen über die Methode interrupt beendet werden. Liest der Thread ein volatile Feld aus, dann kann man von außen auf dieses Feld schreiben um den Thread zu beenden. Es ist auch möglichen einen Thread über die Methode stop zu beenden. Dabei wird der Thread allerdings ohne Vorwarnung gestoppt, ohne die Möglichkeit zu haben begonnene Aufgaben abzuschließen, was zu inkonsistenten Datenmodellen führt. Korrekter Umgang mit Thread.interrupted(): public class Exiter implements Runnable Markus Löberbauer 2010 Seite 13
while(!thread.interrupted()) { // Endless loop oder, falls in der Endlosschleife eine InterruptedException auftreten kann public class Exiter implements Runnable while (!Thread.interrupted()) { try { // do something sleep(1000); // may throw an InterruptedException catch (InterruptedException e) { // Call interrupt() to set interrupted() interrupt(); // finish work Korrekter Umgang mit einem volatile Feld: volatile boolean exit; private class Exiter implements Runnable { while (!exit) { // Endless loop Synchronisation In Java nutzen alle Threads einen gemeinsamen Speicherbereich, bei gemeinsam genutzten Objekten muss der Zugriff daher synchronisiert werden. Synchronisation kann auf Methoden- und Block-Ebene erfolgen. Synchronisiert man auf Blockebene, dann muss explizit ein Objekt angeben werden auf das synchronisiert werden soll. Synchronisiert man auf Methodeneben wird das this-objekt benutzt. Handelt es sich um eine statische Methode wird das Klassen-Objekt benutzt. Synchronisation auf Blockebene ist flexibler, weil man bestimmen kann welches Objekt zur Synchronisation benutzt werden soll; und sie ist sicherer, weil man das Synchronisationsobjekt lokal halten kann. Synchronisierter Block Object obj = new Object(); void foo() { // uncritical stuff synchronized(obj) { // uncritical stuff Synchronisierte Methode synchronized void bar() { // equivalent to void bar() { synchronized(this) { Markus Löberbauer 2010 Seite 14
Bedingtes Warten Muss in einem Thread auf eine Bedingung gewartet werden bevor weiter gearbeitet werden kann, muss man mit dem Monitor arbeiten. Threads können auf einen Monitor warten und wartende Threads benachrichtigen. Jedes Objekt in Java ist ein Monitor, dazu sind in Objekt die Methoden wait, wait(timeout), wait(timeout, nanos), notify und notifyall vorhanden. Die Methode wait blockiert den Thread bis er über den Monitor notifiziert wird; oder der Thread mit interrupt unterbrochen wird. Möchte man maximal nur eine gewisse Zeit warten kann man die Methode wait(timeout) oder wait(timeout, nanos) benutzen. Die Methode notify benachrichtigt einen Thread der auf den Monitor wartet, die Auswahl des Threads erfolgt zufällig. Mit der Methode notifyall werden alle wartenden Threads benachrichtigt. Beispiel: Überweisen eines Geldbetrags. Wobei am Quellkonto genug Geld vorhanden sein muss. public class Bank { private Object lock = new Object(); private Account[] accounts; //... public void transfer(int from, int to, int amount) throws InterruptedException { synchronized(lock) { while (accounts[from] < amount) { lock.wait(); accounts[from] -= amount; accounts[to] += amount; lock.notifyall(); In diesem Beispiel sieht man warum notifyall wichtig ist. Bevor von einem Konto etwas abgebucht werden kann muss genügend Geld vorhanden sein. Das bedeutet eine Überweisung ist eventuell von einer anderen Überweisung abhängig. Würde man hier nur notify verwenden könnten die Threads in eine Blockierung geraten. Mit notifyall haben alle Threads die Möglichkeit ihre Bedingung zu prüfen. Warten auf einen Thread Teilt man eine Aufgabe auf mehrere Threads auf, dann muss man, spätestens sobald man das Ergebnis braucht, warten bis alle Threads fertig sind. Dazu kann man am Thread die Methode join aufrufen. Beispiel: // start an extra thread Thread t = new Thread(...); t.start(); // concurrent execution t.join(); // thread t is dead Markus Löberbauer 2010 Seite 15
IO-Opertion run terminiert Zustände eines Threads neu: erzeugt aber noch nicht gestartet lauffähig o aktiv: wird gerade ausgeführt o bereit: kann ausgeführt werden und wartet auf Zuteilung des Prozessors blockiert o schlafend: mit sleep schlafen gelegt o IO-blockiert: wartet auf Beendigung einer IO-Operation o wartend: wurde mit wait in den wartenden Zustand versetzt o gesperrt: Wartet auf die Aufhebung einer Objekt-Sperre o suspendiert: durch suspend() vorübergehend blockiert Achtung: ist veraltet und sollte nicht verwendet werden tot: run()-methode ausgelaufen blockiert (blocked) schlafend (sleeping) IO-blockiert (IO-blocked) gesperrt (locked) wartend (waiting) suspendiert (suspended) sleep() aufwachen Ende IO-Operation Aufheben Objektsperre Objektsperre (synchronized) wait-anweisung notify / notifyall suspend() resume() aktiv (active) bereit (ready) lauffähig (runnable) start() neu (new) tot (dead) Markus Löberbauer 2010 Seite 16