Wirtschaftsinformatik II Sommersemester 2016 Lo sungshinweise zu den Ü bungen 2-6 @Prof. P. Mandl, M. Dolag, B. Rottmüller, et al. Seite 1 / 6
Übung 2 Verwendung von Java-Threads Ableitung von Klasse Thread public class MyThread extends Thread o Threaderzeugung und Starten private MyThread mythread = new MyThread(); mythread.start(); Implementierung Interface Runnable public class MyRunnable implements Runnable o Threaderzeugung und starten des Threads private MyRunnable myrunnable = new MyRunnable(); Thread thread = new Thread(myRunnable); thread.start(); Java unterstützt keine Mehrfachvererbung. Eine Klasse kann jedoch beliebig viele Interfaces implementieren. Daher kann bei der Interface-Methode zusätzlich noch Vererbung eingesetzt werden. Der Aufruf von run() erzeugt keinen neuen Thread, sondern wird wie ein normaler Methoden-Aufruf abgehandelt. Ein neuer Thread muss explizit mit Aufruf von start() zum Ablauf gebracht werden. Vorteil der Interface-Variante: Java unterstützt keine Mehrfachvererbung. Nutzt man das Runnable-Interface, ist noch die Möglichkeit gegeben, von einer anderen Klassen zu erben. @Prof. P. Mandl, M. Dolag, B. Rottmüller, et al. Seite 2 / 6
Übung 3 Synchronisation von Java Threads Das Programm startet zuerst drei Threads die ohne Synchronisation auf ein CounterObject zugreifen und dieses mit jedem Zugriff um 1 erhöhen. Anschließend wird das gleiche mit Synchronisation (=Definition eines kritischen Abschnitts) vollzogen. Die Deklaration eines Abschnittes mit dem Schlüsselwort synchronized ist relativ teuer, kostet also viel CPU-Zeit. Dadurch benötigt der Ablauf der synchronisierten Threads bedeutend mehr Zeit. Die Threadanzahl verändert sich im Laufe der Verarbeitung. Einige Threads sind bei jedem Java-Prozess vorhanden, die Threads sind JVM-spezifisch. Darunter sind beispielsweise der Garbage-Collector und einige weitere System-Threads zu finden. Das Erzeugen, Starten und Joinen von Threads kann mittels eines Arrays und mit Schleifen wie folgt realisiert werden: private final int MAX_THREADS = 10; Thread[] threadarray = new Thread[MAX_THREADS]; for (int i = 0; i < maxthread; i++) { threadarray[i] = new CountThread1(c, maxcount); threadarray[i].start(); try { for (int i = 0; i < MAX_THREADS; i++) { threadarray[i].join(); catch (Exception e) { e.printstacktrace(); oder so: try { for(thread p : threadarray){ p.join(); catch (Exception e) { e.printstacktrace(); Der join()-aufruf muss in einer separaten Schleife stattfinden, da sonst direkt nach dem Start eines Threads auf dessen Ende gewartet wird, bevor ein neuer Thread gestartet wird Zeitmessung o Start/Stop-Zeit ermitteln private long [begin end] = System.currentTimeMillis(); o Vergangene Zeit (in Millisekunden) = end begin; Prioritäten o JVM-Prioritäten: eine Zahl zwischen Thread.MIN_PRIORITY (1) und Thread.MAX_PRIORITY (10). Dies sind Konstanten aus der Klasse Thread. Standardmäßig wird die Thread.NORM_PRIORITY (5) vergeben. o Windows-Prioritäten: Es können über den Taskmanager 6 Prioritäten zugewiesen werden. Standardmäßig wird Priorität NORMAL (8) verwendet @Prof. P. Mandl, M. Dolag, B. Rottmüller, et al. Seite 3 / 6
Übung 4 - Producer-Consumer-Problem in Java Die Ausgaben entsprechen nicht dem tatsächlichen Programmablauf, da sie nicht an sinnvollen Stellen ausprogrammiert sind. Anhand der Ausgaben könnte der Verdacht entstehen, dass der MessageBuffer mit zwei Nachrichten belegt wird, was aber aufgrund des Quellcodes nicht möglich ist (der produzierende Thread wird zu diesem Zweck mit der wait()-methode aufgehalten. Es werden mehr Nachrichten produziert als konsumiert. In der hier vorgestellten Lösung liest ein Consumer-Thread (in nachfolgendem Beispiel der letzte Thread) die restlichen Nachrichten aus und konsumiert diese: final int AMOUNT_PRODUCER = 3; final int AMOUNT_CONSUMER = 2; int producerloops = 11; int messagecount = producerloops * AMOUNT_PRODUCER; int consumerloops = messagecount/amount_producer; MessageProducer[] arrayprod = new MessageProducer[AMOUNT_PRODUCER]; MessageConsumer[] arraycon = new MessageConsumer[AMOUNT_CONSUMER]; for(int i=0; i< AMOUNT_PRODUCER;i++){ arrayprod[i] = new MessageProducer(producerLoops); for(int i=0; i< AMOUNT_CONSUMER;i++){ if(i == (AMOUNT_CONSUMER - 1)) { arraycon[i] = new MessageConsumer(consumerLoops + messagecount% AMOUNT_CONSUMER); else { arraycon[i] = new MessageConsumer(consumerLoops); notify() weckt einen beliebigen Thread auf, wobei es zu einem Deadlock kommen kann. Der aufgeweckte Thread könnte nämlich ein Producer sein, obwohl ein Consumer benötigt würde oder umgekehrt. notifyall() hingegen weckt alle Threads auf. Alle erhalten irgendwann eine CPU und arbeiten nach dem wait()-aufruf weiter. Ein richtiger Thread ist dann auf alle Fälle dabei. Ein Deadlock wird somit verhindert. @Prof. P. Mandl, M. Dolag, B. Rottmüller, et al. Seite 4 / 6
Übung 5 - Semaphore in Java (Eigenimplementierung) Es handelt sich nicht um eine faire Lösung. Es kann also vorkommen, dass ein Thread länger wartet als ein anderer. Es ist nicht gesichert, dass die Thread-Warteschlange nach FIFO abgearbeitet wird. Weiterhin kann keine genaue Vorhersage getroffen werden, welcher Thread als nächstes an die Reihe kommt, da die CPU-Scheduling-Mechanismen nicht deterministisch sind. So hat jeder Programmablauf eine unterschiedliche Reihenfolge beim Abarbeiten der Threads. @Prof. P. Mandl, M. Dolag, B. Rottmüller, et al. Seite 5 / 6
Übung 6 - Semaphore in Java (Standardklasse) In einem kritischen Abschnitt sollte grundsätzlich (es gibt auch andere Anwendungsszenarien) lediglich ein Thread arbeiten, um die hier gegebenen Synchronisationsprobleme zu vermeiden. Daher muss der Semaphorzähler auf 1 initialisiert werden. Das Programm kann um geeignete System.out.println-Ausgaben vor semaphore.acquire(), vor semaphore.release() und im kritischen Abschnitt erweitert werden. Anschließend kann in der Ausgabe gezählt werden, welcher Thread zuerst wartet und welcher danach den kritischen Abschnitt betritt. Die Fairness-Eigenschaft kann dem Semaphor bei der Erzeugung übergeben werden. Dazu muss der Konstruktur Semaphore(int permits, boolean fair) verwendet werden. Abgefragt werden kann dies über die Semaphore-Methode isfair(). Fair bedeutet hier, dass der zuerst wartende Thread auch zuerst abgearbeitet wird (FIFO-Prinzip der Warteschlange). Ab einer gewissen Thread-Anzahl (abhängig vom der virtuellen Maschine zugewiesenen Speicher wird beim Starten dieser eine OutOfMemory-Exception geworfen, welche geeignet abgefangen werden muss. Wann der Speicher genau ausgeht, hängt von der Systemkonfiguration und den Einstellungen in der JVM ab. Die Heap-Speichergröße kann beim Aufruf eines Java-Programms mit Optionen verändert werden. Eine OutOfMemoryError-Exception kann abgefangen werden. Im catch-block kann das Programm mit einer Fehlermeldung und mit System.exit(0) ordnungsgemäß beendet werden. @Prof. P. Mandl, M. Dolag, B. Rottmüller, et al. Seite 6 / 6