Thread-Grundlage und drei Hauptmerkmale der gleichzeitigen Programmierung

Inhaltsverzeichnis

1. Prozesse und Threads

1. Thread-Erstellungsmethode

 2. Thread-Status

3. Thread beenden

2. Parallelität wird zu drei Hauptmerkmalen

Sichtweite

Ordentlichkeit

Atomizität

synchronisiert

CAS-optimistische Sperre


1. Prozesse und Threads

        Klären Sie zunächst zwei grundlegende Konzepte: Was sind Prozesse und Threads?

        Prozess: Ein Prozess ist die Grundeinheit für die Ressourcenzuweisung durch das Betriebssystem, und ein laufendes Projekt ist ein Prozess.

        Thread: Thread ist die Grundeinheit der Planungsausführung, und ein Prozess besteht im Allgemeinen aus mehreren Threads.

        Eines der Ziele der gleichzeitigen Programmierung besteht darin, die hohe Leistung von Multicore-CPUs voll auszunutzen und die Reaktionszeit der Schnittstelle zu verbessern. Ist es also notwendig, dass eine Single-Core-CPU eine Multithread-Entwicklung durchführen kann? Die Antwort ist auf jeden Fall notwendig. Beispielsweise gibt es jetzt zwei Threads in einer Methode, ein Thread ist von Benutzereingaben abhängig und der andere Thread ist nicht von diesem Schritt abhängig. Wenn also auf Benutzereingaben gewartet wird, kann der andere Thread zuerst ausgeführt werden.

        Es ist zu beachten, dass die Anzahl der Threads nicht so hoch wie möglich ist. Sie muss entsprechend den relevanten Bedingungen der CPU und des Projekts ermittelt werden. Die Berechnungsformel lautet wie folgt, es handelt sich jedoch nur um eine Referenzgröße. Projektüberwachung und Erfahrung sollte durchgeführt werden, um die Anzahl der Threads zu bestimmen. :

        Anzahl Threads = Anzahl CPU-Kerne * CPU-Auslastung * (1+ Verhältnis von Wartezeit zu Rechenzeit)

1. Thread-Erstellungsmethode

Wenn die Thread-Erstellungsmethode unterteilt ist, gibt es die folgenden fünf Typen:

/**
     * 第一种,继承Thread类,重写run方法
     */
    static class MyThread extends Thread{
        @Override
        public void run(){
            System.out.println("create thread method one");
        }
    }
    /**
     * 第二种,实现Runnable接口,重写run方法,没有返回值
     */
    static class MyRun implements Runnable{
        @Override
        public void run() {
            System.out.println("create thread method TWO");
        }
    }
    /**
     * 第三种,实现Callable接口,重写call方法,有返回值
     */
    static class MyCall implements Callable<String>{
        @Override
        public String call() throws Exception {
            System.out.println("create thread method THREE");
            return "success";
        }
    }

    public static void main(String[] args) {
        // 调用线程start()方法,才是将线程启动,
        // 如果是调用run方法,那么相当于是调用一个类的普通方法
        new MyThread().start();
        new Thread(new MyRun()).start();
        // 第四种,使用lambda表达式,这种方式相当于是方法一的简写方式
        new Thread(() ->{
            System.out.println("create thread method FOUR");
        }).start();
        new Thread(new FutureTask<String>(new MyCall())).start();
        // 第五种,使用线程池创建
        ExecutorService service = Executors.newCachedThreadPool();
        service.execute(() -> {
            System.out.println("create thread method FIVE");
        });
        service.shutdown();
    }

Es ist zu beachten, dass nach dem Erstellen des Threads zum Starten des Threads die start()-Methode des aktuellen Threads aufgerufen werden muss, anstatt die run()-Methode aufzurufen. Der Aufruf der run()-Methode dient lediglich der Abwehr des Aufrufs gewöhnlicher Methoden .

 2. Thread-Status

        Thread-Zustände werden hauptsächlich in die folgenden sechs Typen unterteilt:

        NEU: Der Thread wurde gerade erstellt, aber die start()-Methode wurde nicht zum Starten aufgerufen;

        RUNNABLE: Ausführbarer Zustand, der in zwei Zustände unterteilt werden kann: READY und RUNNING. Der Unterschied zwischen den beiden besteht darin, ob die CPU die Zeitscheibe für die Ausführung zuweist. Wenn zugewiesen, handelt es sich um den RUNNING-Zustand, andernfalls um den READY-Zustand.

        WAITING: Wenn Sie darauf warten, geweckt zu werden, z. B. beim Aufrufen von wait (), join () und anderen Methoden, wechseln Sie in diesen Status.

        TIMED WAITING: Automatisches Aufwachen nach einer bestimmten Zeitspanne, z. B. beim Aufrufen der Sleep(2)-Methode, Wechseln Sie in diesen Zustand;

        BLOCKIERT: Blockierter Zustand. Wenn mehrere Threads um eine Sperre konkurrieren, befinden sie sich in diesem Zustand und warten auf die Aufhebung der Sperre.

        TERMINATED: Der Thread wird beendet und zerstört.

        Die Änderung des Thread-Status ist wie folgt:

        

3. Thread beenden

         Zusätzlich zum Abschluss des laufenden Threads und dem normalen Ende des Threads können auch die folgenden vier Möglichkeiten zum Beenden verwendet werden:

         1. Rufen Sie die stop()-Methode des Threads auf. Diese Methode wird jedoch nicht empfohlen. Sie wirkt sich auf die Transaktion aus und führt zu Dateninkonsistenzen. Beispielsweise wurde in einer Transaktion nicht die gesamte Transaktion ausgeführt, wenn die stop()-Methode ausgeführt wird aufgerufen wird, wird die vor stop() vorgenommene Datenänderung nicht rückgängig gemacht.

        2. suspend() zum Anhalten/resume() zum Fortsetzen. Dieses Befehlspaar wird nicht empfohlen, da die suspend()-Methode die Sperre nicht aufhebt und sie vom aktuellen Thread gehalten wird. Wenn die resume()-Methode dies getan hat nicht aufgerufen wurde, werden die Sperrressourcen niemals freigegeben.

        3. Verwenden Sie das Schlüsselwort volatile, um einen Statuscode zu ändern. Wenn der Statuscode nicht angegeben ist, führt der Thread das entsprechende Geschäftsprogramm aus. Wenn der Thread jedoch den angegebenen Statuscode erreicht, endet der Thread normal. Das mögliche Problem besteht jedoch darin, dass sich der aktuelle Thread, wenn er die Methode wait () ausführt, in einem Wartezustand befindet und zu diesem Zeitpunkt nicht in die nächste Schleife eintreten und nicht aus der Schleife springen und den Thread beenden kann.

        4. Verwenden Sie Interrupt, um den Thread zu beenden, indem Sie das Flag-Bit unterbrechen. Diese Methode wird mit dem Thread geliefert und ist besser als die Methode zum Ändern des Statuscodes mit dem Schlüsselwort volatile. Im Folgenden finden Sie eine Einführung in die Unterbrechung.

        interrupt(): Unterbrechen Sie den Thread. Hier müssen Sie auf „Interrupt“ achten. Es fügt lediglich ein Interrupt-Flag zum aktuellen Thread hinzu und unterbricht den Thread nicht wirklich.

        isInterrupt(): Ruft das Interrupt-Flag des aktuellen Threads ab

        statisch unterbrochen: Fragen Sie das Unterbrechungsflag des aktuellen Threads ab und setzen Sie es zurück.

        Zu beachten ist, dass der aktuelle Thread die Methoden wait () und sleep () ausführt. Zu diesem Zeitpunkt führt der aktuelle Thread die Methode interrupt () aus und es wird eine InterruptException ausgelöst. Der oben erwähnte Vorteil von interrupt () besteht darin, dass die Ausnahme durch Try Catch abgefangen wird und der Thread im Catch beendet wird, sodass der Thread nicht warten muss.

2. Parallelität wird zu drei Hauptmerkmalen

Sichtweite

        Die sogenannte Sichtbarkeit bezieht sich auf eine Variable, die von mehreren Threads gemeinsam genutzt wird. Wenn einer der Threads die Variable ändert, können andere Threads den geänderten Wert lesen.

        In Java kann das Schlüsselwort volatile verwendet werden, um die Sichtbarkeit von Variablen zu realisieren, wodurch sichergestellt werden kann, dass die Dateninformationen im Arbeitsspeicher und im Hauptspeicher konsistent sind. Es ist jedoch zu beachten, dass bei Verwendung des Schlüsselworts volatile zum Ändern des Referenztyps nur die Sichtbarkeit des gesamten Referenztyps, dh die Sichtbarkeit der Referenzadresse, nicht jedoch die Sichtbarkeit der Attribute garantiert werden kann der Felder im Referenztyp Zum Beispiel volatile T t = new T(), es gibt int a in T; dieses Attribut, wenn sich die Referenzadresse von t ändert, sind andere Threads sichtbar, aber wenn der Wert des Feldes Wenn sich etwas ändert, sind andere Threads nicht unbedingt sichtbar.

        Die Analyse der Sichtbarkeit auf CPU-Ebene bezieht sich auf die Sichtbarkeit von Cache- und Speicherdaten zwischen verschiedenen CPUs. Die CPU verfügt über einen dreistufigen Cache. Jeder Kern in der CPU verfügt über einen eigenen Cache der ersten Ebene und einen Cache der zweiten Ebene. Jede CPU verfügt über einen eigenen Cache der dritten Ebene. Die Sichtbarkeit in der CPU wird durch ein Cache-Kohärenzprotokoll (üblicherweise) erreicht Mesi-Protokoll). Es ist zu beachten, dass die Sichtbarkeit von Volatile nichts mit dem Cache-Kohärenzprotokoll zu tun hat. Kombinieren Sie es zum besseren Verständnis mit der folgenden Abbildung

        

 ( Cache-Zeile: Eine Cache-Zeile bezieht sich auf die Basiseinheit, die aus dem Speicher in den Cache gelesen wird, mit einer Größe von 64 Byte. Die enthaltene Annotation kann sicherstellen, dass ein Feld mit einer Länge von weniger als 64 Byte als Cache-Zeile gelesen wird, aber bei Verwendung Diese Anmerkung muss zur Laufzeit hinzugefügt werden -XX:-RestrictContended. Diese Anmerkung funktioniert nach 1.9 nicht mehr. )

Ordentlichkeit

        Die gesamte Wortreihenfolge im Programm entspricht nicht unserer Meinung nach einer einzigen Operation. Sie besteht aus mehreren Anweisungen auf der zugrunde liegenden Bytecode-Ebene. Um die Ausführungseffizienz zu verbessern, hat die CPU keinen Einfluss auf die endgültige Konsistenz einzelner Befehle. Thread-Ausführung. Die Anweisungen werden unter der Prämisse neu angeordnet. Hier ein typisches Beispiel zur Veranschaulichung:

        Object object = new Object(); Diese Anweisung kann als eine Operation zum Erstellen eines Objektobjekts in einem Java-Programm betrachtet werden. Wie wird sie also auf Bytecode-Ebene implementiert, wie in der folgenden Abbildung dargestellt (mithilfe des jcalssLib-Plug-Ins)? :

Es besteht aus den oben genannten fünf Anweisungen, und die Bedeutung der spezifischen Anweisungen kann im offiziellen Dokument eingesehen werden. Es ist grob in drei Schritte unterteilt: 1) Speicherplatz beantragen und Standardwerte zuweisen; 2) Konstruktionsmethode ausführen, um Anfangswerte zuzuweisen; 3) Speicher auf das Objekt richten. Wenn die CPU ausgeführt wird, folgt sie nicht unbedingt der Reihenfolge 1->2->3, und die Ausführungsreihenfolge kann 1->3->2 werden. In einem einzelnen Thread gibt es kein Problem, da das Endergebnis dies ist ein Objekt, dem ein Anfangswert zugewiesen ist. Wenn es sich jedoch um Multithreading handelt, kann es zu Problemen kommen. Nehmen Sie den folgenden DCL-Code als Beispiel:

public class Single {
    private volatile static Single SINGLE;
    private Single(){ }
    public static Single getInstance(){
        if (SINGLE == null){
            synchronized (Single.class){
                if (SINGLE == null){
                    SINGLE = new Single();
                }
            }
        }
        return SINGLE;
    }
}

 Ist das Schlüsselwort volatile im obigen Code notwendig? Die Antwort lautet: Ja, wenn kein solches Schlüsselwort vorhanden ist, liegt möglicherweise eine Ausführung außerhalb der Reihenfolge vor, die zu Problemen im Endergebnis führen kann. Jetzt gibt es zwei Threads, ein Thread führt die Methode SINGLE = new Single () aus und Der andere Thread führt die erste leere Beurteilung aus. Wenn das neu erstellte Objekt nicht in Ordnung ist, führen Sie Schritt 3 in der Reihenfolge 1->3->2 aus, und das Cashback-Objekt des zweiten Threads ist nicht leer, es wird SINGLE verwendet Objekt, das nur den Standardindex bedient, kann es zu Problemen kommen, daher ist das Schlüsselwort volatile in DCL ein Muss.

        Wie wird also die Bestellung garantiert (Details siehe oben):

        Hardwareebene: CPU verwendet Speicherbarrieren auf CPU-Ebene, um Folgendes zu implementieren: lfence, sfence, mfence;

        JVM-Ebene: Die virtuelle Maschine nutzt die Speicherbarriere auf der Ebene der virtuellen Maschine, um Folgendes zu realisieren: LoadLoad, StoreStore, LoadStore, StoreLoad. Es ist zu beachten, dass die Speicherbarriere auf der Ebene der virtuellen Maschine durch den Aufruf verwandter Anweisungen auf Hardwareebene, z. B. der Sperranweisung, vervollständigt wird.

Atomizität

        Bevor Sie die Atomizität verstehen, verstehen Sie zunächst einige Konzepte: Rennbedingung: Konkurrenzbedingung, Konkurrenz tritt auf, wenn mehrere Threads auf gemeinsam genutzte Daten zugreifen; Monitor: Sperre; kritischer Abschnitt: kritischer Abschnitt, Code wird ausgeführt, wenn ein Thread eine Sperre hält; Sperrgranularität: Die Ausführungszeit Der kritische Abschnitt ist lang und enthält viele Anweisungen, was bedeutet, dass die Granularität der Sperre relativ grob ist. Andernfalls ist die Granularität der Sperre relativ fein.

        Die sogenannte Atomizität bedeutet, dass die Ausführung dieses Codes nicht unterbrochen werden kann. Bevor der aktuelle Thread mit der Ausführung fertig ist, können andere Threads die Ausführung des Thread-Codes nicht unterbrechen. Atomarität kann durch Sperren des Codes gewährleistet werden.

        Es gibt zwei Arten von Schlössern:

        1. Pessimistische Sperre: Hauptsächlich synchronisiert, unabhängig davon, ob die Sperre auftritt oder nicht, wird sie direkt gesperrt.

        2. Optimistische Sperre: Wird durch die CAS-Spin-Sperre dargestellt und stellt zunächst standardmäßig fest, dass der aktuelle Code nicht gesperrt werden muss. Wenn er jedoch endgültig übermittelt wird, wird beurteilt, ob ein Thread-Wettbewerb vorliegt. Wenn kein Thread-Wettbewerb vorliegt, wird der Die Ausführung endet; andernfalls muss sie gesperrt werden. Erneut ausführen.

        In Bezug auf das Konzept der beiden oben genannten Sperren gibt es eigene geeignete Verwendungsszenarien: Aufgrund der Cas-Spin-Sperre muss sich der Thread drehen und warten, wodurch CPU-Ressourcen verschwendet werden, sodass die Ausführungszeit des kritischen Abschnitts lang ist Wenn viele Threads darum konkurrieren, verwenden Sie die synchronisierte pessimistische Sperre. Wenn die Ausführungszeit des kritischen Abschnitts kurz ist und nur wenige konkurrierende Threads vorhanden sind, verwenden Sie die optimistische CAS-Sperre.

        Synchronisiert und CAS wurden oben erwähnt, und dann werden diese beiden Sperren eingeführt.

synchronisiert

        Synchronized kann Sichtbarkeit und Atomizität garantieren, aber keine Ordnung garantieren. Vor der Optimierung handelt es sich um eine Schwergewichtssperre, und jede Sperre wird vom Betriebssystem vervollständigt. Nach der Optimierung ist sie effizienter, da keine Sperre mehr für eine Sperre auf den Vorgang angewendet wird und eine Reihe von Sperraktualisierungsprozessen durchgeführt werden.

        Bevor Sie den Sperraktualisierungsprozess einleiten, müssen Sie wissen, dass die relevanten Informationen der Sperre im Markierungswort aufgezeichnet sind. Daher sind wir auch verpflichtet, das Referenzobjekt nicht zu ändern, wenn wir ein Referenzobjekt als Sperre verwenden, um dies zu vermeiden Eine Änderung des Markierungsworts des Objekts führt dazu, dass die Sperre ungültig wird.

        Der normale Sperraktualisierungsprozess ist synchronisiert: keine Sperre -> vorgespannte Sperre -> leichte Sperre (Spin-Sperre) -> schwere Sperre. Darunter befinden sich voreingenommene Sperren und leichte Sperren im Benutzerbereich, während schwere Sperren für den Kernelbereich gelten.

        Wenn ein Thread den kritischen Abschnitt der synchronisierten Sperre ausführt, wird das sperrenfreie Objekt zu einer voreingenommenen Sperre aktualisiert, und der aktuelle Thread-Zeiger wird im Markierungswort der Sperre aufgezeichnet und es werden keine anderen Vorgänge ausgeführt. Der Hashcode Der im Markierungswort aufgezeichnete Wert wird in den Thread-Stack-Datensatz eingefügt. Wenn die Ausführung des Threads nicht abgeschlossen ist und weniger als 10 Threads vorhanden sind, die weniger als die Hälfte der Anzahl der CPU-Kerne ausmachen, die um die Sperre konkurrieren, wird die Voreingenommenheit angezeigt Die Sperre wird auf eine leichte Sperre aktualisiert. Zu diesem Zeitpunkt wird die Sperre der voreingenommenen Sperre zuerst entfernt. Die konkurrierenden Threads generieren LR (Sperrdatensatz) in ihrem eigenen Thread-Stapel. Wer auch immer sein LR auf dem Sperrmarkierungswort aufzeichnet Der erste erhält die voreingenommene Sperre, und die anderen Threads beginnen sich zu drehen und zu warten. Nach mehr als 10 konkurrierenden Threads wird die leichte Sperre auf eine schwere Sperre aktualisiert. Zu diesem Zeitpunkt wird eine Sperranwendung für das Betriebssystem erstellt. und die Threads, die um die Sperre konkurrieren, werden in die entsprechende Warteschlange des Betriebssystems gestellt, und der Thread, der die Sperre erhalten hat, läuft normal. Und andere Threads werden in der Warteschlange gezählt, die auf die Freigabe der Sperre wartet. Das Obige ist der gesamte Prozess der Schlossaktualisierung. Dieser Vorgang ist natürlich nicht erforderlich. In einigen Sonderfällen wird der Sperraktualisierungsprozess geändert.

        Wenn die voreingenommene Sperre nicht aktiviert wurde, lautet der Sperraktualisierungsprozess: sperrenfreies Objekt -> leichte Sperre -> schwere Sperre. Mit dem Parameter -XX:BiasedLockingStartupDelay=4000 können Sie die Verzögerungszeit für den Start der voreingenommenen Sperre festlegen. Der Standardwert beträgt 4 Sekunden.

        Synchronized ist eine Wiedereintrittssperre. Die sogenannte Wiedereintrittssperre bedeutet, dass Thread 1 die Sperre a erhält und die Sperre a erneut erwerben muss, bevor die Sperre freigegeben wird. Wenn dies zulässig ist, handelt es sich um eine Wiedereintrittssperre. Wenn dies nicht zulässig ist, ist dies der Fall nicht wiedereintrittsfähige Sperre. Da es sich bei der Synchronisierung um eine Wiedereintrittssperre handelt, muss die Anzahl der Wiedereintritte aufgezeichnet werden, da entsprechend der Anzahl der Wiedereintritte entsprechend oft entsperrt werden muss. Bei voreingenommenen Sperren und leichten Sperren wird die Anzahl der Wiedereintritte aufgezeichnet, indem LR in den Thread-Stack eingefügt wird. Schwere Sperren zeichnen Wiedereintrittsinformationen im Betriebssystem auf.

CAS-optimistische Sperre

        Die Spin-Sperre cas (comapre und swap) wird im Benutzerbereich ausgeführt und beansprucht keinen Kernel-Bereich. Einer seiner allgemeinen Arbeitsabläufe ist die erweiterte Codeausführung für kritische Abschnitte. Nach Abschluss der Ausführung wird der aktuelle Wert mit dem erwarteten Wert verglichen. Wenn die Prüfung bestanden wird, wird der aktuelle Wert auf den erwarteten Wert aktualisiert. Nach dem Ende der Ausführung, falls dies der Fall ist Inkonsistent, der obige Prozess wird in einer Schleife ausgeführt. Wie nachfolgend dargestellt:

Bei der Verwendung von CAS liegt möglicherweise ein ABA-Problem vor, dh das vom aktuellen Thread erhaltene C ist das aktualisierte C, nachdem andere Threads verarbeitet wurden. Um dieses Problem zu lösen, kann es durch Hinzufügen einer Versionsnummer gelöst werden. Wenn es sich um einen Basisdatentyp handelt, wirkt sich das ABA-Problem möglicherweise nicht auf den normalen Geschäftsbetrieb aus. Es ist jedoch zu beachten, dass beim Vergleich nicht nur die Adressen verglichen werden können, sondern auch die Adressen neu verglichen werden müssen, wenn es sich um einen Referenzobjekttyp handelt Methode, sonst treten Probleme auf.

        atomare Atomklasse, die zugehörigen Operationen darin sind atomar und ihre Atomizität wird durch die Verwendung einer optimistischen CAS-Sperre realisiert. Sein CAS hängt von der Implementierung der Assembleranweisung cmpxchg ab. Wenn es sich um eine Multi-Core-CPU handelt, müssen Sie zum Sperren eine Sperranweisung hinzufügen, da die cmpxchg-Anweisung selbst nicht atomar ist und daher gesperrt werden muss.

Ich denke du magst

Origin blog.csdn.net/weixin_38612401/article/details/123916565
Empfohlen
Rangfolge