JUC-Vorlesung 11: Detaillierte Erläuterung der JUC-Sperre LockSupport

JUC-Vorlesung 11: Detaillierte Erläuterung der JUC-Sperre LockSupport

Dieser Artikel ist die elfte Vorlesung von JUC: Ausführliche Erläuterung der JUC-Sperre LockSupport. LockSupport ist die Grundlage von Sperren und eine Werkzeugklasse, die Sperrmechanismen bereitstellt.

1. Verstehen Sie die Interviewfragen großer BAT-Unternehmen

Bitte fahren Sie mit diesen Fragen fort, die Ihnen dabei helfen werden, die relevanten Wissenspunkte weitgehend besser zu verstehen.

  • Warum ist LockSupport auch eine zentrale Basisklasse? Das AQS-Framework basiert auf zwei Klassen: Unsafe (Bereitstellung von CAS-Operationen) und LockSupport (Bereitstellung von Park-/Entpark-Operationen)
  • Schreiben Sie auf, wie eine Synchronisierung durch Parken/Entparken von Wait/Notify bzw. LockSupport erreicht werden kann.
  • Wird LockSupport.park() die Sperrressource freigeben? Was ist mit Condition.await()?
  • Was sind die Unterschiede zwischen Thread.sleep(), Object.wait(), Condition.await() und LockSupport.park()? Wichtige Punkte
  • Was passiert, wenn notify() vor wait() ausgeführt wird?
  • Was passiert, wenn unpark() vor park() ausgeführt wird?

2. Einführung in LockSupport

LockSupport ist das grundlegende Thread-Blockierungsprimitiv, das zum Erstellen von Sperren und anderen Synchronisierungsklassen verwendet wird . Kurz gesagt bedeutet dies beim Aufruf von LockSupport.park, dass der aktuelle Thread wartet, bis die Erlaubnis eingeholt wird. Beim Aufruf von LockSupport.unpark muss der Thread, der auf die Erlaubnis wartet, als Parameter übergeben werden, damit dieser Thread weiter ausgeführt werden kann.

3. Analyse des LockSupport-Quellcodes

3.1. Klassenattribute

public class LockSupport {
    
    
    // Hotspot implementation via intrinsics API
    private static final sun.misc.Unsafe UNSAFE;
    // 表示内存偏移地址
    private static final long parkBlockerOffset;
    // 表示内存偏移地址
    private static final long SEED;
    // 表示内存偏移地址
    private static final long PROBE;
    // 表示内存偏移地址
    private static final long SECONDARY;
    
    static {
    
    
        try {
    
    
            // 获取Unsafe实例
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            // 线程类类型
            Class<?> tk = Thread.class;
            // 获取Thread的 parkBlocker 字段的内存偏移地址
            parkBlockerOffset = UNSAFE.objectFieldOffset(tk.getDeclaredField("parkBlocker"));
            // 获取Thread的 threadLocalRandomSeed 字段的内存偏移地址
            SEED = UNSAFE.objectFieldOffset(tk.getDeclaredField("threadLocalRandomSeed"));
            // 获取Thread的 threadLocalRandomProbe 字段的内存偏移地址
            PROBE = UNSAFE.objectFieldOffset(tk.getDeclaredField("threadLocalRandomProbe"));
            // 获取Thread的 threadLocalRandomSecondarySeed 字段的内存偏移地址
            SECONDARY = UNSAFE.objectFieldOffset(tk.getDeclaredField("threadLocalRandomSecondarySeed"));
        } catch (Exception ex) {
    
     
          	throw new Error(ex); 
        }
    }
}

Beschreibung: Das UNSAFE-Feld repräsentiert sun.misc.Unsafedie Klasse, zeigt ihren Quellcode an, Java Magic-Klasse: Unsichere Anwendungsanalyse , direkte Aufrufe sind in allgemeinen Programmen nicht erlaubt, und der lange Typ stellt die Offset-Adresse des entsprechenden Felds des Instanzobjekts im Speicher dar , und die Offset-Adresse kann verwendet werden. Holen Sie sich den Wert dieses Felds oder legen Sie ihn fest.

3.2. Konstruktor der Klasse

// 私有构造函数,无法被实例化
private LockSupport() {
    
    }

Hinweis: LockSupport verfügt nur über einen privaten Konstruktor und kann nicht instanziiert werden.

3.3. Kernfunktionsanalyse

Bevor Sie die LockSupport-Funktion analysieren, stellen Sie zunächst sun.misc.Unsafedie Park- und Unpark-Funktionen in der Klasse vor, da die Kernfunktionen von LockSupport auf den in der Unsafe-Klasse definierten Park- und Unpark-Funktionen basieren. Die Definitionen der beiden Funktionen sind unten aufgeführt:

public native void park(boolean isAbsolute, long time);
public native void unpark(Thread thread);

Beschreibung: Die Beschreibung der beiden Funktionen lautet wie folgt:

  • Die Parkfunktion blockiert den Thread und der Thread wird blockiert, bis die folgenden Bedingungen auftreten :
    • ① Rufen Sie die Unpark-Funktion auf, um die Berechtigung des Threads freizugeben.
    • ② Der Thread ist unterbrochen;
    • ③ Es ist Zeit zum Einrichten. Und wenn die Zeit eine absolute Zeit ist, ist isAbsolute wahr, andernfalls ist isAbsolute falsch. Wenn die Zeit 0 ist, bedeutet dies, dass unbegrenzt gewartet wird, bis das Entparken erfolgt.
  • Die Funktion „unpark“ gibt die Berechtigung des Threads frei, d. h. sie aktiviert den Thread, der nach dem Aufruf von „park“ blockiert wurde . Diese Funktion ist nicht sicher. Stellen Sie sicher, dass der Thread noch aktiv ist, wenn Sie diese Funktion aufrufen.
1. Parkfunktion

Es gibt zwei überladene Versionen der Parkfunktion. Die Methodenzusammenfassung lautet wie folgt:

public static void park()public static void park(Object blocker)

Hinweis : Der Unterschied zwischen den beiden Funktionen besteht darin, dass die Funktion park () keinen Blocker hat, dh das Feld parkBlocker des Threads ist nicht festgelegt. Die Funktion vom Typ park(Object) lautet wie folgt.

public static void park(Object blocker) {
    
    
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 设置Blocker
    setBlocker(t, blocker);
    // 获取许可
    UNSAFE.park(false, 0L);
    // 重新可运行后再此设置Blocker
    setBlocker(t, null);
}

Hinweis : Wenn Sie die Parkfunktion aufrufen, rufen Sie zuerst den aktuellen Thread ab und legen Sie dann das ParkBlocker-Feld des aktuellen Threads fest. Rufen Sie also die SetBlocker-Funktion auf, rufen Sie dann die Parkfunktion der Unsafe-Klasse auf und rufen Sie dann die SetBlocker-Funktion auf. Die Frage ist also: Warum müssen wir die Funktion setBlocker in dieser Parkfunktion zweimal aufrufen ?

Der Grund ist eigentlich ganz einfach: Beim Aufrufen der Parkfunktion legt der aktuelle Thread zuerst das Feld parkBlocker fest und ruft dann die Parkfunktion von Unsafe auf. Danach wurde der aktuelle Thread blockiert und wartet darauf, dass die Unpark-Funktion des Threads aufgerufen wird Die folgende setBlocker-Funktion kann nicht ausgeführt werden, die Unpark-Funktion wird aufgerufen. Nachdem der Thread die Berechtigung erhalten hat, kann er weiter ausgeführt werden. Das heißt, der zweite setBlocker wird ausgeführt und das parkBlocker-Feld des Threads wird auf Null gesetzt, wodurch die Logik vervollständigt wird der gesamten Parkfunktion. Wenn kein zweiter setBlocker vorhanden ist, wird park(Object blocker) später nicht aufgerufen, sondern die getBlocker-Funktion wird direkt aufgerufen und der vom vorherigen park(Object blocker) festgelegte Blocker wird erhalten, was offensichtlich unlogisch ist . Kurz gesagt, es muss sichergestellt werden, dass das ParkBlocker-Feld des Threads nach Ausführung der gesamten Park-Funktion (Objektblocker) auf Null zurückkehrt. Daher muss die Funktion setBlocker in der Funktion park(Object) zweimal aufgerufen werden. Die setBlocker-Methode lautet wie folgt:

private static void setBlocker(Thread t, Object arg) {
    
    
    // 设置线程t的parkBlocker字段的值为arg
    UNSAFE.putObject(t, parkBlockerOffset, arg);
}

Beschreibung: Mit dieser Methode wird der Wert des parkBlocker-Felds von Thread t auf arg gesetzt.

Eine weitere überladene Version ohne Parameter, die Funktion park(), sieht wie folgt aus.

public static void park() {
    
    
    // 获取许可,设置时间为无限长,直到可以获取许可
    UNSAFE.park(false, 0L);
}

Hinweis : Nach dem Aufruf der Parkfunktion wird der aktuelle Thread deaktiviert, sofern keine Berechtigung verfügbar ist. Der aktuelle Thread bleibt in einem Ruhezustand, bis eine der folgenden drei Situationen eintritt. Das heißt, wenn die folgenden Situationen auftreten, erhält der aktuelle Thread die Erlaubnis und kann weiter ausgeführt werden.

  • Einige andere Thread-Aufrufe entparken mit dem aktuellen Thread als Ziel;
  • Ein anderer Thread unterbricht den aktuellen Thread.
  • Der Anruf kehrt unlogisch (d. h. ohne Grund) zurück.
2. parkNanos-Funktion

Diese Funktion deaktiviert den aktuellen Thread und wartet bis zur angegebenen Wartezeit, bevor die Berechtigung verfügbar ist. Die spezifischen Funktionen sind wie folgt.

public static void parkNanos(Object blocker, long nanos) {
    
    
    if (nanos > 0) {
    
     // 时间大于0
        // 获取当前线程
        Thread t = Thread.currentThread();
        // 设置Blocker
        setBlocker(t, blocker);
        // 获取许可,并设置了时间
        UNSAFE.park(false, nanos);
        // 设置许可
        setBlocker(t, null);
    }
}

Hinweis: Diese Funktion ruft auch zweimal die Funktion setBlocker auf. Der Parameter nanos stellt die relative Zeit und die Wartezeit dar.

3. parkUntil-Funktion

Diese Funktion bedeutet, den aktuellen Thread vor dem angegebenen Zeitlimit zu deaktivieren, sofern keine Erlaubnis verfügbar ist. Die spezifische Funktion lautet wie folgt:

public static void parkUntil(Object blocker, long deadline) {
    
    
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 设置Blocker
    setBlocker(t, blocker);
    UNSAFE.park(true, deadline);
    // 设置Blocker为null
    setBlocker(t, null);
}

Hinweis: Diese Funktion ruft auch die Funktion setBlocker zweimal auf. Der Deadline-Parameter stellt die absolute Zeit und die angegebene Zeit dar.

4. Entparkfunktion

Diese Funktion gibt an, dass die Berechtigung des angegebenen Threads verfügbar gemacht wird, wenn sie nicht bereits verfügbar ist . Wenn ein Thread beim Parken blockiert ist, wird er entsperrt. Ansonsten ist garantiert, dass der nächste Parkruf nicht blockiert wird. Wenn der angegebene Thread noch nicht gestartet wurde, gibt es keine Garantie dafür, dass dieser Vorgang Auswirkungen hat. Die spezifischen Funktionen sind wie folgt:

public static void unpark(Thread thread) {
    
    
    if (thread != null) // 线程为不空
        UNSAFE.unpark(thread); // 释放该线程许可
}

Beschreibung: Geben Sie die Berechtigung frei und der angegebene Thread kann weiter ausgeführt werden.

4. LockSupport-Beispielbeschreibung

4.1. Verwenden Sie Wait/Notify, um eine Thread-Synchronisierung zu erreichen

class MyThread extends Thread {
    
    
    public void run() {
    
    
        synchronized (this) {
    
    
            System.out.println("before notify");
            notify();
            System.out.println("after notify");
        }
    }
}

public class WaitAndNotifyDemo {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        MyThread myThread = new MyThread();            
        synchronized (myThread) {
    
    
            try {
    
            
                myThread.start();
                // 主线程睡眠3s
                Thread.sleep(3000);
                System.out.println("before wait");
                // 阻塞主线程
                myThread.wait();
                System.out.println("after wait");
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }            
        }        
    }
}

Operationsergebnis

before wait
before notify
after notify
after wait

Hinweis: Das spezifische Flussdiagramm sieht wie folgt aus

Bild

Wenn Sie Wait/Notify verwenden, um eine Synchronisierung zu erreichen, müssen Sie zuerst Wait und dann Notify aufrufen. Wenn Sie zuerst Notify und dann Wait aufrufen, funktioniert dies nicht . Der spezifische Code lautet wie folgt

class MyThread extends Thread {
    
    
    public void run() {
    
    
        synchronized (this) {
    
    
            System.out.println("before notify");            
            notify();
            System.out.println("after notify");    
        }
    }
}

public class WaitAndNotifyDemo {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        MyThread myThread = new MyThread();        
        myThread.start();
        // 主线程睡眠3s
        Thread.sleep(3000);
        synchronized (myThread) {
    
    
            try {
    
            
                System.out.println("before wait");
                // 阻塞主线程
                myThread.wait();
                System.out.println("after wait");
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }            
        }        
    }
}

Operationsergebnis:

before notify
after notify
before wait

Hinweis: Da notify zuerst aufgerufen wird und dann wait aufgerufen wird, wird der Hauptthread zu diesem Zeitpunkt immer noch blockiert.

4.2. Verwenden Sie Parken/Entparken, um eine Thread-Synchronisation zu erreichen

import java.util.concurrent.locks.LockSupport;

class MyThread extends Thread {
    
    
    private Object object;

    public MyThread(Object object) {
    
    
        this.object = object;
    }

    public void run() {
    
    
        System.out.println("before unpark");
        try {
    
    
            Thread.sleep(1000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        // 获取blocker
        System.out.println("Blocker info " + LockSupport.getBlocker((Thread) object));
        // 释放许可
        LockSupport.unpark((Thread) object);
        // 休眠500ms,保证先执行park中的setBlocker(t, null);
        try {
    
    
            Thread.sleep(500);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        // 再次获取blocker
        System.out.println("Blocker info " + LockSupport.getBlocker((Thread) object));

        System.out.println("after unpark");
    }
}

public class test {
    
    
    public static void main(String[] args) {
    
    
        MyThread myThread = new MyThread(Thread.currentThread());
        myThread.start();
        System.out.println("before park");
        // 获取许可
        LockSupport.park("ParkAndUnparkDemo");
        System.out.println("after park");
    }
}

Operationsergebnis:

before park
before unpark
Blocker info ParkAndUnparkDemo
after park
Blocker info null
after unpark

Hinweis : Dieses Programm führt zuerst Park aus, führt dann Unpark zur Synchronisierung aus und ruft getBlocker vor und nach Unpark auf. Sie können sehen, dass die Ergebnisse der beiden Zeiten unterschiedlich sind und das Ergebnis des zweiten Aufrufs null ist. Dies liegt daran, dass nach dem Aufruf unpark, Lock.park(Object blocker)die Funktion setBlocker(t, null) in der Funktion wird ausgeführt , sodass getBlocker beim zweiten Aufruf null ist.

Im obigen Beispiel wird zuerst „Park“ und dann „Unpark“ aufgerufen. Ändern Sie nun das Programm so, dass es zuerst „Unpark“ aufruft und dann „Park“ aufruft, um zu prüfen, ob die Synchronisierung korrekt ist. Der spezifische Code lautet wie folgt

import java.util.concurrent.locks.LockSupport;

class MyThread extends Thread {
    
    
    private Object object;

    public MyThread(Object object) {
    
    
        this.object = object;
    }

    public void run() {
    
    
        System.out.println("before unpark");        
        // 释放许可
        LockSupport.unpark((Thread) object);
        System.out.println("after unpark");
    }
}

public class ParkAndUnparkDemo {
    
    
    public static void main(String[] args) {
    
    
        MyThread myThread = new MyThread(Thread.currentThread());
        myThread.start();
        try {
    
    
            // 主线程睡眠3s
            Thread.sleep(3000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        System.out.println("before park");
        // 获取许可
        LockSupport.park("ParkAndUnparkDemo");
        System.out.println("after park");
    }
}

Operationsergebnis:

before unpark
after unpark
before park
after park

Hinweis: Es ist ersichtlich, dass die Synchronisierung immer noch korrekt erreicht werden kann, wenn zuerst „Unpark“ und dann „Park“ aufgerufen wird, ohne dass es zu einer Blockierung aufgrund einer falschen Warte-/Benachrichtigungsaufrufsequenz kommt. Daher ist Parken/Entparken flexibler als Warten/Benachrichtigen.

4.3. Interrupt-Reaktion

Schauen Sie sich das Beispiel unten an

import java.util.concurrent.locks.LockSupport;

class MyThread extends Thread {
    
    
    private Object object;

    public MyThread(Object object) {
    
    
        this.object = object;
    }

    public void run() {
    
    
        System.out.println("before interrupt");
        try {
    
    
            // 休眠3s
            Thread.sleep(3000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }    
        Thread thread = (Thread) object;
        // 中断线程 **
        thread.interrupt();
        System.out.println("after interrupt");
    }
}

public class InterruptDemo {
    
    
    public static void main(String[] args) {
    
    
        MyThread myThread = new MyThread(Thread.currentThread());
        myThread.start();
        System.out.println("before park");
        // 获取许可
        LockSupport.park("ParkAndUnparkDemo");
        System.out.println("after park");
    }
}

Operationsergebnis:

before park
before interrupt
after interrupt
after park

Hinweis : Es ist ersichtlich, dass nach dem Aufrufen von Park durch den Haupt-Thread zum Blockieren ein Interrupt-Signal an den MyThread-Thread gesendet wird. Zu diesem Zeitpunkt wird der Haupt-Thread weiter ausgeführt, was bedeutet, dass Interrupt zu diesem Zeitpunkt dieselbe Rolle wie Unpark spielt Zeit .

5. Tieferes Verständnis

5.1. Der Unterschied zwischen Thread.sleep() und Object.wait()

Schauen wir uns zunächst den Unterschied zwischen Thread.sleep() und Object.wait() an. Das ist eine große Frage. Jeder sollte zwei Punkte sagen können.

  • Thread.sleep() gibt die gehaltene Sperre nicht frei, aber Object.wait() gibt die gehaltene Sperre frei;
  • Thread.sleep() muss die Zeit übergeben, und Object.wait() kann sie übergeben oder nicht. Wenn sie nicht übergeben wird, bedeutet dies, dass sie weiterhin blockiert.
  • Thread.sleep() wacht automatisch auf, wenn die Zeit abgelaufen ist, und setzt dann die Ausführung fort.
  • Object.wait() hat keine Zeit und erfordert das Aufwecken eines anderen Threads mit Object.notify();
  • Object.wait () hat Zeit. Wenn es nicht benachrichtigt wird, wird es automatisch aktiviert, wenn die Zeit abgelaufen ist. Zu diesem Zeitpunkt gibt es zwei Situationen: Eine besteht darin, dass die Sperre sofort erworben wird und der Thread natürlich weitermacht ausführen; der andere besteht darin, dass die Sperre nicht sofort erworben wird. Der Thread tritt in die Synchronisationswarteschlange ein und wartet auf den Erwerb der Sperre.

Tatsächlich besteht der größte Unterschied zwischen ihnen darin, dass Thread.sleep() keine Sperrressourcen freigibt, während Object.wait() Sperrressourcen freigibt.

5.2. Der Unterschied zwischen Object.wait() und Condition.await()

Die Prinzipien von Object.wait() und Condition.await() sind grundsätzlich gleich. Der Unterschied besteht darin, dass die unterste Ebene von Condition.await() LockSupport.park() aufruft, um den aktuellen Thread zu blockieren .

Tatsächlich werden vor dem Blockieren des aktuellen Threads zwei Dinge ausgeführt: Zum einen wird der aktuelle Thread zur Bedingungswarteschlange hinzugefügt, zum anderen wird die Sperre „vollständig“ aufgehoben , dh die Statusvariable wird auf 0 geändert und dann Rufen Sie LockSupport auf. .park() blockiert den aktuellen Thread .

5.3. Der Unterschied zwischen Thread.sleep() und LockSupport.park()

LockSupport.park() hat auch mehrere Brudermethoden – parkNanos(), parkUtil() usw. Die park()-Methode, über die wir hier sprechen, bezieht sich insgesamt auf diesen Methodentyp.

  • Funktionell gesehen sind die Methoden Thread.sleep() und LockSupport.park() ähnlich. Sie blockieren beide die Ausführung des aktuellen Threads und geben die vom aktuellen Thread belegten Sperrressourcen nicht frei.
  • Thread.sleep() kann nicht von außen aufgeweckt werden, es kann nur von selbst aufwachen;
  • Die LockSupport.park()-Methode kann durch einen anderen Thread aktiviert werden, der die LockSupport.unpark()-Methode aufruft;
  • Die Methodendeklaration Thread.sleep() löst eine InterruptedException aus, daher muss der Aufrufer diese Ausnahme abfangen oder erneut auslösen;
  • Die LockSupport.park()-Methode muss keine Interrupt-Ausnahmen abfangen;
  • Thread.sleep() selbst ist eine native Methode;
  • Die unterste Ebene von LockSupport.park() ist die native Methode von Unsafe namens ;

5.4. Der Unterschied zwischen Object.wait() und LockSupport.park()

Beide blockieren die Ausführung des aktuellen Threads. Was ist der Unterschied zwischen ihnen? Nach der obigen Analyse müssen Sie meiner Meinung nach ganz klar sein, stimmt das? Lesen Sie weiter!

  • Die Methode Object.wait() muss in einem synchronisierten Block ausgeführt werden;
  • LockSupport.park() kann überall ausgeführt werden;
  • Die Methode Object.wait() gibt an, dass eine Interrupt-Ausnahme ausgelöst wird und der Aufrufer sie abfangen oder erneut auslösen muss.
  • LockSupport.park() muss keine Interrupt-Ausnahmen abfangen;
  • Object.wait() hat keine Zeitüberschreitung und erfordert die Ausführung von notify() durch einen anderen Thread, um es aufzuwecken, aber es führt nicht unbedingt weiterhin nachfolgende Inhalte aus;
  • LockSupport.park() hat keine Zeitüberschreitung und erfordert die Ausführung von unpark() durch einen anderen Thread, um es aufzuwecken, und der nachfolgende Inhalt wird definitiv weiterhin ausgeführt;

Das zugrunde liegende Prinzip von park()/unpark() ist ein „binäres Semaphor“. Sie können es sich als ein Semaphor mit nur einer Lizenz vorstellen, mit der Ausnahme, dass dieses Semaphor keine weiteren Lizenzen hinzufügt, wenn unpark() wiederholt ausgeführt wird. Das gibt es höchstens eine Lizenz.

1. Was passiert, wenn notify() vor wait() ausgeführt wird?

Wenn der aktuelle Thread nicht der Eigentümer der Sperre dieses Objekts ist, aber die Methode notify() oder wait() des Objekts aufruft, wird eine IllegalMonitorStateException-Ausnahme ausgelöst.

Wenn der aktuelle Thread der Besitzer dieser Objektsperre ist, wird wait() immer blockieren, da es kein anderes notify() gibt, um ihn später zu aktivieren.

2. Was passiert, wenn unpark() vor park() ausgeführt wird?

Der Thread wird nicht blockiert. Park () wird direkt übersprungen und der nachfolgende Inhalt wird weiterhin ausgeführt.

5.5. Gibt LockSupport.park() Sperrressourcen frei?

Nein, es ist nur für das Blockieren des aktuellen Threads verantwortlich, und die Freigabe der Sperrressource wird tatsächlich in der Methode „await()“ von Condition implementiert.

6. Referenzartikel

おすすめ

転載: blog.csdn.net/qq_28959087/article/details/129813032