web-dev-qa-db-ger.com

implementieren Sie Ihre eigene Blockierungswarteschlange in Java

Ich weiß, dass diese Frage schon oft gestellt und beantwortet wurde, aber ich konnte einfach keinen Trick für die Beispiele im Internet finden, wie dieses oder das .

Beide Lösungen prüfen, ob das Array/die Warteschlange/die verknüpfte Liste der blockierenden Warteschlange für notifyAll wartende Threads in der put()-Methode und umgekehrt in den get()-Methoden leer ist. Ein Kommentar im zweiten Link hebt diese Situation hervor und erwähnt, dass dies nicht notwendig ist.

Die Frage ist also: Es kommt mir auch seltsam vor, zu prüfen, ob die Warteschlange leer ist voll, um alle wartenden Threads zu benachrichtigen. Irgendwelche Ideen?

Danke im Voraus.

12
tugcem

Ich weiß, dass dies jetzt eine alte Frage ist, aber nachdem ich die Frage und die Antworten gelesen habe, konnte ich mir nicht helfen, ich hoffe, Sie finden dies nützlich.

In Bezug auf die Überprüfung, ob die Warteschlange tatsächlich voll oder leer ist, bevor andere wartende Threads benachrichtigt werden, fehlt etwas, bei dem es sich bei den beiden Methoden put (T t) und T get() um synchronized handelt Es kann immer nur ein Thread eine dieser Methoden gleichzeitig eingeben, dies hindert sie jedoch nicht daran, zusammenzuarbeiten. Wenn also ein Thread-a die Methode put (T t) eingegeben hat, kann ein anderer Thread-b weiterhin die Anweisungen eingeben und ausführen Mit der Methode T get(), bevor Thread-a put (T t) beendet hat, fühlt sich der Entwickler mit diesem double-checking - Entwurf ein bisschen sicherer, da Sie nicht wissen, ob es in Zukunft noch so weit ist CPU-Kontextwechsel, ob und wann dies geschehen wird.

Ein besserer und empfohlener Ansatz ist die Verwendung von Reentrant Locks Und Conditions:

// Ich habe den Quellcode von diesem bearbeitet Link

Condition isFullCondition;
Condition isEmptyCondition;
Lock lock;

public BQueue() {
    this(Integer.MAX_VALUE);
}

public BQueue(int limit) {
    this.limit = limit;
    lock = new ReentrantLock();
    isFullCondition = lock.newCondition();
    isEmptyCondition = lock.newCondition();
}

public void put (T t) {
    lock.lock();
    try {
       while (isFull()) {
            try {
                isFullCondition.await();
            } catch (InterruptedException ex) {}
        }
        q.add(t);
        isEmptyCondition.signalAll();
    } finally {
        lock.unlock();
    }
 }

public T get() {
    T t = null;
    lock.lock();
    try {
        while (isEmpty()) {
            try {
                isEmptyCondition.await();
            } catch (InterruptedException ex) {}
        }
        t = q.poll();
        isFullCondition.signalAll();
    } finally { 
        lock.unlock();
    }
    return t;
}

Bei diesem Ansatz ist double checking Nicht erforderlich, da das lock -Objekt von beiden Methoden gemeinsam genutzt wird. Dies bedeutet, dass im Gegensatz zu synchronisierten Methoden nur ein Thread a oder b gleichzeitig eine dieser Methoden eingeben kann Wenn Sie verschiedene Monitore erstellen, werden nur die Threads benachrichtigt, die warten, weil die Warteschlange voll ist. Das gleiche gilt für Threads, die warten, weil die Warteschlange leer ist. Dies führt zu einer besseren CPU-Auslastung. Sie finden ein ausführlicheres Beispiel mit dem Quellcode hier

19
Ayesh Qumhieh

Ich denke logischerweise kann es nicht schaden, diese zusätzliche Prüfung vor notifyAll() durchzuführen.

Sie können einfach notifyAll(), sobald Sie etwas in die Warteschlange gestellt haben. Alles wird noch funktionieren und Ihr Code ist kürzer. Es gibt jedoch auch keine Schadensüberprüfung, wenn möglicherweise jemand wartet (indem überprüft wird, ob die Grenze der Warteschlange erreicht ist), bevor Sie notifyAll() aufrufen. Diese zusätzliche Logik erspart unnötige notifyAll()-Aufrufe.

Es hängt nur davon ab, ob Sie einen kürzeren und saubereren Code wünschen oder ob Ihr Code effizienter ausgeführt werden soll. (Die Implementierung von notifyAll() wurde nicht untersucht. Wenn es sich um eine kostengünstige Operation handelt, bei der niemand wartet, ist der Leistungsgewinn für diese zusätzliche Überprüfung möglicherweise nicht ersichtlich.)

1
Adrian Shum

Der Grund, warum die Autoren notifyAll() verwendeten, ist einfach: Sie hatten keine Ahnung, ob es notwendig war oder nicht, und entschieden sich für die "sicherere" Option.

Im obigen Beispiel wäre es ausreichend, nur notify() aufzurufen, da für jedes hinzugefügte einzelne Element unter allen Umständen nur ein einziger wartender Thread bedient werden kann.

Dies wird offensichtlicher, wenn auch Ihre Warteschlange die Option hat, mehrere Elemente in einem Schritt wie addAll(Collection<T> list) hinzuzufügen, da in diesem Fall mehr als ein auf eine leere Liste wartender Thread bedient werden könnte, um genau zu sein: so viele Threads wie Elemente wurde hinzugefügt.

Die notifyAll() verursacht jedoch einen zusätzlichen Overhead in dem speziellen Einzelelementfall, da viele Threads unnötig aufgeweckt werden und daher erneut in den Ruhezustand versetzt werden müssen, wodurch der Warteschlangenzugriff in der Zwischenzeit blockiert wird. Das Ersetzen von notifyAll() durch notify() würde also die Geschwindigkeit verbessern in diesem speziellen Fall .

Aber dann würde nicht das Verwenden von wait/notify und das Synchronisieren von wait/notify, sondern das gleichzeitige Verwenden des Pakets, die Geschwindigkeit um viel mehr erhöhen, als es eine Implementierung von smart wait/notify jemals erreichen könnte.

0
TwoThe