Programmation concurrente Java mortelle (6): AQS expliqué en détail, cette fois, je comprendrai parfaitement le principe du verrouillage dans les packages concurrents Java, donc je n'ai pas à le répéter à chaque entretien

Êtes-vous souvent interrogé sur les verrous en Java pendant l'entretien? Peut-être avez-vous également vu des concepts et des connaissances connexes dans des articles de blog sur Internet, mais si vous n'avez pas une compréhension approfondie, vous pouvez trier ce morceau de connaissance pour former votre propre carte cérébrale de connaissances, et vous l'oublierez bientôt. Le résultat est que chaque entretien doit être revu dès le début, ce qui est long et laborieux.

Aujourd'hui, j'ai commencé à découvrir les verrous dans la concurrence Java. L'objectif principal est de trier les API et les composants liés aux verrous dans les packages de concurrence Java. Le but est de savoir comment l'utiliser et comment le mettre en œuvre. Ce n'est qu'en sachant de quoi il s'agit et pourquoi vous pourrez l'utiliser correctement et gérer l'entretien.

2

Afin de réduire le fardeau des lecteurs, cet article parle principalement d'AQS, c'est-à- AbstractQueuedSynchronizerdire de voir comment il implémente la voix de verrouillage.

Verrouiller l'interface

En parlant de verrous, vous allez certainement penser au mot-clé synchronized , c'est vrai, il a été utilisé par les programmes java pour réaliser la fonction de verrouillage avant jdk1.5. Après jdk1.5, l'interface de verrouillage est ajoutée au package concurrent pour implémenter la fonction de verrouillage. Sa fonction est similaire à synchronisé, mais elle doit être affichée pour acquérir et libérer le verrou.

L'utilisation de Lock est également très simple, comme le montre la démo suivante:

Lock lock = new ReentrantLock();
lock.lock();
try {
} finally {
    lock.unlock();
}

Ici, nous devons expliquer les principales fonctionnalités que la synchronisation fournie par l'interface de verrouillage ne possède pas:

  • Acquisition d'essai du verrou : le thread actuel tente d'acquérir le verrou.Si le verrou actuel n'est pas acquis par d'autres threads, le résultat est acquis et conservé.

  • Acquisition de verrou interruptible : contrairement à la synchronisation, le thread qui acquiert le verrou peut répondre à l'interruption. Lorsque le thread qui acquiert le verrou est interrompu par d'autres threads, l'exception d'interruption est levée et le verrou est libéré en même temps.

  • Acquérir le verrou au fil du temps: acquérez le verrou avant l'heure spécifiée et revenez si le délai d'attente ne peut pas être acquis.

Lock est une interface qui définit les opérations de base d'acquisition et de libération des verrous:

Expliquez la signification de l'API de haut en bas:

  1. Acquérir le verrou Une fois que le thread actuel a acquis le verrou, il revient de cette méthode et la méthode se bloque lors de l'acquisition du verrou;

  2. La différence avec lock () est que cette méthode répondra aux interruptions;

  3. Les tentatives non bloquantes d'acquérir le verrou, la méthode retourne immédiatement et retourne true si elle est acquise, sinon elle retourne false;

  4. Acquérir le verrou au fil du temps, lorsque les trois scénarios de délai d'expiration, d'interruption et d'acquisition sans délai d'expiration, le verrou est acquis, il reviendra;

  5. Relâchez le verrou et réveillez les nœuds suivants;

  6. Obtenez le composant de notification d'attente, qui est lié au verrou actuel, et la méthode d'attente du composant ne peut être appelée qu'après l'acquisition du verrou;

Synchroniseur de file d'attente (AQS)

Le synchroniseur de file d'attente AbstractQueuedSynchronizerqui peut être considéré comme la pierre angulaire de Java et le contrat pour construire une variété de verrous et de conteneurs de sécurité mis en œuvre, tels que pour atteindre ReentrantLock, ReadWriteLock, CountDownLatch, etc. sont moins AQS.

AQS lui-même utilise une variable membre int pour représenter l'état de synchronisation et termine la mise en file d'attente du thread d'acquisition de ressources via la file d'attente FIFO intégrée. Doug Lea, l'auteur du package concurrent Java, voulait qu'il devienne la base de la plupart des exigences de synchronisation lors de sa conception.

Le synchroniseur est la clé de la réalisation de la serrure, bien sûr, il peut aussi s'agir de n'importe quel composant de synchronisation.Le synchroniseur est agrégé dans la réalisation de la serrure, et le synchroniseur sert à réaliser la sémantique de la serrure. La relation entre les deux peut être comprise comme le fait que le verrou est une API orientée programmeur définie dans l'interface de verrouillage, qui définit l'interface interactive utilisée par le programmeur et masque les détails d'implémentation. Le synchroniseur est destiné à l'implémenteur du verrou. Il simplifie l'implémentation du verrou et protège les détails d'implémentation sous-jacents tels que la gestion de l'état de synchronisation, la mise en file d'attente des threads, l'attente et la notification. Cette conception est très bonne et elle isole les domaines auxquels les utilisateurs et les exécutants doivent prêter attention.

Exemple d'utilisation d'AQS

La conception du synchroniseur AQS est basée sur la méthode modèle, c'est-à-dire que l'utilisateur doit hériter du synchroniseur et réécrire la méthode spécifiée, puis combiner le synchroniseur dans un composant de synchronisation personnalisé et appeler la méthode modèle fournie par le synchroniseur. Ces méthodes modèles vont Appelez la méthode de remplacement de l'utilisateur.

Afin de permettre aux utilisateurs de réécrire la méthode spécifiée, le synchroniseur fournit trois méthodes de base:

  1. getState (), récupère l'état de synchronisation actuel

  2. setState (int newState): définit l'état de synchronisation actuel

  3. compareAndSetState (int expect, int update): utilisez CAS pour définir l'état actuel, cette méthode peut garantir l'atomicité du paramètre d'état

同步可重写的方法分为独占式获取锁和共享式获取锁,这里为了不给读者增加负担,只列出独占式获取锁的可重写方法。下面列出简化的源码

protected boolean tryAcquire(long arg) {    
    throw new UnsupportedOperationException();
}
protected boolean tryRelease(long arg) {
    throw new UnsupportedOperationException();
}
protected boolean isHeldExclusively() {
    throw new UnsupportedOperationException();
}

可以看到这些需要重写的方法都是没有具体实现的,所以在使用的时候需要我们去实现。

上面列出需要需要自定义同步组件实现的方法,接下来我们看看同步器提供了哪些模板方法,由于篇幅原因,为了不给读者的阅读带来压力,所以只列出几个核心的方法,具体的大家可以看到JDK源码中 AbstractQueuedSynchronizer 的具体实现。

独占式获取锁

可响应中断的获取锁

释放锁

总的来说,同步器提供的模板方法基本上分为3类:独占式获取与释放同步状态、共享式获取与释放同步状态和查询同步队列中的等待线程情况。自定义同步组件将使用同步器提供的模板方法来实现自己的同步语义。

说了这么多,接下来,我们自己实现一个独占锁,采用组合自定义同步器AQS的方式,帮助大家掌握同步器的工作原理,只有搞懂了AQS才能更加深入的去学习理解 并发包中的其它同步组件。

示例如下如下:

如示例代码所示,大家可以看到实现一个简单的独占锁利用AQS是非常容易的。Mutex中定义了一个静态内部类,它继承了同步器实现了独占式获取和释放同步状态。

tryAcquire(int acquires) 方法中,如果经过CAS设置成功(同步状态设置为1),则代表获 取了同步状态,而在 tryRelease(int releases) 方法中只是将同步状态重置为0。用户使用Mutex时并不会直接和内部同步器的实现打交道,而是调用Mutex提供的方法,在Mutex的实现中,以获 取锁的 lock() 方法为例,只需要在方法实现中调用同步器的模板方法acquire(int args) 即可,这样大大简化了实现一个可靠自定义同步组件的门槛。

AQS实现源码分析

AQS结构

先来看下AQS中都有哪些属性,看了这个你基本就知道AQS实现锁的套路了。

看了之后你会发现很简单吧,就只有三个核心属性。

Le synchroniseur s'appuie sur la file d'attente de synchronisation interne pour terminer la gestion de l'état de synchronisation. Le processus est le suivant: lorsque le thread ne parvient pas à obtenir l'état de synchronisation, le synchroniseur construit le thread actuel et l'état d'attente dans un nœud (Node), l'ajoute à la file d'attente et en même temps Bloquer le thread actuel. Lorsque l'état de synchronisation est libéré, le thread du premier nœud sera réveillé pour lui faire essayer d'acquérir à nouveau l'état de synchronisation de verrouillage.

Les nœuds de la file d'attente de synchronisation sont utilisés pour enregistrer la référence, l'état d'attente et les nœuds prédécesseurs et successeurs du thread qui n'ont pas réussi à obtenir l'état de synchronisation. Examinons le code:

La structure de données de Node n'est en fait pas compliquée, il ne s'agit que de thread + waitStatus + pre + next + nextWaitercinq attributs. Vous devez d'abord avoir ce concept à l'esprit.

Le nœud est la base de la file d'attente de synchronisation. Le synchroniseur a la tête et le nœud de queue. Le thread qui ne parvient pas à obtenir la synchronisation deviendra le nœud et rejoindra la queue de la file d'attente. La structure de base de la file d'attente de synchronisation est la suivante:

Grâce à l'introduction ci-dessus, vous pouvez être anxieux et vouloir voir comment AQS acquiert et libère les verrous.

Ensuite, suivez le code d'implémentation spécifique, je ne suis pas trop long.

Acquérir le verrou

Comme mentionné ci-dessus, les verrous d'acquisition sont divisés en types exclusifs et partagés. Afin de rendre la lecture plus fluide, nous ne regardons ici que les verrous d'acquisition exclusifs. Je pense que vous avez maîtrisé le mode de verrouillage d'acquisition exclusif, et il n'y a donc aucun problème avec l'acquisition partagée. de.

Il y a très peu de code ci-dessus et la logique est relativement claire. Tout d'abord, la méthode tryAcquire (arg) sera appelée. Comme mentionné ci-dessus, cette méthode doit être implémentée par le composant de synchronisation lui-même, comme le verrou Mutex que nous avons implémenté ci-dessus. Cette méthode garantit l'acquisition thread-safe de l'état de synchronisation, tryAcquire (arg) renvoie true pour indiquer que l'acquisition réussit et se termine normalement. Sinon, il construira le nœud de synchronisation (Node.EXCLUSIVE exclusif) et par la addWaiter(Node mode)méthode sera ajouté à la queue de la synchronisation de la file d'attente, l'appel final acquireQueued(final Node node, int arg)pour obtenir l'état de la synchronisation par l'approche "cycle de mort". S'il n'est pas disponible, le thread correspondant dans le nœud sera bloqué et le réveil après avoir été bloqué ne peut être obtenu qu'en retirant la file d'attente du nœud précurseur ou en interrompant le thread bloqué.

Regardons la structure du nœud et l'ajoutons à la file d'attente de synchronisation.

Le code ci-dessus utilise la méthode compareAndSetTail (pred, node) pour garantir que le nœud peut être ajouté en toute sécurité lors de l'ajout du nœud construit à la fin de la file d'attente de synchronisation.

下面我们来看下当上面的快速加入同步队列末尾不满足条件时(即上面代码中显示的队列为空或者有多线程并发入队),走到了 enq(node) 方法,即采用自旋的方式入队。

具体就不啰嗦了,上述代码已经写得很清晰了,就是 enq(final Node node) 方法,在死循环中通过CAS将节点设置为尾结点之后,线程才从该方法返回。否则当前线程不断的尝试。 可以看到这个方法使用场景本来不是线程安全的,因为同时可能有很多 调用 tryAcquire 方法获取同步状态失败的线程要进行入队操作。此处巧妙的用自旋加CAS将并发请求变得 “串行化了”。

经过上述方法,节点就进入了同步队列中后,就进入到了一个下一个自旋的过程,每个节点(即获取锁失败的线程)都在自省的观察,当条件满足,获取到了同步状态,就可以从这个自旋过程中退出,否则就苦逼的滞留在这个自旋过程中,并且阻塞节点线程。具体代码如下:

如上,假如当前node本来就不是队头或者就是 tryAcquire(arg) 没有抢赢别人,就是走到下一个分支判断:shouldParkAfterFailedAcquire(p, node) 当前线程没有抢到锁,是否需要挂起当前线程

上面的代码你一定要自己的理解,如果思路断了希望从上面在顺一遍,以免浪费时间。

这里我们分析下private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) 这个方法返回值的情况:

  1. 如果返回true, 说明前驱节点的 waitStatus==-1,是正常情况,那么当前线程需要被挂起,等待以后被唤醒,就等着前驱节点拿到锁,然后释放锁的时候叫你好了;

  2. 如果返回false, 说明当前不需要被挂起,为什么呢?往后看

shouldParkAfterFailedAcquire(Node pred, Node node) 这个方法返回后,是true 则执行 parkAndCheckInterrupt() 方法:

接下来说说如果 shouldParkAfterFailedAcquire(p, node) 返回false的情况:仔细看shouldParkAfterFailedAcquire(p, node),我们可以发现,其实第一次进来的时候,一般都不会返回true的,原因很简单,前驱节点的 waitStatus=-1 是依赖于后继节点设置的。也就是说,我都还没给前驱设置-1呢,怎么可能是true呢,但是要看到,这个方法是套在循环里的,所以第二次进来的时候状态就是-1了。

如果看到这里思路还是比较清晰的话,那么这里我们再来解释下为什么shouldParkAfterFailedAcquire(p, node) 返回false的时候不直接挂起线程?

这是因为经过这个方法后,当前节点的前一个节点有可能因为超时或者中断而取消阻塞退出同步队列因此设置了新的父节点,这个父节点有可能就已经是head了,这里有没有恍然大悟的感觉。。。

说到这里也就明白了 AQS同步器获取锁的过程,还是希望你能多看几遍 acquireQueued(final Node node, int arg) 方法。 代码不多,花时间推演下各个分支进入的原因,这个时间是值得投入的。

释放锁

当前线程获取同步状态并执行了相应逻辑之后,就需要释放同步状态,使得后续节点能够继续获取同步状态。通过调用同步器的release(int arg)方法可以释放同步状态,该方法在释放了同步状态之后,会唤醒其后继节点(进而使后继节点重新尝试获取同步状态)。

唤醒的代码还是比较简单的,你如果上面加锁的都看懂了,下面都不需要看就知道怎么回事了!

唤醒线程以后,被唤醒的线程将从以下代码中继续往前走:

好了,能看完到这里的你肯定已经对于AQS同步器独占式获取锁和解锁流程有了一定的了解,这篇文章就不继续怼源码了。 相信你看懂了上面的,如果还有问题或者想看下非独占式获取释放锁流程,自己去老老实实仔细看看代码吧。

总结

总结一下吧。

Java并发包中提供了锁的另一种实现Lock接口,它定义了锁的获取和释放基本操作。

Le synchroniseur de file d'attente AbstractQueuedSynchronizerqui peut être considéré comme la pierre angulaire de Java et le contrat pour construire une variété de verrous et de conteneurs de sécurité mis en œuvre, tels que pour atteindre ReentrantLock, ReadWriteLock, CountDownLatch, etc. sont moins AQS.

Dans un environnement simultané, divers verrous qui implémentent l'interface de verrouillage sont fournis dans le package simultané. Ils s'appuient sur le synchroniseur AQS pour effectuer les opérations de verrouillage et de déverrouillage. La réalisation d'AQS nécessite principalement la coordination des trois volets suivants:

  1. État de verrouillage. Nous avons besoin de savoir si le verrou est occupé par d'autres threads. C'est la fonction de l'état. Lorsqu'il est à 0, cela signifie qu'aucun thread n'a le verrou. Vous pouvez concourir pour ce verrou. Utilisez CAS pour définir l'état sur 1. Si CAS réussit, cela signifie Le verrou est saisi afin que les autres threads ne puissent pas l'attraper. Si le verrou est réentré, l'état peut être +1 et le déverrouillage est réduit de 1 jusqu'à ce que l'état redevienne 0, ce qui signifie que le verrou est libéré, donc lock () et unlock () doivent être Besoin d'être jumelé. Réveillez ensuite le premier thread de la file d'attente de synchronisation et laissez-le maintenir le verrou.

  2. Blocage et déblocage des threads. AQS utilise LockSupport.park (thread) pour suspendre les threads et unpark pour réveiller les threads.

  3. Bloquez la file d'attente. Parce qu'il peut y avoir de nombreux threads en concurrence pour les verrous, mais qu'un seul thread peut obtenir le verrou et que les autres threads doivent attendre. À ce stade, une file d'attente est nécessaire pour gérer ces threads. AQS utilise une file d'attente FIFO, qui est une liste liée. Chaque nœud contient une référence aux nœuds suivants.

exemple de graphique

Cette image est utilisée pour passer en revue le processus d'acquisition du verrou. Si vous êtes encore un peu abasourdi après l'avoir lu, voici une autre occasion de vous aider à faire le tri. Combinez cette image et réfléchissez bien. Vous avez cette idée en tête, puis examinez à nouveau le code source. .

(Fin de cet article)


Matériaux de référence :

  1. Zhou Zhiming: "Compréhension approfondie de la machine virtuelle Java"

  2. Fang Tengfei: "L'art de la programmation simultanée Java"

Je suppose que tu aimes

Origine blog.csdn.net/taurus_7c/article/details/105760231
conseillé
Classement