Vorlesung 3
Threads und Nebenläufigkeitsprobleme

Threads und Nebenläufigkeitsprobleme

Einführung

Nebenläufigkeit bezieht sich auf die gleichzeitige Ausführung mehrerer Threads in einem Programm. Während Threads die Ausführung von Aufgaben parallelisieren und die Anwendungsleistung verbessern können, bringen sie auch mehrere Herausforderungen und Probleme mit sich, insbesondere wenn sie gemeinsame Ressourcen verwenden.

Arten von Nebenläufigkeitsproblemen

Race Conditions (Wettlaufsituationen)

Eine Race Condition tritt auf, wenn zwei oder mehr Threads gleichzeitig auf gemeinsame Daten zugreifen und die Endergebnisse vom Zeitpunkt der Ausführung abhängen. Dies kann zu inkonsistenten oder falschen Ergebnissen führen.

Deadlocks (Verklemmungen)

Ein Deadlock tritt auf, wenn zwei oder mehr Threads auf Ressourcen warten, die von anderen Threads gehalten werden, wodurch ein unendlicher Wartungszustand entsteht. In diesem Zustand kann keiner der Threads seine Ausführung fortsetzen.

Livelocks

Ein Livelock ähnelt einem Deadlock, jedoch ändern die betroffenen Threads ständig ihren Zustand als Reaktion auf die Aktionen des anderen, ohne nennenswerte Fortschritte zu machen.

Starvation (Verhungern)

Starvation tritt auf, wenn ein Thread nie die CPU-Zeit erhält, die er benötigt, um fortzufahren, weil andere Threads kontinuierlich bevorzugt werden.

Lösungen für Nebenläufigkeitsprobleme

Synchronisation

Synchronisation stellt sicher, dass nur ein Thread gleichzeitig auf eine kritische Region zugreifen kann. Dies kann durch die Verwendung des synchronized Schlüsselworts in Java erreicht werden.

public class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

Locks

Die java.util.concurrent.locks Bibliothek bietet flexiblere Lock-Implementierungen als die synchronisierten Methoden und Blöcke.

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Counter {
    private int count = 0;
    private final Lock lock = new ReentrantLock();

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }

    public int getCount() {
        return count;
    }
}

Warten und Benachrichtigen

Die Methoden wait() und notify() der Object-Klasse können verwendet werden, um die Kommunikation zwischen Threads zu koordinieren.

public class SharedResource {
    private boolean available = false;

    public synchronized void produce() throws InterruptedException {
        while (available) {
            wait();
        }
        available = true;
        notifyAll();
    }

    public synchronized void consume() throws InterruptedException {
        while (!available) {
            wait();
        }
        available = false;
        notifyAll();
    }
}

Fazit

Nebenläufigkeitsprobleme stellen eine große Herausforderung in der parallelen Programmierung dar. Das Verständnis und die Anwendung geeigneter Synchronisationstechniken und -mechanismen sind entscheidend, um sichere und effiziente nebenläufige Programme zu erstellen. Java bietet eine Vielzahl von Werkzeugen und Bibliotheken, um diese Herausforderungen zu bewältigen, einschließlich synchronisierter Methoden, Lock-Objekten und der Verwendung von wait() und notify().

Threads: Synchronisierung

Einführung

Synchronisierung ist ein Konzept in der parallelen Programmierung, das sicherstellt, dass mehrere Threads sicher auf gemeinsam genutzte Ressourcen zugreifen können. Ohne Synchronisierung können Race Conditions und andere Nebenläufigkeitsprobleme auftreten, die zu inkonsistenten Daten und unvorhersehbarem Verhalten führen.

Das synchronized Schlüsselwort

In Java kann das synchronized Schlüsselwort verwendet werden, um sicherzustellen, dass nur ein Thread gleichzeitig auf eine kritische Region zugreifen kann. Dies kann auf Methodenebene oder auf Blockebene erfolgen.

Synchronisierte Methoden

Eine synchronisierte Methode ermöglicht den exklusiven Zugriff auf eine Methode für einen Thread.

public class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }
}

Synchronisierte Blöcke

Synchronisierte Blöcke bieten eine feinere Kontrolle über die Synchronisierung, da sie nur einen bestimmten Teil einer Methode synchronisieren können.

public class Counter {
    private int count = 0;
    private final Object lock = new Object();

    public void increment() {
        synchronized (lock) {
            count++;
        }
    }

    public int getCount() {
        synchronized (lock) {
            return count;
        }
    }
}

Locks und ReentrantLock

Die java.util.concurrent.locks Bibliothek bietet die Lock Schnittstelle und die ReentrantLock Klasse, die mehr Flexibilität und Funktionen als das synchronized Schlüsselwort bieten.

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Counter {
    private int count = 0;
    private final Lock lock = new ReentrantLock();

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }

    public int getCount() {
        lock.lock();
        try {
            return count;
        } finally {
            lock.unlock();
        }
    }
}

Warten und Benachrichtigen

Die Methoden wait(), notify() und notifyAll() werden verwendet, um die Kommunikation zwischen Threads zu koordinieren. Diese Methoden müssen innerhalb eines synchronisierten Blocks oder einer synchronisierten Methode aufgerufen werden.

public class SharedResource {
    private boolean available = false;

    public synchronized void produce() throws InterruptedException {
        while (available) {
            wait();
        }
        available = true;
        notifyAll();
    }

    public synchronized void consume() throws InterruptedException {
        while (!available) {
            wait();
        }
        available = false;
        notifyAll();
    }
}

Fazit

Die Synchronisierung von Threads ist entscheidend für die Erstellung sicherer und effizienter nebenläufiger Programme. Java bietet mehrere Mechanismen zur Synchronisierung, einschließlich des synchronized Schlüsselworts, der Lock Schnittstelle und der wait() und notify() Methoden. Das Verständnis und die richtige Anwendung dieser Mechanismen sind wesentlich, um Race Conditions, Deadlocks und andere Nebenläufigkeitsprobleme zu vermeiden.

Threads: Thread-Kommunikation

Einführung

Thread-Kommunikation ist ein wichtiger Aspekt der nebenläufigen Programmierung, der es Threads ermöglicht, sicher und effizient miteinander zu interagieren. In Java gibt es mehrere Mechanismen, um die Kommunikation zwischen Threads zu handhaben, einschließlich der Methoden wait(), notify() und notifyAll().

Warten und Benachrichtigen

Die Methoden wait(), notify() und notifyAll() der Object-Klasse werden verwendet, um die Kommunikation und Synchronisation zwischen Threads zu koordinieren. Diese Methoden müssen innerhalb eines synchronisierten Blocks oder einer synchronisierten Methode aufgerufen werden.

Die Methode wait()

Die Methode wait() versetzt den aufrufenden Thread in einen Wartezustand, bis er von einem anderen Thread mit notify() oder notifyAll() geweckt wird.

public synchronized void waitForCondition() throws InterruptedException {
    while (!condition) {
        wait();
    }
    // Proceed when condition is met
}

Die Methode notify()

Die Methode notify() weckt einen einzigen wartenden Thread, der auf das gleiche Objekt wartet. Wenn mehrere Threads auf das Objekt warten, wird einer von ihnen willkürlich geweckt.

public synchronized void signalCondition() {
    condition = true;
    notify();
}

Die Methode notifyAll()

Die Methode notifyAll() weckt alle wartenden Threads, die auf das gleiche Objekt warten. Jeder der geweckten Threads muss erneut die Bedingung überprüfen, bevor er fortfährt.

public synchronized void signalAllConditions() {
    condition = true;
    notifyAll();
}

Beispiel: Einfache Thread-Kommunikation

Im folgenden Beispiel wird die Kommunikation zwischen einem Produzenten-Thread und einem Konsumenten-Thread demonstriert. Der Produzent erzeugt Daten und signalisiert dem Konsumenten, dass die Daten verfügbar sind.

public class SharedResource {
    private boolean available = false;

    public synchronized void produce() throws InterruptedException {
        while (available) {
            wait();
        }
        available = true;
        notifyAll();
    }

    public synchronized void consume() throws InterruptedException {
        while (!available) {
            wait();
        }
        available = false;
        notifyAll();
    }
}

public class Producer implements Runnable {
    private final SharedResource resource;

    public Producer(SharedResource resource) {
        this.resource = resource;
    }

    @Override
    public void run() {
        try {
            while (true) {
                resource.produce();
                System.out.println("Produced");
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

public class Consumer implements Runnable {
    private final SharedResource resource;

    public Consumer(SharedResource resource) {
        this.resource = resource;
    }

    @Override
    public void run() {
        try {
            while (true) {
                resource.consume();
                System.out.println("Consumed");
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

public class Main {
    public static void main(String[] args) {
        SharedResource resource = new SharedResource();
        Thread producerThread = new Thread(new Producer(resource));
        Thread consumerThread = new Thread(new Consumer(resource));

        producerThread.start();
        consumerThread.start();
    }
}

Fazit

Die Kommunikation zwischen Threads ist ein wesentlicher Bestandteil der nebenläufigen Programmierung. In Java ermöglichen die Methoden wait(), notify() und notifyAll() eine effektive und koordinierte Interaktion zwischen Threads. Durch das richtige Verständnis und die Anwendung dieser Methoden können Entwickler sicherstellen, dass ihre Programme korrekt und effizient arbeiten.