Wie kann man in einem Programm mit mehreren Threads auf den Abschluss einer Methodenausführung warten?

Die Methode Thread.join() erlaubt es, den aktuellen Thread auf den aufrufenden warten zu lassen.

Längere Anteile einer Programmausführung werden gemeinhin in eigene Threads ausgelagert, die parallel ausgeführt werden. Da Threads jedoch asynchron arbeiten, ist nicht recht klar, zu welchem Zeitpunkt welcher Thread aktiv ist.

Das Beispiel zeigt dies anhand eines Schreib- und Lesevorgangs in ein und die selbe Datei.

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;

public class AufMethodeWarten {

    static String file = "test.txt";

    public static void main(String[] args) {

        Thread t1 = new Thread() {
            public void run() {
                write();
                System.out.println("writing finished");
            }
        };
        t1.start();

        Thread t2 = new Thread() {
            public void run() {
                read();
                System.out.println("reading finished");
            }
        };
        t2.start();

    }

    static void write() {
        PrintWriter writer = null;
        try {
            writer = new PrintWriter(new FileWriter(file), true);
            for (int i = 0; i < 10; i++) {
                System.out.println("writing " + i);
                writer.write(new Integer(i).toString() + "\n");
            }
        } catch (IOException ioe) {
            ioe.printStackTrace();
        } finally {
            if (writer != null) {
                writer.flush();
                writer.close();
            }
        }
    }

    static void read() {
        BufferedReader reader = null;
        String zeile;
        try {
            reader = new BufferedReader(new FileReader(file));
            while ((zeile = reader.readLine()) != null) {
                System.out.println("reading " + zeile);
            }
        } catch (IOException ioe) {
            ioe.printStackTrace();
        } finally {
            try {
                if (reader != null){
                    reader.close();
                }
            } catch (IOException ioe) {
            }
        }
    }
}

Im Beispiel sind neben main() zwei Methoden deklariert, die einmal schreibend und einmal lesend auf ein und die selbe Datei zugreifen. Beide Methoden werden nacheinander in main() aufgerufen und liefern die folgende Ausgabe:

reading 0 reading 1 writing 0 reading 2 reading 3 reading 4 writing 1 reading 5 writing 2 reading 6 reading 7 reading 8 reading 9 writing 3 writing 4 reading finished writing 5 writing 6 writing 7 writing 8 writing 9 writing finished

Das so nicht vorhersehbare Ergebnis fällt bei fast jedem Programmstart anders aus, da sowohl der Zeitpunkt als auch die Dauer der Threadausführung zunächst nicht zu beeinflussen sind. Erkennbar ist dies an der abwechselnden Ausführung beider Methoden. Dies hat Vorteile, da auf diese Weise beide Ausführungsstränge pseudo-gleichzeitig, in Wirklichkeit jedoch schnell wechselnd ausgeführt werden.
Was jedoch, wenn beide Threads nacheinander ablaufen sollen, z.B. um den Schreibvorgang zunächst abzuschließen bevor die Datei ausgelesen wird?

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;

public class AufMethodeWarten {

    static String file = "test.txt";

    public static void main(String[] args) {

        Thread t1 = new Thread() {
            public void run() {
                write();
                System.out.println("writing finished");
            }
        };
        t1.start();

        Thread t2 = new Thread() {
            public void run() {
                try {
                    t1.join();
                } catch (InterruptedException e1) {
                    e1.printStackTrace();
                }
                read();
                System.out.println("reading finished");
            }
        };
        t2.start();

    }

    static void write() {
        PrintWriter writer = null;
        try {
            writer = new PrintWriter(new FileWriter(file), true);
            for (int i = 0; i < 10; i++) {
                System.out.println("writing " + i);
                writer.write(new Integer(i).toString() + "\n");
            }
        } catch (IOException ioe) {
            ioe.printStackTrace();
        } finally {
            if (writer != null) {
                writer.flush();
                writer.close();
            }
        }
    }

    static void read() {
        BufferedReader reader = null;
        String zeile;
        try {
            reader = new BufferedReader(new FileReader(file));
            while ((zeile = reader.readLine()) != null) {
                System.out.println("reading " + zeile);
            }
        } catch (IOException ioe) {
            ioe.printStackTrace();
        } finally {
            try {
                if (reader != null) {
                    reader.close();
                }
            } catch (IOException ioe) {
            }
        }
    }
}

Hierzu bietet die Klasse Thread die mehrfach überladene Methode join() an. Wird sie - wie hier - ohne Parameter innerhalb des schreibenden Threads auf dem Objekt des lesenden Threads aufgerufen, so wartet dieser mit seiner Ausführung, bis der Schreibvorgang abgeschlossen ist. Das Ergebnis ist die folgende Ausgabe:

writing 0
writing 1
writing 2
writing 3
writing 4
writing 5
writing 6
writing 7
writing 8
writing 9
writing finished
reading 0
reading 1
reading 2
reading 3
reading 4
reading 5
reading 6
reading 7
reading 8
reading 9
reading finished

Der Aufruf von join() muss innerhalb eines try-catch-Blockes erfolgen, um eventuelle Unterbrechungen abzufangen.

Neben der parameterlosen Version bietet die Klasse Thread noch zwei weitere Varianten der Methode: join(long millis) wartet lediglich millis Millisekunden bis der Thread ausgeführt wird. Die zweite Variante besitzt noch einen zusätzlichen int-Parameter für eine Angabe von Nanosekunden: join(long millis, int nanos). Die hier angewandte parameterlose Variante entspricht exakt der Ausführung von join(0).