15.000 Wörter, 6 Codefälle und 5 schematische Diagramme ermöglichen es Ihnen, Synchronized gründlich zu verstehen

Synchronisiert

Dieser Artikel konzentriert sich auf das synchronisierte Schlüsselwort und verwendet eine große Anzahl von Bildern und Fällen, um die Implementierung von CAS, die synchronisierte Java-Ebene und die C++-Ebene, das Prinzip der Sperraktualisierung, den Quellcode usw. zu beschreiben.

Ungefähre Betrachtungszeit: 17 Minuten

Sie können diesen Artikel mit ein paar Fragen lesen. Wenn Sie ihn sorgfältig lesen, werden die Probleme gelöst:

1. Wie wird synchronisiert verwendet? Wie wird es auf Java-Ebene implementiert?

2. Was ist CAS? Welche Vorteile kann es bringen? Was sind die Nachteile?

3. Was ist das Markenwort? Was hat das mit synchronisiert zu tun?

4. Was ist die Lock-Upgrade-Optimierung von synchronisiert? Wie implementiert man es auf C++-Ebene?

5. Wird sich das Lightweight-Lock-CAS in JDK 8 drehen, wenn es ausfällt?

6. Was ist ein Objektmonitor? Wie wird die Wait/Notify-Methode implementiert? Wie werden Threads nach dem Blockieren in der Blockierungswarteschlange sortiert, wenn synchronisiert verwendet wird?

...

Synchronisierte Implementierung auf Java-Ebene

synchronisiert wirkt auf Codeblöcke oder Methoden, um den Synchronisationsmechanismus in einer gleichzeitigen Umgebung sicherzustellen.

Jeder Thread, der auf eine Synchronisierung stößt, muss zuerst die Sperre erwerben, bevor er Vorgänge im Codeblock oder in der Methode ausführen kann.

In Java verfügt jedes Objekt über ein entsprechendes Überwachungsobjekt (Monitor). Wenn die Sperre von Objekt A erworben wird, zeigt ein Feld im Überwachungsobjekt von Objekt A auf den aktuellen Thread und zeigt an, dass dieser Thread die Sperre von Objekt A erworben hat . (Detaillierte Prinzipien werden später beschrieben)

synchronisiert kann für normale Objekte und statische Objekte verwendet werden. Bei Verwendung für statische Objekte und statische Methoden erhält es die Sperre des entsprechenden Klassenobjekts.

Wenn synchronisiert auf einen Codeblock einwirkt, werden die Bytecodeanweisungen „monitorentry“ und „monitorexit“ verwendet, um das Sperren und Entsperren zu identifizieren.

Wenn synchronisiert auf eine Methode angewendet wird, wird synchronisiert zum Zugriffsbezeichner hinzugefügt.

Die Anweisung kann zwei Monitorexit-Anweisungen enthalten, da Monitorexit beim Auftreten einer Ausnahme automatisch ausgeführt wird, um sie zu entsperren.

Der normale Prozess ist PC 12-14. Wenn in diesem Zeitraum eine Ausnahme auftritt, springt er zu PC 17 und führt schließlich Monitorexit auf PC 19 aus, um ihn zu entsperren.

        Object obj = new Object();
        synchronized (obj) {
        }

Bild.png

Im vorherigen Artikel haben wir über Atomizität, Sichtbarkeit und Ordnung gesprochen

Die synchronisierten Bytecode-Anweisungen zum Sperren und Entsperren verwenden Barrieren. Beim Sperren wird der gemeinsam genutzte Speicher erneut aus dem Hauptspeicher gelesen. Vor dem Entsperren werden die Arbeitsspeicherdaten in den Hauptspeicher zurückgeschrieben, um die Sichtbarkeit sicherzustellen.

Da die Ausführung nach dem Erwerb der Sperre der seriellen Ausführung entspricht, sind Atomizität und Ordnung gewährleistet. Es ist zu beachten, dass die Anweisungen zwischen Sperren und Entsperren weiterhin neu angeordnet werden können.

CAS

Um das Synchronisationsprinzip und das Sperren-Upgrade besser zu erklären, sprechen wir zunächst über CAS

Wie wir im vorherigen Artikel gesagt haben, kann Volatile die Atomizität zusammengesetzter Operationen nicht garantieren. Die Verwendung der synchronisierten Methode oder von CAS kann die Atomizität zusammengesetzter Operationen sicherstellen.

Was ist CAS?

Der vollständige Name von CAS lautet Compare And Swap. Wenn Sie die Daten nach dem Lesen ändern möchten, vergleichen Sie die gelesenen Daten mit dem Wert an der Adresse. Wenn sie gleich sind, ersetzen Sie den Wert an der Adresse durch den Zielwert. Wenn ja Wenn sie nicht gleich sind, wird sie normalerweise zurückgesetzt. Lesen Sie die Daten und führen Sie dann den CAS-Vorgang aus. Dies bedeutet, dass Sie es nach einem Fehler erneut versuchen müssen.

Synchronisiertes Sperren ist eine pessimistische Strategie. Jedes Mal, wenn Sie darauf stoßen, glauben Sie, dass ein Parallelitätsproblem vorliegt, und müssen vor dem Betrieb zunächst die Sperre erwerben.

CAS ist eine optimistische Strategie. Jedes Mal, wenn Sie mutig vorgehen und der Vorgang fehlschlägt (CAS-Fehler), verwenden Sie Ausgleichsmaßnahmen (Wiederholung nach Fehler).

Die Kombination aus CAS und fehlgeschlagenem Wiederholungsversuch (Schleife) stellt eine optimistische Sperre oder Spin-Sperre dar (Schleifenversuche sind der Selbstrotation sehr ähnlich).

Die atomaren Klassen im Concurrent-Paket verlassen sich auf Unsafe, um eine große Anzahl von CAS-Operationen zu verwenden, beispielsweise die automatische Inkrementierung von AtomicInteger.

    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
    //var1是调用方法的对象,var2是需要读取/修改的值在这个对象上的偏移量,var4是自增1
    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            //var5是通过对象和字段偏移量获取到字段最新值
            var5 = this.getIntVolatile(var1, var2);
            //cas:var1,var2找到字段的值 与 var5比较,相等就替换为 var5+var4 
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
        return var5;
    }

CAS kann nur eine Variable bearbeiten. Wenn Sie mehrere Variablen bearbeiten möchten, können Sie nur eine Ebene kapseln (mehrere Variablen als Felder eines neuen Objekts kapseln) und diese dann verwendenAtomicReference

Ich frage mich, ob jemand von Ihnen entdeckt hat, dass es einen Fehler im CAS-Prozess gibt, d. h. zwischen dem Lesen von Daten und dem Vergleichen von Daten. Wenn die Daten von A nach B und dann nach A geändert werden, kann CAS auch erfolgreich ausgeführt werden .

Einige Unternehmen können dieses Szenario akzeptieren, andere nicht. Dies ist das sogenannte ABA-Problem.

Die Lösung des ABA-Problems ist relativ einfach. Sie können beim Vergleich eine automatisch inkrementierte Versionsnummer anhängen. JDK bietet auch atomare Klassen zur Lösung des ABA-Problems.AtomicStampedReference

CAS kann das Blockieren von Threads vermeiden, aber wenn es weiterhin fehlschlägt, wird die Schleife fortgesetzt, was den CPU-Overhead erhöht. Die Anzahl/Dauer der Wiederholungsversuche nach einem CAS-Fehler ist schwer zu bewerten.

Daher eignet sich der CAS-Betrieb für Szenarien mit geringem Wettbewerb. Der Overhead des CPU-Leerlaufs wird gegen den Overhead der Thread-Blockierung und Suspendierung/Wiederaufnahme ausgetauscht.

Schloss-Upgrade

Frühere Versionen von synchronisierten Threads, die die Sperre nicht erhalten können, werden direkt angehalten, was zu einer schlechten Leistung führt.

Die Implementierung von Synchronized ist in JDK 6 optimiert, d. h. Lock-Upgrade

Der Sperrstatus kann in keine Sperre, voreingenommene Sperre, leichte Sperre und schwere Sperre unterteilt werden.

Wir können schwere Sperren vorübergehend so verstehen, dass der Thread hängen bleibt, wenn die Sperre nicht frühzeitig erhalten werden kann. Die neue Optimierung besteht aus leichten Sperren und voreingenommenen Sperren.

Wort markieren

Um das Sperren-Upgrade besser zu erklären, sprechen wir zunächst über das Markierungswort im Java-Objektheader.

Bei unseren folgenden Erkundungen dreht sich alles um virtuelle 64-Bit-Maschinen.

Der Speicher eines Java-Objekts besteht aus Markierungswort und Klassifizierungswort. Wenn es sich um ein Array handelt, zeichnen Sie die Länge und die Instanzdaten (Feld) auf und füllen Sie es (füllen Sie es auf ein Vielfaches von 8 Bytes).

Das Markierungswort zeichnet den Sperrstatus auf, und die aufgezeichneten Daten unterscheiden sich je nach Sperrstatus.

Die folgende Tabelle zeigt den Inhalt der Markierungswortdatensätze von „keine Sperre“ bis „schwere Sperre“.

|----------------------------------------------------------------------|--------|--------|
| unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1  | lock:2 | 无锁   
|----------------------------------------------------------------------|--------|--------|
|  thread:54 |         epoch:2      | unused:1 | age:4 | biased_lock:1 | lock:2 | 偏向锁
|----------------------------------------------------------------------|--------|--------|
|                     ptr_to_lock_record:62                            | lock:2 | 轻量级锁
|----------------------------------------------------------------------|--------|--------|
|                     ptr_to_heavyweight_monitor:62                    | lock:2 | 重量级锁
|----------------------------------------------------------------------|--------|--------|

unbenutzt bedeutet noch nicht genutzt

Identity_hashcode wird verwendet, um konsistentes Hashing aufzuzeichnen

Das Alter wird zur Aufzeichnung des GC-Alters verwendet

„biased_lock“ gibt an, ob die voreingenommene Sperre verwendet werden soll. 0 bedeutet nicht aktiviert, 1 bedeutet aktiviert

Sperre wird verwendet, um das Sperrstatus-Flag zu identifizieren, 01 keine Sperre oder voreingenommene Sperre, 00 leichte Sperre, 10 schwere Sperre

Thread wird verwendet, um den voreingenommenen Thread zu identifizieren

Epochendatensatz-Bias-Zeitstempel

ptr_to_lock_record zeichnet den Sperrdatensatz im Stapelrahmen auf (später beschrieben).

ptr_to_heavyweight_monitor zeichnet den Thread auf, der die Schwergewichtssperre erhält

jolView Markierungswort

Schüler, die mit Markierungswörtern besser vertraut sind, können es überspringen.

Nachdem wir das Markierungswort verstanden haben, machen wir uns mit dem Markierungswort in verschiedenen Sperrzuständen vertraut. Ich verwende jol, um den Speicher anzuzeigen.

       <!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-core -->
        <dependency>
            <groupId>org.openjdk.jol</groupId>
            <artifactId>jol-core</artifactId>
            <version>0.12</version>
        </dependency>
kein Schloss

Das von den Schülern während des Experiments verwendete Markierungswort unterscheidet sich möglicherweise von dem in meinen Kommentaren. Wir überprüfen hauptsächlich den Wert der Sperrkennung und ob die voreingenommene Sperre aktiviert ist.

Bild.png

    public void noLock() {
        Object obj = new Object();
        //mark word  00000001 被unused:1,age:4,biased_lock:1,lock:2使用,001表示0未启用偏向锁,01表示无锁
        //01 00 00 00  (00000001 00000000 00000000 00000000)
        //00 00 00 00  (00000000 00000000 00000000 00000000)
        ClassLayout objClassLayout = ClassLayout.parseInstance(obj);
        System.out.println(objClassLayout.toPrintable());
        //计算一致性哈希后
        //01 b6 ce a8
        //6a 00 00 00
        obj.hashCode();
        System.out.println(objClassLayout.toPrintable());
        //进行GC 查看GC年龄 0 0001 0 01 前2位表示锁状态01无锁,第三位biased_lock为0表示未启用偏向锁,后续四位则是GC年龄age 1
        //09 b6 ce a8 (00001001 10110110 11001110 10101000)
        //6a 00 00 00 (01101010 00000000 00000000 00000000)
        System.gc();
        System.out.println(objClassLayout.toPrintable());
    }
leichtes Schloss
    public void lightLockTest() throws InterruptedException {
        Object obj = new Object();
        ClassLayout objClassLayout = ClassLayout.parseInstance(obj);
        //1334729950
        System.out.println(obj.hashCode());
        //0 01 无锁
        //01 4e c0 d5 (00000001 01001110 11000000 11010101)
        //6a 00 00 00 (01101010 00000000 00000000 00000000)
        System.out.println(Thread.currentThread().getName() + ":");
        System.out.println(objClassLayout.toPrintable());
        Thread thread1 = new Thread(() -> {
            synchronized (obj) {
                // 110110 00 中的00表示轻量级锁其他62位指向拥有锁的线程
                //d8 f1 5f 1d (11011000 11110001 01011111 00011101)
                //00 00 00 00 (00000000 00000000 00000000 00000000)
                System.out.println(Thread.currentThread().getName() + ":");
                System.out.println(objClassLayout.toPrintable());
                //1334729950
                //无锁升级成轻量级锁后 hashcode未变 对象头中没存储hashcode 只存储拥有锁的线程
                //(实际上mark word内容被存储到lock record中,所以hashcode也被存储到lock record中)
                System.out.println(obj.hashCode());
            }
        }, "t1");
        thread1.start();
        //等待t1执行完 避免 发生竞争
        thread1.join();
        //轻量级锁 释放后 mark word 恢复成无锁 存储哈希code的状态
        //01 4e c0 d5 (00000001 01001110 11000000 11010101)
        //6a 00 00 00 (01101010 00000000 00000000 00000000)
        System.out.println(Thread.currentThread().getName() + ":");
        System.out.println(objClassLayout.toPrintable());
        Thread thread2 = new Thread(() -> {
            synchronized (obj) {
                //001010 00 中的00表示轻量级锁其他62位指向拥有锁的线程
                //28 f6 5f 1d (00101000 11110110 01011111 00011101)
                //00 00 00 00 (00000000 00000000 00000000 00000000)
                System.out.println(Thread.currentThread().getName() + ":");
                System.out.println(objClassLayout.toPrintable());
            }
        }, "t2");
        thread2.start();
        thread2.join();
    }
Vorspannungssperre
    public void biasedLockTest() throws InterruptedException {
        //延迟让偏向锁启动
        Thread.sleep(5000);
        Object obj = new Object();
        ClassLayout objClassLayout = ClassLayout.parseInstance(obj);
        //1 01 匿名偏向锁 还未设置偏向线程
        //05 00 00 00 (00000101 00000000 00000000 00000000)
        //00 00 00 00 (00000000 00000000 00000000 00000000)
        System.out.println(Thread.currentThread().getName() + ":");
        System.out.println(objClassLayout.toPrintable());
        synchronized (obj) {
            //偏向锁 记录 线程地址
            //05 30 e3 02 (00000101 00110000 11100011 00000010)
            //00 00 00 00 (00000000 00000000 00000000 00000000)
            System.out.println(Thread.currentThread().getName() + ":");
            System.out.println(objClassLayout.toPrintable());
        }
        Thread thread1 = new Thread(() -> {
            synchronized (obj) {
                //膨胀为轻量级 0 00 0未启用偏向锁,00轻量级锁
                //68 f4 a8 1d (01101000 11110100 10101000 00011101)
                //00 00 00 00 (00000000 00000000 00000000 00000000)
                System.out.println(Thread.currentThread().getName() + ":");
                System.out.println(objClassLayout.toPrintable());
            }
        }, "t1");
        thread1.start();
        thread1.join();
    }
Schweres Schloss
    public void heavyLockTest() throws InterruptedException {
        Object obj = new Object();
        ClassLayout objClassLayout = ClassLayout.parseInstance(obj);
        Thread thread1 = new Thread(() -> {
            synchronized (obj) {
                //第一次 00 表示 轻量级锁
                //d8 f1 c3 1e (11011000 11110001 11000011 00011110)
                //00 00 00 00 (00000000 00000000 00000000 00000000)
                System.out.println(Thread.currentThread().getName() + ":");
                System.out.println(objClassLayout.toPrintable());
                //用debug控制t2来竞争
                //第二次打印 变成 10 表示膨胀为重量级锁(t2竞争)  其他62位指向监视器对象
                //fa 21 3e 1a (11111010 00100001 00111110 00011010)
                //00 00 00 00 (00000000 00000000 00000000 00000000)
                System.out.println(Thread.currentThread().getName() + ":");
                System.out.println(objClassLayout.toPrintable());
            }
        }, "t1");
        thread1.start();
        Thread thread2 = new Thread(() -> {
            synchronized (obj) {
                //t2竞争 膨胀为 重量级锁 111110 10 10为重量级锁
                //fa 21 3e 1a (11111010 00100001 00111110 00011010)
                //00 00 00 00 (00000000 00000000 00000000 00000000)
                System.out.println(Thread.currentThread().getName() + ":");
                System.out.println(objClassLayout.toPrintable());
            }
        }, "t2");
        thread2.start();
        thread1.join();
        thread2.join();
        //10 重量级锁 未发生锁降级
        //3a 36 4d 1a (00111010 00110110 01001101 00011010)
        //00 00 00 00 (00000000 00000000 00000000 00000000)
        System.out.println(Thread.currentThread().getName() + ":");
        System.out.println(objClassLayout.toPrintable());
    }

leichtes Schloss

Es werden leichte Sperren vorgeschlagen , um den Overhead zu reduzieren, der durch herkömmliche schwere Sperren mithilfe von Mutexes (Suspendierungs-/Wiederaufnahme-Threads) verursacht wird.

Bei weniger konkurrierenden Szenarien ist die Zeit zum Erlangen der Sperre immer kurz und der Overhead beim Anhalten von Threads im Benutzermodus und Kernel-Modus ist relativ groß. Verwenden Sie leichte Sperren, um den Overhead zu reduzieren.

Wie wird also eine leichte Sperre implementiert?

Leichte Sperren werden hauptsächlich durch Sperrdatensätze, Markierungswörter und CAS implementiert. Sperrdatensätze werden im Stapelrahmen des Threads gespeichert, um Sperrinformationen aufzuzeichnen.

Sperren

Überprüfen Sie, ob sich das Objekt in einem sperrfreien Zustand befindet. Wenn sich das Objekt in einem sperrfreien Zustand befindet, wird das Markierungswort in das verschobene Markierungswort im Sperrdatensatz kopiert.

Bild.png

Versuchen Sie dann , mit CAS einen Teil des Inhalts im Markierungswort zu ersetzen und auf diesen Sperrdatensatz zu verweisen . Wenn dies gelingt, bedeutet dies, dass die Sperre erfolgreich erworben wurde.

Bild.png

Wenn das Objekt eine Sperre hält, prüft es, ob der Thread, der die Sperre hält, der aktuelle Thread ist. In diesem Fall des Wiedereintritts ist der Datensatz im Sperrdatensatz nicht mehr das Markierungswort, sondern null.

Im Falle eines Wiedereintritts müssen Sie nur eine automatische Inkrementzählung durchführen. Beim Entsperren wird ein Null-Sperre-Datensatz abgezogen.

Bild.png

Wenn CAS fehlschlägt oder der Thread, der die Sperre hält, nicht der aktuelle Thread ist, wird die Sperrenerweiterung ausgelöst.

Der Schlüsselcode lautet wie folgt:

void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {
  //当前对象的mark word
  markOop mark = obj->mark();
  assert(!mark->has_bias_pattern(), "should not see bias pattern here");
  //如果当前对象是无锁状态 
  if (mark->is_neutral()) {
    //将mark word复制到lock record
    lock->set_displaced_header(mark);
    //CAS将当前对象的mark word内容替换为指向lock record
    if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) {
      TEVENT (slow_enter: release stacklock) ;
      return ;
    }
  } else
  //如果有锁  判断是不是当前线程获取锁
  if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) {
    assert(lock != mark->locker(), "must not re-lock the same lock");
    assert(lock != (BasicLock*)obj->mark(), "don't relock with same BasicLock");
    //可重入锁 复制null
    lock->set_displaced_header(NULL);
    return;
  }
  //有锁并且获取锁的线程不是当前线程 或者 CAS失败 进行膨胀
  lock->set_displaced_header(markOopDesc::unused_mark());
  ObjectSynchronizer::inflate(THREAD, obj())->enter(THREAD);
}
Freischalten

Überprüfen Sie, ob der kopierte Inhalt im Sperrdatensatz leer ist. Wenn er leer ist, bedeutet dies, dass es sich um eine Wiedereintrittssperre handelt.

Wenn es nicht leer ist, prüfen Sie, ob das Markierungswort auf den Sperrdatensatz verweist. Wenn dies der Fall ist, versucht CAS, den Markierungswortdatensatz, der auf den Sperrdatensatz verweist, durch das verschobene Markierungswort im Sperrdatensatz (d. h. die ursprüngliche Markierung) zu ersetzen Wort).

Wenn das Markierungswort nicht auf den Sperrdatensatz verweist oder CAS fehlschlägt, bedeutet dies, dass Konkurrenz besteht. Wenn andere Threads nicht sperren können, verweist das Markierungswort auf die Schwergewichtssperre, die direkt erweitert wird.

Bild.png

Der Schlüsselcode lautet wie folgt:

void ObjectSynchronizer::fast_exit(oop object, BasicLock* lock, TRAPS) {
  assert(!object->mark()->has_bias_pattern(), "should not see bias pattern here");
  //获取复制的mark word
  markOop dhw = lock->displaced_header();
  markOop mark ;
  //如果为空 说明是可重入
  if (dhw == NULL) {
     // Recursive stack-lock.
     // Diagnostics -- Could be: stack-locked, inflating, inflated.
     mark = object->mark() ;
     assert (!mark->is_neutral(), "invariant") ;
     if (mark->has_locker() && mark != markOopDesc::INFLATING()) {
        assert(THREAD->is_lock_owned((address)mark->locker()), "invariant") ;
     }
     if (mark->has_monitor()) {
        ObjectMonitor * m = mark->monitor() ;
        assert(((oop)(m->object()))->mark() == mark, "invariant") ;
        assert(m->is_entered(THREAD), "invariant") ;
     }
     return ;
  }
  mark = object->mark() ;
  //如果mark word指向lock record
  if (mark == (markOop) lock) {
     assert (dhw->is_neutral(), "invariant") ;
     //尝试CAS将指向lock record的mark word替换为原来的内容
     if ((markOop) Atomic::cmpxchg_ptr (dhw, object->mark_addr(), mark) == mark) {
        TEVENT (fast_exit: release stacklock) ;
        return;
     }
  }
  //未指向当前lock record或者CAS失败则膨胀
  ObjectSynchronizer::inflate(THREAD, object)->exit (true, THREAD) ;
}

Vorspannungssperre

Hotspot-Entwickler haben getestet, dass in einigen Szenarien immer derselbe Thread die Sperre erhält. In diesem Szenario hofft man, die Sperre mit weniger Overhead zu erhalten.

Wenn die voreingenommene Sperre aktiviert ist und sie sich in einem sperrfreien Zustand befindet, wird das Markierungswort geändert, um eine bestimmte Thread-ID vorzuspannen, um diesen Thread zu identifizieren und die Sperre zu erhalten (die Sperre ist auf diesen Thread ausgerichtet).

Wenn Sie sich in einer voreingenommenen Sperre befinden, kann diese bei Konkurrenz zu einer leichten Sperre erweitert werden. Wenn Sie konsistente Hashes speichern möchten, wird sie ebenfalls zu einer schweren Sperre erweitert.

JDK8 aktiviert Bias-Sperren standardmäßig. In höheren Versionen von JDK sind Bias-Sperren standardmäßig nicht aktiviert. Dies kann daran liegen, dass die Aufrechterhaltung von Bias-Sperren die Vorteile überwiegt, sodass wir keine eingehendere Untersuchung durchführen werden.

Schweres Schloss

Objektmonitor

Verwenden Sie Objektüberwachungsobjekte, um schwere Sperren zu implementieren

Der Objektmonitor verwendet einige Felder zum Aufzeichnen von Informationen, z. B.: Das Objektfeld dient zum Aufzeichnen des gesperrten Objekts, das Header-Feld zum Aufzeichnen des Markierungsworts des gesperrten Objekts und das Eigentümerfeld zum Aufzeichnen Notieren Sie den Thread, der das Schloss hält.

Der Objektmonitor verwendet Blockierungswarteschlangen zum Speichern von Threads, die nicht um Sperren konkurrieren können, und Warteschlangen zum Speichern von Threads, die Wait aufrufen, um in den Wartezustand zu gelangen.

Blockierungswarteschlange und Warteschlange sind analog zu AQS und Condition unter gleichzeitigen Paketen.

Der Objektmonitor verwendet den CXQ-Stapel und die Eintragslistenwarteschlange, um die Blockierungswarteschlange zu implementieren. Der CXQ-Stapel speichert konkurrierende Threads und die Eintragsliste speichert relativ stabile Threads, die im Wettbewerb fehlgeschlagen sind. Verwenden Sie den Wartesatz, um die Warteschlange zu implementieren.

Wenn ein Thread „wait“ aufruft, gelangt er in die Wartewarteschlange des Wartesatzes.

Beim Aufruf von notify wird nur der Kopfknoten der Warteschlange zu cxq hinzugefügt und der Thread wird nicht aktiviert, um zu konkurrieren.

Der eigentliche erwachende Thread besteht darin, den Kopfknoten in der Eintragsliste der stabilen Warteschlange zu wecken, um zu konkurrieren, wenn die Sperre aufgehoben wird. Zu diesem Zeitpunkt kann der erwachte Knoten möglicherweise nicht unbedingt die Sperre ergreifen, da sich der Thread beim Betreten ebenfalls dreht cxq. Ergreifen Sie Sperren, um unfaire Sperren zu erhalten

Wenn in der stabilen Eintragsliste keine gespeicherten Threads vorhanden sind, werden alle im cxq-Stapel gespeicherten Threads in der Eintragsliste gespeichert und dann geweckt. Zu diesem Zeitpunkt gilt: Je später der Thread in cxq eintritt, desto früher wird er geweckt (der cxq-Stack ist zuerst rein, zuletzt raus).

Tatsächlich ähnelt die Implementierung der von AQS. Schauen wir uns diesen Code an:

T1-t6 erwerben die gleiche Sperre und verwenden den t1-Thread zum Blockieren für eine Weile. Nachfolgende t2-t6-Threads werden nacheinander gestartet. Da die Sperre aufgrund der Rotation nicht erworben werden kann, werden sie der Reihe nach platziert.cxq:t2,t3,t4,t5,t6

Wenn t1 die Sperre aufhebt, wird der Thread in cxq gespeichert, da kein Thread in der Eintragsliste vorhanden ist, entry list:t6,t5,t4,t3,t2und aktiviert dann t6.

Da keine nachfolgenden Threads konkurrieren, ist die endgültige Ausführungsreihenfolget1,t6,t5,t4,t3,t2

Object obj = new Object();
new Thread(() -> {
    synchronized (obj) {
        try {
            //输入阻塞
            //阻塞的目的是让  其他线程自旋完未获取到锁,进入cxq栈
            System.in.read();
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " 获取到锁");
    }
}, "t1").start();
//sleep控制线程阻塞的顺序
Thread.sleep(50);
new Thread(() -> {
    synchronized (obj) {
        System.out.println(Thread.currentThread().getName() + " 获取到锁");
    }
}, "t2").start();
Thread.sleep(50);
new Thread(() -> {
    synchronized (obj) {
        System.out.println(Thread.currentThread().getName() + " 获取到锁");
    }
}, "t3").start();
Thread.sleep(50);
new Thread(() -> {
    synchronized (obj) {
        System.out.println(Thread.currentThread().getName() + " 获取到锁");
    }
}, "t4").start();
Thread.sleep(50);
new Thread(() -> {
    synchronized (obj) {
        System.out.println(Thread.currentThread().getName() + " 获取到锁");
    }
}, "t5").start();
Thread.sleep(50);
new Thread(() -> {
    synchronized (obj) {
        System.out.println(Thread.currentThread().getName() + " 获取到锁");
    }
}, "t6").start();

Nachdem wir ein allgemeines Verständnis des Objektmonitors erlangt haben, werfen wir einen Blick auf Expansion und Spin.

Erweiterung

Während der Erweiterung wird es vier Zustände geben, nämlich

aufgeblasen: Das Markierungswort-Sperrflag ist 10 (2), was anzeigt, dass es aufgeblasen wurde und direkt zum Objektmonitor zurückkehrt.

Inflation läuft: Wenn andere Threads bereits aufgeblasen werden, warten Sie eine Weile und durchlaufen Sie dann den Status, um in die erweiterte Logik zu gelangen.

Stapelverriegelte, leichte Schlosserweiterung

neutrale, sperrfreie Erweiterung

Die Logik von Lightweight-Sperren und sperrenfreier Erweiterung ist ähnlich. Beide müssen ein Objektüberwachungsobjekt erstellen und einige Attribute darin festlegen (z. B. Wort markieren, welches Objekt der Sperre, welcher Thread die Sperre hält ...) , und verwenden Sie schließlich CAS, um das Markierungswort zu ersetzen und auf den Objektmonitor zu verweisen

ObjectMonitor * ATTR ObjectSynchronizer::inflate (Thread * Self, oop object) {
  for (;;) {
      const markOop mark = object->mark() ;
      assert (!mark->has_bias_pattern(), "invariant") ;
      // The mark can be in one of the following states:
      // *  Inflated     - just return
      // *  Stack-locked - coerce it to inflated
      // *  INFLATING    - busy wait for conversion to complete
      // *  Neutral      - aggressively inflate the object.
      // *  BIASED       - Illegal.  We should never see this
      // CASE: inflated 
      // 已膨胀:查看 mark word 后两位是否为2  是则膨胀完 返回monitor对象
      if (mark->has_monitor()) {
          ObjectMonitor * inf = mark->monitor() ;
          assert (inf->header()->is_neutral(), "invariant");
          assert (inf->object() == object, "invariant") ;
          assert (ObjectSynchronizer::verify_objmon_isinpool(inf), "monitor is invalid");
          return inf ;
      }
      // CASE: inflation in progress - inflating over a stack-lock.
      // 膨胀中: 等待一会 再循环 从膨胀完状态退出
      if (mark == markOopDesc::INFLATING()) {
         TEVENT (Inflate: spin while INFLATING) ;
         ReadStableMark(object) ;
         continue ;
      }
      // CASE: stack-locked
      //轻量级锁膨胀
      if (mark->has_locker()) {
          //创建ObjectMonitor
          ObjectMonitor * m = omAlloc (Self) ;
          m->Recycle();
          m->_Responsible  = NULL ;
          m->OwnerIsThread = 0 ;
          m->_recursions   = 0 ;
          m->_SpinDuration = ObjectMonitor::Knob_SpinLimit ;   // Consider: maintain by type/class
          //cas将mark word替换指向ObjectMonitor
          markOop cmp = (markOop) Atomic::cmpxchg_ptr (markOopDesc::INFLATING(), object->mark_addr(), mark) ;         
          //cas 失败 则说明其他线程膨胀成功,删除当前monitor 退出
          if (cmp != mark) {
             omRelease (Self, m, true) ;
             continue ;       // Interference -- just retry
          }
          markOop dmw = mark->displaced_mark_helper() ;
          assert (dmw->is_neutral(), "invariant") ;
          //成功 设置mark word
          m->set_header(dmw) ;
          //设置持有锁的线程
          m->set_owner(mark->locker());
          //设置锁的是哪个对象
          m->set_object(object);
          guarantee (object->mark() == markOopDesc::INFLATING(), "invariant") ;
          //修改mark word对象头信息 锁状态 2
          object->release_set_mark(markOopDesc::encode(m));
          if (ObjectMonitor::_sync_Inflations != NULL) ObjectMonitor::_sync_Inflations->inc() ;
          TEVENT(Inflate: overwrite stacklock) ;
          if (TraceMonitorInflation) {
            if (object->is_instance()) {
              ResourceMark rm;
              tty->print_cr("Inflating object " INTPTR_FORMAT " , mark " INTPTR_FORMAT " , type %s",
                (void *) object, (intptr_t) object->mark(),
                object->klass()->external_name());
            }
          }
          return m ;
      }
      // CASE: neutral
      //无锁膨胀 与轻量级锁膨胀类似,也是创建monitor对象并注入属性,只是很多属性为空
      assert (mark->is_neutral(), "invariant");
      ObjectMonitor * m = omAlloc (Self) ;
      m->Recycle();
      m->set_header(mark);
      m->set_owner(NULL);
      m->set_object(object);
      m->OwnerIsThread = 1 ;
      m->_recursions   = 0 ;
      m->_Responsible  = NULL ;
      m->_SpinDuration = ObjectMonitor::Knob_SpinLimit ;       // consider: keep metastats by type/class
      //cas 更新 mark word 失败循环等待  成功返回
      if (Atomic::cmpxchg_ptr (markOopDesc::encode(m), object->mark_addr(), mark) != mark) {
          m->set_object (NULL) ;
          m->set_owner  (NULL) ;
          m->OwnerIsThread = 0 ;
          m->Recycle() ;
          omRelease (Self, m, true) ;
          m = NULL ;
          continue ;
      }
     
      if (ObjectMonitor::_sync_Inflations != NULL) ObjectMonitor::_sync_Inflations->inc() ;
      TEVENT(Inflate: overwrite neutral) ;
      if (TraceMonitorInflation) {
        if (object->is_instance()) {
          ResourceMark rm;
          tty->print_cr("Inflating object " INTPTR_FORMAT " , mark " INTPTR_FORMAT " , type %s",
            (void *) object, (intptr_t) object->mark(),
            object->klass()->external_name());
        }
      }
      return m ;
  }
}
Drehen

Nach der Erweiterung werden vor der endgültigen Aussetzung ein fester Spin und ein adaptiver Spin durchgeführt.

Der Standardwert für Drehungen beträgt 10+1 Mal

Der adaptive Spin startet 5000 Mal. Wenn die aktuelle Konkurrenz geringer ist und die Sperre erreicht wird, wird die Anzahl der Spins erhöht. Wenn die aktuelle Konkurrenz hoch ist und keine Sperre erreicht wird, wird die Anzahl der Spins verringert.

int ObjectMonitor::TrySpin_VaryDuration (Thread * Self) {
    // Dumb, brutal spin.  Good for comparative measurements against adaptive spinning.
    int ctr = Knob_FixedSpin ;
    if (ctr != 0) {
        while (--ctr >= 0) {
            if (TryLock (Self) > 0) return 1 ;
            SpinPause () ;
        }
        return 0 ;
    }
    //先进行固定11自旋次数 获取到锁返回,没获取到空转
    for (ctr = Knob_PreSpin + 1; --ctr >= 0 ; ) {
      if (TryLock(Self) > 0) {
        // Increase _SpinDuration ...
        // Note that we don't clamp SpinDuration precisely at SpinLimit.
        // Raising _SpurDuration to the poverty line is key.
        int x = _SpinDuration ;
        if (x < Knob_SpinLimit) {
           if (x < Knob_Poverty) x = Knob_Poverty ;
           _SpinDuration = x + Knob_BonusB ;
        }
        return 1 ;
      }
      SpinPause () ;
    }
    
    //自适应自旋 一开始5000 如果成功认为此时竞争不大 自旋获取锁成功率高 增加重试次数 如果失败则减少
    //...
}   

Zusammenfassen

Dieser Artikel konzentriert sich auf die Synchronisierung und beschreibt in einfachen Worten die Implementierung von CAS und die Synchronisierung auf Java- und C++-Ebene, Sperraktualisierungsprinzipien, Fälle, Quellcode usw.

synchronisiert wird in Szenarien verwendet, die eine Synchronisierung unter Parallelität erfordern. Es kann Atomizität, Sichtbarkeit und Ordnung erfüllen. Es kann für gewöhnliche Objekte und statische Objekte verwendet werden. Wenn es für statische Objekte verwendet wird, dient es dazu, die Sperre des entsprechenden Klassenobjekts zu erhalten .

Wenn synchronisiert auf einen Codeblock einwirkt, werden die Bytecode-Anweisungen „monitorentry“ und „monitorexit“ verwendet, um das Sperren und Entsperren zu identifizieren; wenn synchronisiert auf eine Methode einwirkt, wird das synchronisierte Schlüsselwort zur Zugriffskennung hinzugefügt, und die virtuelle Maschine verwendet implizit „monitorentry“ und „monitorexit“.

CAS vergleicht und ersetzt, oft mit dem Wiederholungsmechanismus implementiert, um optimistische Sperren/Spin-Sperren zu implementieren. Der Vorteil besteht darin, dass Thread-Hänge in Szenarien mit geringer Konkurrenz mit weniger Overhead ersetzt werden können, es verursacht jedoch ABA-Probleme und kann die Anzahl der Wiederholungsversuche nicht vorhersagen setzt die CPU im Leerlauf. Kosten und andere Probleme

Es werden leichte Sperren vorgeschlagen, um Mutexe mit geringerem Overhead in Szenarien mit alternativer Ausführung/weniger Konkurrenz zu ersetzen; implementiert mit CAS und Sperraufzeichnung

Wenn beim Sperren einer leichten Sperre keine Sperre vorhanden ist, kopieren Sie das Markierungswort in den Sperrdatensatz, und dann ersetzt CAS das Objektmarkierungswort, um auf den Sperrdatensatz zu verweisen. Wenn dies fehlschlägt, wird es erweitert; wenn die Sperre bereits gehalten wird , wird der Thread bestimmt, der die Sperre hält. Wenn es sich um den aktuellen Thread handelt, wird die Häufigkeit akkumuliert. Wenn es sich nicht um den aktuellen Thread handelt, wird er erweitert.

Überprüfen Sie beim Entsperren der Lightweight-Sperre, ob die Kopie des Sperrdatensatzes null ist. Wenn dies der Fall ist, bedeutet dies, dass die Sperre wiedereintrittsfähig ist und die Häufigkeit um eins reduziert wird. Andernfalls ersetzt CAS das kopierte Markierungswort Wenn das Ersetzen fehlschlägt, bedeutet dies, dass andere Threads um die Markierung konkurrieren. Word hat auf den Objektmonitor hingewiesen, um auf die Freigabe der Schwergewichtssperre hinzuweisen

Biased Locking wird vorgeschlagen, um den Overhead von CAS durch einen geringeren Overhead in Szenarien zu ersetzen, in denen ein Thread häufig ausgeführt wird. Höhere Versionen sind jedoch standardmäßig nicht mehr aktiviert.

Schwere Sperren werden vom Objektmonitor implementiert. Im Objektmonitor werden cxq und Eintragsliste zur Bildung der Blockierungswarteschlange verwendet, und der Wartesatz wird zur Bildung der Warteschlange verwendet.

Wenn die Wartemethode ausgeführt wird, wird der Thread erstellt, wenn der Knoten dem Wartesatz beitritt. Wenn die Benachrichtigungsmethode ausgeführt wird, wird der Wartesatzwarteschlangenkopfknoten zu cxq hinzugefügt und der Eintragslistenwarteschlangenkopfknoten wird aktiviert, um um den zu konkurrieren Sperre nur, wenn die Sperre aufgehoben wird, auch wenn die Sperre nicht ergriffen wird. Wenn ein Knoten zu cxq hinzugefügt wird, dreht er sich immer noch, sodass es nicht unbedingt der Hauptknoten der Eintragsliste ist, der die Sperre ergreifen kann Erzielen Sie unfaire Sperren. Wenn die Eintragsliste leer ist, fügen Sie den Knoten im cxq-Stack zur Eintragslistenwarteschlange hinzu (Knoten, die später in cxq eintreten, werden zuerst aktiviert).

Beim Erweitern auf eine Schwergewichtssperre gibt es vier Situationen: Wenn der Status erweitert ist, wird das Objektüberwachungsobjekt direkt zurückgegeben. Wenn der Status erweitert ist, bedeutet dies, dass andere Threads erweitert werden und warten. Die nächste Schleife wird in die erweiterte Schleife eingegeben Logik; wenn der Status „Leichte Sperrenerweiterung“ oder „Sperrenfreie Erweiterung“ vorliegt, wird das Objektmonitorobjekt erstellt, einige wichtige Attribute werden festgelegt und CAS ersetzt das Markierungswort, um auf den Objektmonitor zu verweisen.

Das Schwergewichtsschloss führt einen festen Spin und einen adaptiven Spin aus, bevor es endgültig hängt (erhöhen Sie die Anzahl der Spins, wenn die Konkurrenz in letzter Zeit klein ist; reduzieren Sie die Anzahl der Spins, wenn die Konkurrenz groß ist).

Zum Schluss (tun Sie es nicht kostenlos, drücken Sie einfach dreimal hintereinander, um um Hilfe zu bitten ~)

Dieser Artikel ist in der Spalte „Vom Punkt zur Linie und von der Linie zur Oberfläche“ enthalten, um in einfachen Worten ein Java-Wissenssystem für gleichzeitige Programmierung aufzubauen . Interessierte Studenten können weiterhin aufmerksam sein.

Die Notizen und Fälle dieses Artikels wurden in gitee-StudyJava und github-StudyJava aufgenommen . Interessierte Studierende können weiterhin unter stat~ darauf achten

Falladresse:

Gitee-JavaConcurrentProgramming/src/main/java/B_synchronized

Github-JavaConcurrentProgramming/src/main/java/B_synchronized

Wenn Sie Fragen haben, können Sie diese im Kommentarbereich diskutieren. Wenn Sie Cai Cais Schreiben für gut halten, können Sie es liken, verfolgen und sammeln, um es zu unterstützen~

Folgen Sie Cai Cai und teilen Sie weitere nützliche Informationen, öffentliches Konto: Cai Cais private Back-End-Küche

Lei Jun: Die offizielle Version von Xiaomis neuem Betriebssystem ThePaper OS wurde verpackt. Ein Popup-Fenster auf der Lotterieseite der Gome App beleidigt seinen Gründer. Die US-Regierung beschränkt den Export von NVIDIA H800 GPU nach China. Die Xiaomi ThePaper OS-Schnittstelle ist offengelegt. Ein Master hat Scratch verwendet, um den RISC-V-Simulator zu reiben, und er wurde erfolgreich ausgeführt. Linux-Kernel RustDesk Remote Desktop 1.2.3 veröffentlicht, verbesserte Wayland-Unterstützung Nach dem Herausziehen des Logitech-USB-Empfängers stürzte der Linux-Kernel ab. DHH scharfe Überprüfung der „Verpackungstools“. ": Das Frontend muss überhaupt nicht erstellt werden (No Build) JetBrains startet Writerside, um technische Dokumentation zu erstellen. Tools für Node.js 21 offiziell veröffentlicht
{{o.name}}
{{m.name}}

Supongo que te gusta

Origin my.oschina.net/u/6903207/blog/10112326
Recomendado
Clasificación