Heim  >  Artikel  >  Java  >  [Bekämpfung der Java-Parallelität] ----- Eingehende Analyse des Implementierungsprinzips der Synchronisierung

[Bekämpfung der Java-Parallelität] ----- Eingehende Analyse des Implementierungsprinzips der Synchronisierung

黄舟
黄舟Original
2017-02-24 09:58:501074Durchsuche

Ich erinnere mich, als ich anfing, Java zu lernen, wann immer ich auf Multithreading stieß. Im Vergleich zu uns damals war synchronisiert so magisch und leistungsstark. Damals gaben wir ihm den Namen „synchronisiert“. Dies wurde auch zu unserer bewährten Lösung für Multithreading-Situationen. Im Verlauf unserer Studie wissen wir jedoch, dass die synchronisierte Sperre im Vergleich zu Lock so umständlich erscheint, dass wir sie für nicht so effizient halten und sie langsam aufgeben.

Es stimmt, dass mit den verschiedenen Optimierungen der Synchronisierung in Javs SE 1.6 die Synchronisierung nicht so schwer erscheint. Folgen wir LZ, um den Implementierungsmechanismus von synchronisiert zu erkunden, wie Java ihn optimiert, den Sperroptimierungsmechanismus, die Sperrspeicherstruktur und den Upgrade-Prozess

Implementierungsprinzip

synchronisiert Es kann Stellen Sie sicher, dass beim Ausführen einer Methode oder eines Codeblocks jeweils nur eine Methode den kritischen Abschnitt betreten kann. Dadurch kann auch die Speichersichtbarkeit gemeinsam genutzter Variablen sichergestellt werden

Jedes Objekt in Java kann verwendet werden Als Sperre ist dies die Grundlage für die synchronisierte Implementierung der Synchronisierung:
1. Gewöhnliche Synchronisierungsmethode, die Sperre ist das aktuelle Instanzobjekt
2. Statische Synchronisierungsmethode, die Sperre ist das Klassenobjekt der aktuellen Klasse
3. Synchronisationsmethodenblock, die Sperre ist Objekte in Klammern

Wenn ein Thread auf einen synchronisierten Codeblock zugreift, muss er zuerst die Sperre erhalten, um den synchronisierten Code auszuführen, wenn er beendet wird oder eine Ausnahme auslöst. Es muss die Sperre aufheben. Wie wird dieser Mechanismus implementiert? Schauen wir uns zunächst einen einfachen Code an:

public class SynchronizedTest {
    public synchronized void test1(){

    }    public void test2(){        synchronized (this){

        }
    }
}

Verwenden Sie das Javap-Tool, um die generierten Klassendateiinformationen anzuzeigen und die Implementierung von Synchronize zu analysieren
[Bekämpfung der Java-Parallelität] ----- Eingehende Analyse des Implementierungsprinzips der Synchronisierung
Wie aus ersichtlich ist Oben wird der Synchronisationscodeblock mithilfe der Anweisungen „monitorenter“ und „monitorexit“ implementiert. Die Synchronisationsmethode (es ist hier nicht offensichtlich, dass Sie sich die zugrunde liegende Implementierung der JVM ansehen müssen) basiert auf der Implementierung von ACC_SYNCHRONIZED im Methodenmodifikator.
Synchronisierter Codeblock: Die Monitorenter-Anweisung wird am Anfang des synchronisierten Codeblocks eingefügt, und die Monitorexit-Anweisung wird am Ende des synchronisierten Codeblocks eingefügt. Die JVM muss sicherstellen, dass jeder Monitorenter hat einen entsprechenden Monitorexit. Jedem Objekt ist ein Monitor zugeordnet. Wenn ein Monitor gehalten wird, befindet er sich in einem gesperrten Zustand. Wenn der Thread die Monitorenter-Anweisung ausführt, versucht er, den dem Objekt entsprechenden Monitorbesitz zu erhalten, dh die Sperre des Objekts zu erhalten.
Synchronisierte Methode: Die synchronisierte Methode wird übersetzt Ein normaler Methodenaufruf und Rückgabeanweisungen wie: invokevirtual und areturn Anweisungen haben keine speziellen Anweisungen auf der VM-Bytecode-Ebene, um die durch synchronisierte Methode zu implementieren. Stattdessen wird die synchronisierte Flagposition im Feld access_flags der Methode auf 1 gesetzt in der Methodentabelle der Klassendatei. Zeigt an, dass es sich bei der Methode um eine synchronisierte Methode handelt, und verwendet das Objekt, das die Methode aufruft, oder die Klasse, zu der die Methode gehört, um Klass als Sperrobjekt im internen Objekt der JVM darzustellen. (Auszug aus: http://www.php.cn/)

Lassen Sie uns mit der Analyse fortfahren, aber bevor wir tiefer gehen, müssen wir zwei wichtige Konzepte verstehen: Java-Objektheader und Monitor.

Java-Objekt-Header, Monitor

Java-Objekt-Header und Monitor sind die Grundlage für die Synchronisierung! Diese beiden Konzepte werden im Folgenden ausführlich vorgestellt.

Java-Objekt-Header

Die von der Synchronisierung verwendete Sperre wird im Java-Objekt-Header gespeichert. Der Objektheader der virtuellen Hotspot-Maschine enthält hauptsächlich zwei Datenteile: Mark Word (Markierungsfeld) und Klass Pointer (Typzeiger). Unter anderem ist Klass Point der Zeiger des Objekts auf seine Klassenmetadaten. Die virtuelle Maschine verwendet diesen Zeiger, um zu bestimmen, zu welcher Klasse das Objekt gehört, um die Laufzeitdaten des Objekts selbst zu speichern Schlösser und der Schlüssel zur Vorspannungsverriegelung, daher wird sich das Folgende darauf konzentrieren

Mark Word.
Mark Word wird zum Speichern der Laufzeitdaten des Objekts selbst verwendet, z. B. Hash-Code (HashCode), Alter der GC-Generierung, Sperrstatus-Flag, vom Thread gehaltene Sperre, voreingenommene Thread-ID, voreingenommener Zeitstempel usw. Java-Objektheader belegen im Allgemeinen zwei Maschinencodes (in einer virtuellen 32-Bit-Maschine entspricht 1 Maschinencode 4 Bytes, also 32 Bit). Wenn es sich bei dem Objekt jedoch um einen Array-Typ handelt, sind drei Maschinencodes erforderlich, da die JVM virtuell ist Maschine kann Die Größe des Java-Objekts wird durch die Metadateninformationen des Java-Objekts bestimmt. Die Größe des Arrays kann jedoch nicht anhand der Metadaten des Arrays bestätigt werden. Daher wird ein Block zum Aufzeichnen der Array-Länge verwendet. Die folgende Abbildung zeigt die Speicherstruktur des Java-Objektheaders (virtuelle 32-Bit-Maschine):
[Bekämpfung der Java-Parallelität] ----- Eingehende Analyse des Implementierungsprinzips der Synchronisierung
Die Objektheaderinformationen stellen zusätzliche Speicherkosten dar, die nichts mit den durch das Objekt definierten Daten zu tun haben selbst, aber angesichts der Platzeffizienz der virtuellen Maschine ist Mark Word als nicht feste Datenstruktur konzipiert, um so viele Daten wie möglich auf sehr kleinem Raum zu speichern. Es wird seinen eigenen Speicherplatz entsprechend dem Status des Objekts wiederverwenden Mit anderen Worten, Mark Word ändert sich, während das Programm ausgeführt wird. Der Änderungsstatus ist wie folgt (32-Bit-virtuelle Maschine):
[Bekämpfung der Java-Parallelität] ----- Eingehende Analyse des Implementierungsprinzips der Synchronisierung

Eine kurze Einführung in den Java-Objektheader Schauen wir uns als nächstes den Monitor an.

Monitor

Was ist Monitor? Wir können es als Synchronisationswerkzeug verstehen oder es als Synchronisationsmechanismus beschreiben. Normalerweise wird es als Objekt beschrieben.
So wie alles ein Objekt ist, sind alle Java-Objekte geborene Monitore. Jedes Java-Objekt hat das Potenzial, ein Monitor zu werden, da im Design von Java jedes Java-Objekt mit einer Handvoll Monitoren geboren wird Eine fehlende Sperre wird als interne Sperre oder Monitorsperre bezeichnet.
Monitor ist eine Thread-private Datenstruktur. Jeder Thread verfügt über eine Liste verfügbarer Monitordatensätze und es gibt auch eine global verfügbare Liste. Jedes gesperrte Objekt ist einem Monitor zugeordnet (das LockWord im MarkWord des Objektheaders zeigt auf die Startadresse des Monitors. Gleichzeitig gibt es im Monitor ein Besitzerfeld, das die eindeutige Kennung des Threads speichert). Besitzt die Sperre, was darauf hinweist, dass die Sperre im Besitz dieses Threads ist. Seine Struktur ist wie folgt:
[Bekämpfung der Java-Parallelität] ----- Eingehende Analyse des Implementierungsprinzips der Synchronisierung
Eigentümer: Anfänglich bedeutet NULL, dass derzeit kein Thread Eigentümer des Monitordatensatzes ist. Wenn der Thread die Sperre erfolgreich besitzt, wird die eindeutige Identität des Threads gespeichert. Wenn die Sperre aufgehoben wird, wird sie auf NULL gesetzt.
EntryQ: Ordnet eine System-Mutex-Sperre (Semaphor) zu und blockiert alle Threads, die den Monitordatensatz nicht sperren können.
RcThis: Gibt die Anzahl aller Threads an, die blockiert sind oder auf den Monitordatensatz warten.
Nest: Wird verwendet, um die Wiedereintrittssperrenzählung zu implementieren.
HashCode: Speichert den aus dem Objektheader kopierten HashCode-Wert (kann auch das GC-Alter enthalten).
Kandidat: Wird verwendet, um unnötiges Blockieren oder Warten auf das Aufwecken von Threads zu vermeiden, da jeweils nur ein Thread die Sperre erfolgreich besitzen kann, wenn der vorherige Thread, der die Sperre aufhebt, alle Threads aufweckt Blockierende oder wartende Threads führen zu unnötigen Kontextwechseln (von blockiert zu bereit und dann erneut blockiert aufgrund des Ausfalls konkurrierender Sperren), was zu erheblichen Leistungseinbußen führt. Für Candidate gibt es nur zwei mögliche Werte: 0 bedeutet, dass kein Thread aktiviert werden muss; 1 bedeutet, dass ein Nachfolger-Thread aktiviert werden muss, um um die Sperre zu konkurrieren.
Auszug aus: Implementierungsprinzipien und Anwendungen von Synchronized in Java)
Wir wissen, dass Synchronized eine schwere Sperre ist und nicht sehr effizient. Gleichzeitig war dieses Konzept immer in unseren Köpfen, aber die Implementierung von In JDK 1.6 wurden verschiedene Optimierungen vorgenommen, um es nicht so schwer zu machen. Welche Optimierungsmethoden hat die JVM übernommen?

Sperrenoptimierung

jdk1.6 hat eine große Anzahl von Optimierungen für die Implementierung von Sperren eingeführt, wie z. B. Spin-Sperren, adaptive Spin-Sperren, Sperreneliminierung, Sperrvergröberung, voreingenommene Sperren und Lightweight Techniken wie Level-Locking werden verwendet, um den Overhead von Lock-Vorgängen zu reduzieren.
Sperren gibt es hauptsächlich in vier Zuständen: Kein Sperrzustand, voreingenommener Sperrzustand, leichter Sperrzustand und schwerer Sperrzustand. Sie werden mit der harten Konkurrenz schrittweise aktualisiert. Beachten Sie, dass Sperren hochgestuft, aber nicht herabgestuft werden können. Diese Strategie dient dazu, die Effizienz beim Erwerb und Freigeben von Sperren zu verbessern.

自旋锁

线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作,势必会给系统的并发性能带来很大的压力。同时我们发现在许多应用上面,对象锁的锁状态只会持续很短一段时间,为了这一段很短的时间频繁地阻塞和唤醒线程是非常不值得的。所以引入自旋锁。
何谓自旋锁?
所谓自旋锁,就是让该线程等待一段时间,不会被立即挂起,看持有锁的线程是否会很快释放锁。怎么等待呢?执行一段无意义的循环即可(自旋)。
自旋等待不能替代阻塞,先不说对处理器数量的要求(多核,貌似现在没有单核的处理器了),虽然它可以避免线程切换带来的开销,但是它占用了处理器的时间。如果持有锁的线程很快就释放了锁,那么自旋的效率就非常好,反之,自旋的线程就会白白消耗掉处理的资源,它不会做任何有意义的工作,典型的占着茅坑不拉屎,这样反而会带来性能上的浪费。所以说,自旋等待的时间(自旋的次数)必须要有一个限度,如果自旋超过了定义的时间仍然没有获取到锁,则应该被挂起。
自旋锁在JDK 1.4.2中引入,默认关闭,但是可以使用-XX:+UseSpinning开开启,在JDK1.6中默认开启。同时自旋的默认次数为10次,可以通过参数-XX:PreBlockSpin来调整;
如果通过参数-XX:preBlockSpin来调整自旋锁的自旋次数,会带来诸多不便。假如我将参数调整为10,但是系统很多线程都是等你刚刚退出的时候就释放了锁(假如你多自旋一两次就可以获取锁),你是不是很尴尬。于是JDK1.6引入自适应的自旋锁,让虚拟机会变得越来越聪明。

适应自旋锁

JDK 1.6引入了更加聪明的自旋锁,即自适应自旋锁。所谓自适应就意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。它怎么做呢?线程如果自旋成功了,那么下次自旋的次数会更加多,因为虚拟机认为既然上次成功了,那么此次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多。反之,如果对于某个锁,很少有自旋能够成功的,那么在以后要或者这个锁的时候自旋的次数会减少甚至省略掉自旋过程,以免浪费处理器资源。
有了自适应自旋锁,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测会越来越准确,虚拟机会变得越来越聪明。

锁消除

为了保证数据的完整性,我们在进行操作时需要对这部分操作进行同步控制,但是在有些情况下,JVM检测到不可能存在共享数据竞争,这是JVM会对这些同步锁进行锁消除。锁消除的依据是逃逸分析的数据支持。
如果不存在竞争,为什么还需要加锁呢?所以锁消除可以节省毫无意义的请求锁的时间。变量是否逃逸,对于虚拟机来说需要使用数据流分析来确定,但是对于我们程序员来说这还不清楚么?我们会在明明知道不存在数据竞争的代码块前加上同步吗?但是有时候程序并不是我们所想的那样?我们虽然没有显示使用锁,但是我们在使用一些JDK的内置API时,如StringBuffer、Vector、HashTable等,这个时候会存在隐形的加锁操作。比如StringBuffer的append()方法,Vector的add()方法:

    public void vectorTest(){
        Vector<String> vector = new Vector<String>();        for(int i = 0 ; i < 10 ; i++){
            vector.add(i + "");
        }

        System.out.println(vector);
    }

在运行这段代码时,JVM可以明显检测到变量vector没有逃逸出方法vectorTest()之外,所以JVM可以大胆地将vector内部的加锁操作消除。

锁粗化

我们知道在使用同步锁的时候,需要让同步块的作用范围尽可能小—仅在共享数据的实际作用域中才进行同步,这样做的目的是为了使需要同步的操作数量尽可能缩小,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁。
在大多数的情况下,上述观点是正确的,LZ也一直坚持着这个观点。但是如果一系列的连续加锁解锁操作,可能会导致不必要的性能损耗,所以引入锁粗话的概念。
锁粗话概念比较好理解,就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。如上面实例:vector每次add的时候都需要加锁操作,JVM检测到对同一个对象(vector)连续加锁、解锁操作,会合并一个更大范围的加锁、解锁操作,即加锁解锁操作会移到for循环之外。

Leichte Sperren

Der Hauptzweck der Einführung leichter Sperren besteht darin, den Leistungsverbrauch zu reduzieren, der durch herkömmliche schwere Sperren mithilfe von Betriebssystem-Mutexes ohne Multithread-Konkurrenz verursacht wird. Wenn die Bias-Lock-Funktion deaktiviert ist oder mehrere Threads um die Bias-Sperre konkurrieren und die Bias-Sperre auf eine Lightweight-Sperre aktualisiert wird, wird versucht, die Lightweight-Sperre zu erwerben. Die Schritte sind wie folgt:
Erfassen Sie die Sperre
1 . Bestimmen Sie, ob sich das aktuelle Objekt in einem sperrenfreien Zustand befindet (Hashcode, 0, 01). Wenn ja, erstellt die JVM zunächst einen Bereich mit dem Namen Lock Record im Stapelrahmen des aktueller Thread zum Speichern des aktuellen Status des Mark Word (der Beamte fügt dieser Kopie ein Displaced Mark Word hinzu); andernfalls führen Sie Schritt (3) aus. Die JVM verwendet die CAS-Operation, um zu versuchen, das Markierungswort des Objekts so zu aktualisieren, dass es auf die Sperrdatensatzkorrektur zeigt. Wenn dies gelingt, bedeutet dies, dass um die Sperre konkurriert wird, dann wird das Sperrflag auf 00 geändert (was anzeigt, dass dieses Objekt vorhanden ist). Wenn der Synchronisierungsvorgang fehlschlägt, wird Schritt (3) ausgeführt. Bestimmen Sie das aktuelle Objekt. Zeigt das Markierungswort auf den Stapelrahmen des aktuellen Threads? Wenn dies der Fall ist, bedeutet dies, dass der aktuelle Thread bereits die Sperre des aktuellen Objekts enthält und der Synchronisationscodeblock direkt ausgeführt wird. Andernfalls bedeutet dies nur, dass das Sperrobjekt von anderen Threads vorbelegt wurde und zu diesem Zeitpunkt leichtgewichtig ist . Die Level-Sperre muss zu einer Schwergewichtssperre erweitert werden, das Sperrflag wird 10 und der Thread, der später wartet, wechselt in den Blockierungszustand

Die Sperre freigeben

Dasselbe gilt für die Freigabe von Lightweight-Sperren. Die Hauptschritte sind wie folgt: 1. Holen Sie sich die im Displaced Mark Word gespeicherten Daten CAS-Operation zum Ersetzen des Markierungsworts des aktuellen Objekts durch die abgerufenen Daten. Wenn dies erfolgreich ist, bedeutet dies, dass die Sperre erfolgreich aufgehoben wurde. Andernfalls wird ausgeführt (3). Wenn die CAS-Operation fehlschlägt, bedeutet dies, dass dies der Fall ist Andere Threads versuchen, die Sperre zu erlangen, und der angehaltene Thread muss beim Aufheben der Sperre aktiviert werden.

Bei leichten Schlössern besteht die Grundlage für die Leistungsverbesserung darin, dass „bei den meisten Schlössern während des gesamten Lebenszyklus keine Konkurrenz besteht, zusätzlich zu den Mehrkosten des gegenseitigen Ausschlusses.“ Es gibt zusätzliche CAS-Operationen, daher sind leichte Sperren im Falle eines Multithread-Wettbewerbs langsamer als schwere Sperren

Die folgende Abbildung zeigt den Prozess zum Erfassen und Freigeben von leichten Sperren


Voreingenommene Sperre
[Bekämpfung der Java-Parallelität] ----- Eingehende Analyse des Implementierungsprinzips der SynchronisierungDer Hauptzweck der Einführung einer voreingenommenen Sperre besteht darin, unnötige, leichte Sperrausführungspfade ohne Multithread-Konkurrenz zu minimieren. Wie oben erwähnt, erfordern die Sperr- und Entsperrvorgänge von Lightweight-Schlössern mehrere atomare CAS-Anweisungen. Wie reduziert voreingenommenes Sperren unnötige CAS-Operationen? Wir können es verstehen, wenn wir uns die Struktur der Markusarbeit ansehen. Sie müssen nur überprüfen, ob es sich um eine voreingenommene Sperre handelt, die Sperr-ID und die Thread-ID. Der Verarbeitungsablauf ist wie folgt:

Sperre abrufen

1 Überprüfen Sie, ob das Mark Word vorhanden ist ein voreingenommener Zustand, das heißt, ob es sich um eine voreingenommene Sperre handelt 1. Wenn es sich im voreingenommenen Zustand befindet, führen Sie einen Test durch Schritt (5), andernfalls führen Sie Schritt (3) aus. Wenn die Thread-ID nicht die aktuelle Thread-ID ist, konkurrieren Sie um die Sperre durch die CAS-Operation. Wenn der Wettbewerb erfolgreich ist, ersetzen Sie die Thread-ID von Mark Word die aktuelle Thread-ID, andernfalls Thread ausführen (4);
4. Konkurriert um die Sperre über CAS, was beweist, dass derzeit eine Multi-Thread-Konkurrenzsituation vorliegt Die voreingenommene Sperre wird ausgesetzt, die voreingenommene Sperre wird zu einer leichten Sperre aktualisiert und dann führt der am sicheren Punkt blockierte Thread den Synchronisierungscodeblock weiter aus
Sperre freigeben

Bei der Freigabe der Voreingenommenheitssperre wird ein Mechanismus verwendet, der nur durch die Konkurrenz freigegeben wird. Der Thread ergreift nicht die Initiative, um die Voreingenommenheit aufzuheben. Die Sperre muss warten, bis andere Threads konkurrieren. Der Widerruf der voreingenommenen Sperre muss auf den globalen Sicherheitspunkt warten (zu diesem Zeitpunkt ist kein ausgeführter Code vorhanden). Die Schritte sind wie folgt:
1. Halten Sie den Thread an, der die voreingenommene Sperre besitzt, und stellen Sie fest, ob das Sperrobjekt noch gesperrt ist.
2. Brechen Sie die voreingenommene Sperre ab und kehren Sie in den sperrfreien Zustand zurück leichte Sperre. Status;

Die folgende Abbildung zeigt den Erfassungs- und Freigabeprozess von voreingenommenen Sperren


Schwergewichtssperre wird überwacht Innerhalb der Objektmonitor-Implementierung basiert der Kern des Monitors auf der Mutex Lock-Implementierung des zugrunde liegenden Betriebssystems. Das Umschalten zwischen Threads im Betriebssystem erfordert einen Wechsel vom Benutzermodus in den Kernelmodus, und die Wechselkosten sind sehr hoch.


Das Obige ist [Tödliche Java-Parallelität] ----- eine eingehende Analyse der Implementierungsprinzipien der Synchronisierung. Weitere verwandte Inhalte finden Sie auf der chinesischen PHP-Website (www.php.cn). )!


[Bekämpfung der Java-Parallelität] ----- Eingehende Analyse des Implementierungsprinzips der Synchronisierung

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn
Vorheriger Artikel:Java-GenerikaNächster Artikel:Java-Generika