In diesem Artikel werden zunächst die Lock-Schnittstelle, die Klassenhierarchie von ReentrantLock und das einfache Prinzip der Sperrfunktionsvorlagenklasse AbstractQueuedSynchronizer vorgestellt und anschließend die internen Prinzipien von ReentrantLock durch Analyse der Sperre erläutert Methode und Entsperrmethode von ReentrantLock und schließlich eine Zusammenfassung erstellen. In diesem Artikel werden Bedingungsvariablen in ReentrantLock nicht behandelt.
Die Sperrschnittstelle ist eine Abstraktion von Tools zur Steuerung der Parallelität. Es ist flexibler als die Verwendung des synchronisierten Schlüsselworts und kann Bedingungsvariablen unterstützen. Es handelt sich um ein Tool zur Steuerung der Parallelität. Im Allgemeinen steuert es die Exklusivität einer bestimmten gemeinsam genutzten Ressource. Mit anderen Worten, nur ein Thread kann diese Sperre erwerben und gleichzeitig Ressourcen belegen. Wenn andere Threads die Sperre erhalten möchten, müssen sie warten, bis dieser Thread die Sperre aufhebt. ReentrantLock in der Java-Implementierung ist eine solche Sperre. Eine andere Art von Sperre, die es mehreren Threads ermöglichen kann, Ressourcen zu lesen, aber nur einem Thread das Schreiben von Ressourcen ermöglichen kann, ist ReadWriteLock, eine solche spezielle Sperre, die als Lese-/Schreibsperre bezeichnet wird. Im Folgenden finden Sie eine Gesamtbeschreibung mehrerer Methoden der Lock-Schnittstelle:
Methodenname | Beschreibung|||||||||||||||
Sperre | Sperre erwerben Wenn die Sperre nicht erworben werden kann, wird der aktuelle Thread außerplanmäßig, bis die Sperre erworben wird | ||||||||||||||
lockInterruptably | Erhalten Sie die Sperre, sofern der aktuelle Thread nicht unterbrochen wird. Wenn die Sperre erworben wird, kehren Sie sofort zurück. Wenn sie nicht erworben werden kann, wird der aktuelle Thread außerplanmäßig und schläft, bis die folgenden zwei Dinge passieren:
|
||||||||||||||
tryLock | If Die Sperre kann beim Aufruf erworben werden, dann wird die Sperre erfasst und true zurückgegeben. Wenn die aktuelle Sperre nicht erworben werden kann, gibt diese Methode sofort false | ||||||||||||||
tryLcok(long time,TimeUnit unit) | Versuchen Sie, die Sperre innerhalb der angegebenen Zeit zu erhalten. Wenn die Sperre erworben werden kann, erwerben Sie die Sperre und true zurückgeben, wenn die aktuelle Sperre nicht erworben werden kann, wird der aktuelle Thread außerplanmäßig, bis eines der folgenden drei Dinge eintritt: 1 Der aktuelle Thread erhält die Sperre 2 durch eine weitere Thread-Unterbrechung blockiert3. Die angegebene Wartezeit ist abgelaufen | ||||||||||||||
Entsperren | Gibt die vom aktuellen Thread belegte Sperre frei | ||||||||||||||
newCondition | Gibt eine Bedingungsvariable zurück, die der aktuellen Sperre zugeordnet ist. Bevor diese Bedingungsvariable verwendet werden kann, muss der aktuelle Thread die Sperre belegen. Wenn Sie die Methode „await“ von Condition aufrufen, wird die Sperre vor dem Warten atomar freigegeben und die Sperre wird atomar erworben, nachdem auf das Erwachen gewartet wurde |
Als nächstes stellen wir vor, wie das gesamte ReentrantLock rund um die beiden Methoden Sperren und Entsperren funktioniert. Bevor wir ReentrantLock einführen, werfen wir zunächst einen Blick auf die Klassenhierarchie von ReentrantLock und seinem eng verwandten AbstractQueuedSynchronizer
ReentrantLock Es implementiert die Lock-Schnittstelle und verfügt über drei interne Klassen: Sync, NonfairSync und FairSync. Sync ist ein abstrakter Typ, der AbstractQueuedSynchronizer erbt. Diese AbstractQueuedSynchronizer ist eine Vorlagenklasse, die viele sperrbezogene Funktionen implementiert und Hook-Methoden für die Benutzerimplementierung bereitstellt, z. B. tryAcquire und tryRelease , usw. Sync implementiert die tryRelease-Methode von AbstractQueuedSynchronizer. Die Klassen NonfairSync und FairSync erben von Sync, implementieren die Lock-Methode und verfügen dann über unterschiedliche Implementierungen von tryAcquire für faire bzw. unfaire Preemption.
Zunächst erbt AbstractOwnableSynchronizer. Die Implementierung von AbstractOwnableSynchronizer ist sehr einfach und die Variable exclusiveOwnerThread wird intern zur Darstellung eines exklusiven Threads verwendet .
Zweitens verwendet AbstractQueuedSynchronizer intern die CLH-Sperrwarteschlange, um die gleichzeitige Ausführung in eine serielle Ausführung umzuwandeln. Die gesamte Warteschlange ist eine doppelt verknüpfte Liste. Jeder Knoten in der CLH-Sperrwarteschlange speichert Verweise auf den vorherigen Knoten und den nächsten Knoten, den Thread, der dem aktuellen Knoten entspricht, und einen Status. Dieser Status wird verwendet, um anzugeben, ob der Thread blockieren soll. Wenn der vorherige Knoten des Knotens freigegeben wird, wird der aktuelle Knoten geweckt und zum Kopf. Neu hinzugefügte Knoten werden am Ende der Warteschlange platziert.
1. Wenn wir bei der Initialisierung von ReentrantLock nicht übergeben, ob die Parameter fair sind, wird standardmäßig die unfaire Sperre verwendet, nämlich NonfairSync.
2. Wenn wir die Sperrmethode von ReentrantLock aufrufen, rufen wir tatsächlich die Sperrmethode von NonfairSync auf. Diese Methode verwendet zuerst die CAS-Operation, um zu versuchen, die Sperre zu ergreifen. Bei Erfolg wird der aktuelle Thread auf diese Sperre gesetzt, was anzeigt, dass die Vorabbelegung erfolgreich war. Wenn dies fehlschlägt, rufen Sie die Methode zum Erwerb der Vorlage auf und warten Sie auf die vorzeitige Beendigung. Der Code lautet wie folgt:
static final class NonfairSync extends Sync { final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); } protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); } }
3. Der Aufruf von acquire(1) verwendet tatsächlich die Acquire-Methode von AbstractQueuedSynchronizer, bei der es sich um eine Reihe von Sperren-Preemption-Vorlagen handelt. Das Gesamtprinzip besteht darin, zuerst zu versuchen, die Sperre zu erhalten Wenn es nicht erfasst wird, wird bei Erfolg ein Knoten des aktuellen Threads zur CLH-Warteschlange hinzugefügt, was darauf hinweist, dass er auf die vorzeitige Freigabe wartet. Rufen Sie dann den Preemption-Modus der CLH-Warteschlange auf. Beim Betreten wird auch eine Operation zum Erlangen der Sperre ausgeführt. Wenn diese immer noch nicht erhalten werden kann, wird LockSupport.park aufgerufen, um den aktuellen Thread anzuhalten. Wann wird der aktuelle Thread aktiviert? Wenn der Thread, der die Sperre hält, „Unlock“ aufruft, weckt er den Thread auf dem Knoten neben dem Kopfknoten der CLH-Warteschlange und ruft die Methode „LockSupport.unpark“ auf. Der Erfassungscode ist wie folgt relativ einfach:
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
3.1 Die tryAcquire-Hook-Methode wird zuerst innerhalb der Erfassungsmethode verwendet, um erneut zu versuchen, die Sperre zu erhalten. Diese Methode verwendet tatsächlich nonfairTryAcquire in der NonfairSync-Klasse. Das spezifische Implementierungsprinzip vergleicht zunächst, ob der aktuelle Sperrstatus 0 ist. Wenn er 0 ist, wird versucht, die Sperre atomar zu ergreifen (den Status auf 1 setzen und dann den aktuellen Thread auf einen exklusiven Thread setzen, wenn die aktuelle Sperre vorliegt). Der Status ist nicht 0. Vergleichen Sie den aktuellen Sperrstatus. Wenn dies der Fall ist, wird der Wert der Statusvariablen erhöht. Daraus können wir den Grund für den Wiedereintritt erkennen Die Wiedereintrittssperre bedeutet, dass derselbe Thread die von ihm belegte Sperre wiederholt verwenden kann. Wenn keine der oben genannten Bedingungen erfüllt ist, geben Sie bei einem Fehler „false“ zurück. Der Code lautet wie folgt:
final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }
3.2 Sobald tryAcquire „false“ zurückgibt, wird in den „acquireQueued“-Prozess eingetreten, bei dem es sich um den Preemption-Modus handelt, der auf der CLH-Warteschlange basiert:
3.2.1. Fügen Sie zunächst in der CLH-Sperre einen Warteknoten am Ende der Warteschlange hinzu und rufen Sie addWaiter auf. Hier müssen Sie die Initialisierungssituation berücksichtigen einen Kopfknoten und fügen Sie dann den aktuellen Knoten zum Schwanz hinzu. Fügen Sie dann einfach den Knoten am Ende hinzu.
Der Code lautet wie folgt:
private Node addWaiter(Node mode) { // 初始化一个节点,这个节点保存当前线程 Node node = new Node(Thread.currentThread(), mode); // 当CLH队列不为空的视乎,直接在队列尾部插入一个节点 Node pred = tail; if (pred != null) { node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } // 当CLH队列为空的时候,调用enq方法初始化队列 enq(node); return node; } private Node enq(final Node node) { for (;;) { Node t = tail; if (t == null) { // 初始化节点,头尾都指向一个空节点 if (compareAndSetHead(new Node())) tail = head; } else {// 考虑并发初始化 node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }
3.2.2 Geben Sie nach dem Hinzufügen des Knotens zur CLH-Warteschlange die Methode „acquireQueued“ ein.
Zuallererst ist die äußere Schicht eine unendliche for-Schleife Wenn der aktuelle Knoten der nächste Knoten des Kopfknotens ist und die Sperre durch tryAcquire erhalten wird, bedeutet dies, dass der Kopf Der Knoten hat die Sperre aufgehoben und der aktuelle Thread wird vom Thread des Hauptknotens aktiviert. Zu diesem Zeitpunkt können Sie den aktuellen Knoten als Hauptknoten festlegen, das Fehlerflag auf „false“ setzen und dann zurückkehren. Die nächste Variable des vorherigen Knotens wird auf Null gesetzt und beim nächsten GC gelöscht.
如果本次循环没有获取到锁,就进入线程挂起阶段,也就是shouldParkAfterFailedAcquire这个方法。
代码如下:
final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; for (;;) { final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; return interrupted; } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } }
3.2.3、如果尝试获取锁失败,就会进入shouldParkAfterFailedAcquire方法,会判断当前线程是否挂起,如果前一个节点已经是SIGNAL状态,则当前线程需要挂起。如果前一个节点是取消状态,则需要将取消节点从队列移除。如果前一个节点状态是其他状态,则尝试设置成SIGNAL状态,并返回不需要挂起,从而进行第二次抢占。完成上面的事后进入挂起阶段。
代码如下:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; if (ws == Node.SIGNAL) // return true; if (ws > 0) { // do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { // compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; }
3.2.4、当进入挂起阶段,会进入parkAndCheckInterrupt方法,则会调用LockSupport.park(this)将当前线程挂起。代码:
private final boolean parkAndCheckInterrupt() { LockSupport.park(this); return Thread.interrupted(); }
1、调用unlock方法,其实是直接调用AbstractQueuedSynchronizer的release操作。
2、进入release方法,内部先尝试tryRelease操作,主要是去除锁的独占线程,然后将状态减一,这里减一主要是考虑到可重入锁可能自身会多次占用锁,只有当状态变成0,才表示完全释放了锁。
3、一旦tryRelease成功,则将CHL队列的头节点的状态设置为0,然后唤醒下一个非取消的节点线程。
4、一旦下一个节点的线程被唤醒,被唤醒的线程就会进入acquireQueued代码流程中,去获取锁。
具体代码如下:
unlock代码:
public void unlock() { sync.release(1); }
release方法代码:
public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }
Sync中通用的tryRelease方法代码:
protected final boolean tryRelease(int releases) { int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { free = true; setExclusiveOwnerThread(null); } setState(c); return free; }
unparkSuccessor代码:
private void unparkSuccessor(Node node) { int ws = node.waitStatus; if (ws < 0) compareAndSetWaitStatus(node, ws, 0); Node s = node.next; if (s == null || s.waitStatus > 0) { s = null; for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) s = t; } if (s != null) LockSupport.unpark(s.thread); }
公平锁和非公平锁,在CHL队列抢占模式上都是一致的,也就是在进入acquireQueued这个方法之后都一样,它们的区别在初次抢占上有区别,也就是tryAcquire上的区别,下面是两者内部调用关系的简图:
NonfairSync lock —> compareAndSetState | —> setExclusiveOwnerThread —> accquire | —> tryAcquire |—>nonfairTryAcquire |—> acquireQueued FairSync lock —> acquire | —> tryAcquire |—>!hasQueuePredecessors |—>compareAndSetState |—>setExclusiveOwnerThread |—> acquireQueued
真正的区别就是公平锁多了hasQueuePredecessors这个方法,这个方法用于判断CHL队列中是否有节点,对于公平锁,如果CHL队列有节点,则新进入竞争的线程一定要在CHL上排队,而非公平锁则是无视CHL队列中的节点,直接进行竞争抢占,这就有可能导致CHL队列上的节点永远获取不到锁,这就是非公平锁之所以不公平的原因。
线程使用ReentrantLock获取锁分为两个阶段,第一个阶段是初次竞争,第二个阶段是基于CHL队列的竞争。在初次竞争的时候是否考虑队列节点直接区分出了公平锁和非公平锁。在基于CHL队列的锁竞争中,依靠CAS操作保证原子操作,依靠LockSupport来做线程的挂起和唤醒,使用队列来保证并发执行变成了串行执行,从而消除了并发所带来的问题。总体来说,ReentrantLock是一个比较轻量级的锁,而且使用面向对象的思想去实现了锁的功能,比原来的synchronized关键字更加好理解。
Das obige ist der detaillierte Inhalt vonDetaillierter Beispielcode, der das Prinzip der Wiedereintrittssperre in Java erläutert. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!