Cet article présente d'abord l'interface Lock, la hiérarchie des classes de ReentrantLock et le principe simple de la classe de modèle de fonction de verrouillage AbstractQueuedSynchronizer, puis explique les principes internes de ReentrantLock en analysant le verrou. méthode et méthode de déverrouillage de ReentrantLock , et enfin faire un résumé. Cet article ne couvre pas les variables de condition dans ReentrantLock.
L'interface de verrouillage est une abstraction d'outils permettant de contrôler la concurrence. Il est plus flexible que l'utilisation du mot-clé synchronisé et peut prendre en charge les variables de condition. C'est un outil de contrôle de la concurrence. De manière générale, il contrôle l'exclusivité d'une certaine ressource partagée. En d’autres termes, un seul thread peut acquérir ce verrou et occuper des ressources en même temps. Si d'autres threads souhaitent acquérir le verrou, ils doivent attendre que ce thread libère le verrou. ReentrantLock dans l’implémentation Java est un tel verrou. Un autre type de verrou, qui peut permettre à plusieurs threads de lire des ressources, mais ne peut autoriser qu'un seul thread à écrire des ressources, est un verrou spécial, appelé verrou en lecture-écriture. Ce qui suit est une description générale de plusieurs méthodes de l'interface Lock :
Nom de la méthode | Description|||||||||||||||
verrouillage | Acquérir le verrou. Si le verrou ne peut pas être acquis, le thread actuel devient non planifiable jusqu'à ce que le verrou soit acquis | ||||||||||||||
lockInterruptably | Acquérir le verrou à moins que le thread actuel ne soit interrompu. Si le verrou est acquis, revenez immédiatement. S'il ne peut pas être acquis, le thread actuel devient non planifiable et se met en veille jusqu'à ce que les deux choses suivantes se produisent :
|
||||||||||||||
tryLock | Si le verrou peut être acquis lorsqu'il est appelé, puis acquérez le verrou et retournez vrai. Si le verrou actuel ne peut pas être acquis, alors cette méthode retournera immédiatement faux | ||||||||||||||
tryLcok(long time,TimeUnit unit) | Essayez d'acquérir le verrou dans le délai spécifié. Si le verrou peut être acquis, obtenez-le. et renvoie true, si le verrou actuel ne peut pas être acquis, alors le thread actuel devient non planifiable jusqu'à ce que l'une des trois choses suivantes se produise : 1 Le thread actuel acquiert le verrou 2. bloqué par une autre interruption du fil3. Le temps d'attente spécifié est écoulé | ||||||||||||||
déverrouiller | Libérer le verrou occupé par le thread actuel | ||||||||||||||
newCondition | Renvoie une variable de condition associée au verrou actuel. Avant d'utiliser cette variable de condition, le thread actuel doit occuper le verrou. La méthode wait de la condition appelante libérera atomiquement le verrou avant d'attendre et acquerra atomiquement le verrou après avoir attendu d'être réveillé |
Ensuite, nous présenterons comment l'ensemble du ReentrantLock fonctionne autour des deux méthodes de verrouillage et de déverrouillage. Avant de présenter ReentrantLock, examinons d'abord la hiérarchie de classes de ReentrantLock et son AbstractQueuedSynchronizer
ReentrantLock Il implémente le Interface de verrouillage et comporte trois classes internes, Sync, NonfairSync et FairSync. Sync est un type abstrait qui hérite de AbstractQueuedSynchronizer est une classe de modèle qui implémente de nombreuses fonctions liées au verrouillage et fournit des méthodes de hook pour l'implémentation de l'utilisateur, telles que tryAcquire, tryRelease. , etc. Sync implémente la méthode tryRelease de AbstractQueuedSynchronizer. Les classes NonfairSync et FairSync héritent de Sync, implémentent la méthode de verrouillage, puis ont respectivement différentes implémentations de tryAcquire pour une préemption équitable et une préemption injuste.
Tout d'abord, AbstractQueuedSynchronizer hérite de AbstractOwnableSynchronizer L'implémentation de AbstractOwnableSynchronizer est très simple. Elle représente un synchroniseur exclusif, et la variable exclusiveOwnerThread est utilisée en interne pour représenter un thread exclusif. .
Deuxièmement, AbstractQueuedSynchronizer utilise en interne la file d'attente de verrouillage CLH pour transformer l'exécution simultanée en exécution série. La file d'attente entière est une liste doublement chaînée. Chaque nœud de la file d'attente de verrouillage CLH enregistrera les références au nœud précédent et au nœud suivant, le thread correspondant au nœud actuel et un état. Ce statut est utilisé pour indiquer si le thread doit se bloquer. Lorsque le nœud précédent du nœud est libéré, le nœud actuel est réveillé et devient la tête. Les nœuds nouvellement ajoutés seront placés à la fin de la file d'attente.
1. Lors de l'initialisation de ReentrantLock, si nous ne précisons pas si les paramètres sont équitables, alors le verrouillage injuste sera utilisé par défaut, qui est NonfairSync.
2. Lorsque nous appelons la méthode de verrouillage de ReentrantLock, nous appelons en fait la méthode de verrouillage de NonfairSync. Cette méthode utilise d'abord l'opération CAS pour essayer de saisir le verrou. En cas de succès, le thread actuel est défini sur ce verrou, indiquant que la préemption est réussie. En cas d'échec, appelez la méthode d'acquisition du modèle et attendez la préemption. Le code est le suivant :
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. L'appel de acquire(1) utilise en fait la méthode d'acquisition de AbstractQueuedSynchronizer, qui est un ensemble de modèles de préemption de verrou. Le principe général est d'essayer d'acquérir le verrou en premier. S'il n'est pas acquis, en cas de succès, un nœud du thread actuel est ajouté à la file d'attente CLH, indiquant qu'il est en attente de préemption. Entrez ensuite dans le mode de préemption de la file d'attente CLH Lors de l'entrée, il effectuera également une opération pour acquérir le verrou. S'il ne peut toujours pas être obtenu, LockSupport.park sera appelé pour suspendre le thread en cours. Alors, quand le fil actuel sera-t-il réveillé ? Lorsque le thread détenant le verrou appelle unlock, il réveille le thread sur le nœud à côté du nœud principal de la file d'attente CLH et appelle la méthode LockSupport.unpark. Le code d'acquisition est relativement simple, comme suit :
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
3.1. La méthode hook tryAcquire est d'abord utilisée dans la méthode d'acquisition pour tenter d'acquérir à nouveau le verrou. Cette méthode utilise en fait nonfairTryAcquire dans la classe NonfairSync. Le principe d'implémentation spécifique Il compare d'abord si l'état actuel du verrou est 0. S'il est 0, il essaie de saisir atomiquement le verrou (définit l'état sur 1, puis définit le thread actuel sur un thread exclusif si le verrou actuel). status n'est pas 0, comparez l'état actuel du verrou. Si le thread et le thread occupant le verrou sont le même thread, si c'est le cas, la valeur de la variable d'état sera augmentée, nous pouvons voir la raison pour laquelle le réentrant. Le verrou est réentrant est que le même thread peut utiliser à plusieurs reprises le verrou qu'il occupe. Si aucune des conditions ci-dessus n’est remplie, renvoie false en cas d’échec. Le code est le suivant :
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. Une fois que tryAcquire renvoie false, il entrera dans le processus acquireQueued, qui est le mode de préemption basé sur la file d'attente CLH :
3.2.1. Tout d'abord, dans le verrou CLH, ajoutez un nœud en attente à la fin de la file d'attente. Ce nœud enregistre le thread actuel et est implémenté en appelant addWaiter. Ici, vous devez considérer la situation d'initialisation. Lorsque le premier nœud en attente entre, vous devez l'initialiser. un nœud principal, puis ajoutez le nœud actuel à la queue. Ensuite, ajoutez simplement le nœud à la fin.
Le code est le suivant :
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 Après avoir ajouté le nœud à la file d'attente CLH, entrez la méthode acquireQueued.
Tout d'abord, la couche externe est une boucle for infinie Si le nœud actuel est le nœud suivant du nœud principal et que le verrou est obtenu via tryAcquire, cela signifie que la tête. Le nœud a libéré le verrou et le thread actuel Il est réveillé par le thread du nœud principal. À ce moment, vous pouvez définir le nœud actuel comme nœud principal, définir l'indicateur d'échec sur false, puis revenir. Comme pour le nœud précédent, sa variable suivante est définie sur null et sera effacée lors du prochain GC.
如果本次循环没有获取到锁,就进入线程挂起阶段,也就是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关键字更加好理解。
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!