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 { public void run() { 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 { public void run() { 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 public void run() { © 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 public void run() { 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 { public void run() { 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 Synchronisierte Methode Object obj = new Object(); synchronized void bar() { // do critical stuff here } void foo() { // uncritical stuff synchronized(obj) { // do critical stuff here } // uncritical stuff } © Markus Löberbauer 2010 // equivalent to void bar() { synchronized(this) { // do critical stuff here } } 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 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) g un we is en d() () wa it-A n um e (syn chro n y /n o t i f yA ll tspe rre suspendiert (suspended) res be aktiv (active) no tif Aufh e nO b j e ktsp erre Ende IO-Operatio n chen aufwa p() slee Obje k wartend (waiting) su sp gesperrt (locked) ized) IO-blockiert (IO-blocked) IO-Opertion schlafend (sleeping) bereit (ready) neu (new) © Markus Löberbauer 2010 run terminiert start() lauffähig (runnable) tot (dead) Seite 16