Was bedeuten die Modifikatoren synchronized und volatile?

Beide Modifikatoren werden im Rahmen der nebenläufigen Programmierung verwendet, synchronized dient zur Sicherung der Datenkonsistenz, wenn mehrere Threads gleichzeitig auf eine Methode zugreifen; volatile bewirkt, dass mehrere Threads gleichzeitig auf die damit gekennzeichnete Variable zugreifen können.

Es kann leicht geschehen, dass sich, bei der gleichzeitigen Ausführung mehrerer Threads auf einer Methode des selben Objektes, deren Ausführungen überschneiden und so zu unkontrollierten Ergebnissen führen. Das folgende Beispiel zeigt, wie die Synchronisation von Methoden dies verhindern kann.

In der Klasse SyncCounter wird eine einfache int-Variable deklariert und mit 0 initialisiert. Sie wird mit dem Schlüsselwort volatile gekennzeichnet damit mehrere Threads gleichzeitig auf sie zugreifen können. Der Wert der Variablen kann über die Methoden inc() und dec() inkrementiert und dekrementiert, sowie über einen Getter zurückgegeben werden.

public class SyncCounter {

    private volatile int i = 0;

    public synchronized void inc() throws InterruptedException {
        Thread.sleep(6);
        i++;
    }

    public synchronized void dec() throws InterruptedException {
        Thread.sleep(6);
        i--;
    }

    public synchronized int getI() {
        return i;
    }
}

In den ersten beiden Methoden wird über das Pausieren des zugreifenden Threads für 6 Millisekunden eine kurze Unterbrechung provoziert, die es durch das etwas größere Zeitfenster anderen Threads leichter ermöglicht, auf die Variable zuzugreifen.
Alle Methoden sind als synchronized gekennzeichnet. Dies bewirkt, dass die Ausführung der Methode als atomar, also nicht unterbrechbar, behandelt wird. Wird also z.B. die Methode inc() aufgerufen, so pausiert der aufrufende Thread für sechs Millisekunden und inkrementiert dann i bevor die Methode terminiert. Erst dann kann auf dem selben Objekt eine weitere Methode aufgerufen werden.

public class SynchronizedClass {

    public static void main(String[] args) throws InterruptedException {
        countMultiple();
    }

    static void countMultiple() throws InterruptedException {
        SyncCounter c = new SyncCounter();
        for (int i = 0; i < 100; i++) {
            new Thread() {
                public void run() {
                    try {
                        c.inc();
                        c.dec();
                    } catch (InterruptedException e) {
                    }
                }
            }.start();
        }
        Thread.sleep(2000);
        System.out.println("Final: " + c.getI());
    }
}

Die Klasse SynchronizedClass ruft in main() die Methode countMultiple() auf. In ihr wird zunächst ein Objekt vom Typ SyncCounter erzeugt. In einer Schleife werden daraufhin eine Reihe an Threads erzeugt, von denen jeder versucht, auf die Methoden inc() und dec() des Objektes zuzugreifen und hierüber den Variablenwert des Objektes zu ändern. Wie oben erläutert, geschieht dies bei den hier vorliegenden als synchronized gekennzeichneten Methoden streng nacheinander.
Nach Durchlaufen der Schleife ruht der Haupt-Thread noch zwei Sekunden, um das sukzessive Abarbeiten der Neben-Threads zu ermöglichen, bevor der Endwert der Variablen ausgegeben wird. Da bei jedem Schleifendurchlauf der Wert einmal inkrementiert und einmal dekrementiert wird, ist dieser nach Abschluss gleich 0.

Die genaue Bedeutung des Schlüsselwortes synchronized wird deutlich, wenn man es aus den Methodendeklarationen entfernt, das Programm erneut kompiliert und dann ausführt:

public void inc() throws InterruptedException {
        //...
}

public void dec() throws InterruptedException {
    //...
}

public int getI() {
    return i;
} 

Die ausgegebenen Ergebnisse unterscheiden sich nun von Mal zu Mal und belaufen sich nur gelegentlich auf den erwarteten Wert 0, sondern nehmen wechselnde positive und negative Werte an.
Die Erklärung: Alle drei Methodenausführungen werden nicht mehr atomar durchgeführt. Vielmehr können sich die Methodenaufrufe der einzelnen Threads überschneiden, sodass es zu unregelmäßigen, nicht kalkulierbaren Inkrementierungen und Dekrementierungen kommt.

Eine weiter in die Materie einführende, gleichwohl ausgezeichnete deutsche Darstellung des Synchronisierungsprinzips findet sich unter dem Titel Java Multithread Support - Basics in den Aufsätzen zum Concurrent Programming von Angelika Langer und Klaus Kreft.