Das Schuljahr der zwölften Jahrgangsstufe nähert sich mit großen Schritten dem Abitur und ich hetzte durch den Stoff. Aktuell bin ich bei Threads angelangt, um so die Synchronisierungsproblematik aufzuzeigen. Der Weg bis hierhin sah ungefähr so aus:

  1. Netzwerke und Topologien
  2. Ein einfacher Portscanner, der extem langsam ist.
  3. Ein besserer Portscanner, der mehrere Ports gleichzeitig testet in dem jeweils ein Thread gestartet wird.
  4. Was würde passieren, wenn zwei oder mehr Thread auf die gleiche Resource zugreifen würden?
  5. Erläuterung des Erzeuger-Verbraucher-Problematik
  6. Lösung mit Hilfe von Semaphoren und Monitoren.

Später kommen dann noch die Deadlocks dazu. Da ich zum Glück schon relativ genau auf die von-Neumann-Architektur eingegangen bin, kann ich die systemnahe Programmierung relativ zügig machen. Vor allem weil ich ausnahmsweise nur von sehr programmiererfahrenen Schülern umgeben bin, die es gewohnt sind in verschiedenen Sprachen zu programmieren. Das bringt mich dann zwar teilweise an meinen Wissensgrenzen, aber so habe ich auch mal wieder einen Anlass genauer darüber nachzudenken, wie die Dinge so funktionieren. Ein Beispiel gefällig?

Nach welchem Verfahren erhalten die in Java programmierten Threads ihre Rechenzeit? RoundRobin, also Zeitscheiben-Verfahren? Bekommt jeder Prozess gleich viel Rechenzeit? Wenn ja wieviel? Und warum so viel? Macht das die JVM oder das BS? Und nach welchem Verfahren? Und kann man das als Programmierer beeinflussen?

Die meisten Fragen kann ich leider nicht  oder nur unsicher beantworten, vielleicht kann ja ein Leser mir da helfen. Ich denke, dass es ein Zeitscheibenverfahren der JVM ist. Als Programmierer kann man nur die Priorität der Threads beeinflussen. Welche konkrete Auswirkung diese Änderung hat, kann ich aber  nicht genau sagen.

Wie sähe so etwas eigentlich unter Python aus? Ich spiele ja immer noch mit dem Gedanken die Oberstufe ganz auf Python umzustellen.

Die Lehrerküche als Erzeuger-Verbraucher-Modell

Als Beispiel für ein Erzeuger-Verbraucher-Modell habe ich hier mal die Kaffeemaschine in der Lehrerküche programmiert – die es bei uns in dieser Form aber nicht mehr gibt, weil entweder keiner Kaffee kochen wollte und/oder keiner sauber machen wollte und/oder weil es noch eine Senseo gibt und den Filterkaffee keiner mehr getrunken hat.

Bildschirmfoto 2016-02-04 um 08.08.23

Das Programm wird über die Klasse LehrerKueche gestartet:

public class LehrerKueche {
    public static void main(String[] args) {
        Kaffeemaschine ourKaffeemaschine= new Kaffeemaschine();
        KaffeeKocher armerKollege = new KaffeeKocher(ourKaffeemaschine);
        armerKollege.start();
        for (int cntLehrer = 0; cntLehrer<30; cntLehrer++) {
            (new Lehrer(ourKaffeemaschine)).start();
        }
    }
}

Es gibt einen armen Kollegen, der immer Kaffee kochen muss (Erzeuger). Die Kaffeemaschine agiert als Monitor, wobei die Anzahl der noch vorhandenen Portionen die Semaphore ist. Semaphoren kann man sich als Regel vorstellen, die eingehalten werden sollen. Monitore passen auf, dass die Regeln eingehalten werden. Semaphoren gibt es in binärer Form und als zählende Semaphore, so wie im Beispiel der Lehrerküche. Hier ist es die Anzahl an Kaffeeportionen, die Runtergezählt werden (P-Operation) und wieder hochgzählt wird (V-Operation).

Ein anderes Beispiel? Die Tochter darf nur 1 Freund haben (binäre Semaphore) und der Vater passt auf, dass es auch wirklich so ist. Der Vater ist der Monitor.

Jetzt also zum Monitor Kaffeemaschine:

public class Kaffeemaschine {
    private int anzahlPortionen = 0;
    
    public synchronized boolean holen() {
        if (anzahlPortionen>0) {
            System.out.println(Thread.currentThread().getName()+" holt Kaffee");
            anzahlPortionen--;
            return true;
        }
        System.out.println(Thread.currentThread().getName()+" Kein Kaffee");
        return false;
    }
    
    public synchronized void kochen() {
        if (anzahlPortionen<=0) {
            try{
                Thread.sleep(2000); // Kaffeekochen dauert
            } catch( InterruptedException e ) {}
            System.out.println(Thread.currentThread().getName()+" Kaffee gekocht");
            anzahlPortionen=10;
        }
    }
}

Das Schlüsselwort synchronized organisiert dabei in Java den kontrollierten Zugriff auf die Methoden und gibt der Klasse die logische Bedeutung eines Monitors. Benutzt ein Thread eine synchronized-Methode kann auf keine der anderen synchronized-Methoden zugegriffen werden.

Jetzt noch die Lehrer:

public class Lehrer extends Thread{
    private Kaffeemaschine myKaffeemaschine;
    private boolean hatKaffee;
    
    public Lehrer(Kaffeemaschine myKaffeemaschineNeu) {
        myKaffeemaschine = myKaffeemaschineNeu;
        hatKaffee = false;
    }
    
    public void run() {        
        while (!hatKaffee) {
            try{
                //"Wartezeit" in Millisekunden 
                //damit nicht alle gleichzeitig kommen
                sleep((int)(Math.random()*1000)); 
            } catch( InterruptedException e ) {}
            hatKaffee = myKaffeemaschine.holen();
        }
    }

}

Und noch der KaffeeKocher:

public class KaffeeKocher extends Thread{
    private Kaffeemaschine myKaffeemaschine;
    
    public KaffeeKocher(Kaffeemaschine myKaffeemaschineNeu) {
        myKaffeemaschine = myKaffeemaschineNeu;
    }
    
    public void run() {
        while (true) {
            try{
                sleep((int)(Math.random()*500)); // "Wartezeit"
            } catch( InterruptedException e ) {}
            myKaffeemaschine.kochen();
        }
    }
}

Und damit keiner was abtippen muss – auch wenn ich von Abtippen im Informatikunterricht für wichtig halte, da nur dann die Schüler, gerade die schwächeren, mal den Quelltext auch wirklich lesen und nicht nur den Grauwert aufnehmen – stelle ich den Quelltext zum Download zur Verfügung: Lehrerkueche.zip

Was haltet ihr von der Umsetzung? Was könnte man anders machen?