Maison >Java >javaDidacticiel >Avez-vous utilisé la bonne serrure ? Une brève discussion sur les questions de « verrouillage » Java
À chaque époque, ceux qui savent apprendre ne seront pas maltraités
J'ai récemment découvert que les nouveaux collègues de l'entreprise ont des malentendus sur les serrures, alors parlons aujourd'hui des « serrures » et des conteneurs de sécurité simultanés dans Java Quelles sont les précautions d'emploi ?
Mais avant cela, nous devons encore expliquer pourquoi nous devons verrouiller cette chose. Cela commence par la source du bug de concurrence.
J'ai écrit un article sur ce problème en 2019. En repensant à cet article maintenant, je me sens vraiment gêné.
Jetons un coup d'œil à la source. Nous savons qu'un ordinateur possède un processeur, une mémoire et un disque dur. La vitesse de lecture du disque dur est la plus lente, suivie de la lecture de la mémoire. . La lecture de la mémoire est plus lente que le fonctionnement du CPU. C'était trop lent, j'ai donc créé un autre cache CPU, L1, L2 et L3.
C'est ce Cache CPU couplé à la situation actuelle du CPU multicœur qui produit un BUG de concurrence.
Il s'agit d'un code très simple. S'il y a un thread A et un thread B qui exécutent cette méthode respectivement dans le CPU-A et le CPU-B, leur opération consiste à accéder d'abord à a depuis la mémoire principale vers le CPU In. le cache, la valeur de a dans leur cache est entièrement 0 à ce moment.
Ensuite, ils exécutent respectivement a++. À ce moment-là, la valeur de a à leurs yeux respectifs est 1. Lorsque a est flashé plus tard dans la mémoire principale, la valeur de a est toujours 1. C'est un problème évident. l'ajout final de un est exécuté deux fois. Le résultat est 1 et non 2.
Ce problème est appelé problème de visibilité.
En regardant notre déclaration a++, nos langages actuels sont tous des langages de haut niveau. C'est en fait très similaire au sucre syntaxique. Cela semble très pratique à utiliser. instructions qui doivent vraiment être exécutées.
Une instruction dans un langage de haut niveau peut être traduite en plusieurs instructions CPU. Par exemple, a++ peut être traduit en au moins trois instructions CPU.
Obtenez un de mémoire pour vous inscrire
+1 dans l'inscription
Écrivez le résultat dans le cache ou dans la mémoire
C'est impossible de le faire ; interrompre une instruction car elle est atomique. En fait, le CPU peut exécuter une instruction lorsque la tranche de temps est écoulée. À ce moment, le contexte passe à un autre thread, qui exécute également a++. Lorsque vous revenez en arrière, la valeur de a est en fait fausse.
Ce problème s'appelle le problème de l'atomicité.
Et afin d'optimiser les performances, le compilateur ou l'interpréteur peut modifier l'ordre d'exécution des instructions. C'est ce qu'on appelle le réarrangement des instructions. L'exemple le plus classique est la double vérification du mode singleton. Afin d'améliorer l'efficacité d'exécution, le CPU s'exécutera dans le désordre. Par exemple, lorsque le CPU attend le chargement des données en mémoire, il constate que l'instruction d'ajout suivante ne dépend pas du résultat de calcul de l'instruction précédente, donc il exécute d'abord l'instruction d'addition.
Ce problème s'appelle le problème de commande.
Nous avons maintenant analysé les sources des bugs de concurrence, à savoir ces trois problèmes majeurs. On voit que qu'il s'agisse de cache CPU, de CPU multicœur, de langage de haut niveau ou de réarrangement dans le désordre, c'est en fait nécessaire, nous ne pouvons donc affronter ces problèmes que de front.
Résoudre ces problèmes consiste à désactiver la mise en cache, à interdire le réarrangement des instructions du compilateur, l'exclusion mutuelle, etc. Aujourd'hui, notre sujet est lié à l'exclusion mutuelle.
L'exclusion mutuelle garantit que les modifications apportées aux variables partagées s'excluent mutuellement, c'est-à-dire qu'un seul thread s'exécute en même temps. Lorsqu’il est question d’exclusion mutuelle, je crois que ce qui vient à l’esprit de tout le monde, c’est le lock. Oui, notre sujet aujourd'hui est les serrures ! Les verrous sont conçus pour résoudre le problème de l’atomicité.
Quand il s'agit de verrous, la première réaction des étudiants Java est le mot-clé synchronisé, après tout, il est pris en charge au niveau du langage. Jetons d'abord un coup d'œil à la synchronisation. Certains étudiants ne comprennent pas bien la synchronisation, son utilisation comporte donc de nombreux pièges.
Jetons d'abord un coup d'œil à un code. Ce code est notre façon d'augmenter les salaires. En fin de compte, des millions seront payés. Et un fil compare toujours si nos salaires sont égaux. Permettez-moi de dire brièvement IntStream .rangeClosed(1,1000000).forEach
, certaines personnes ne sont peut-être pas familières avec cela. Ce code équivaut à une boucle for 1 million de fois. IntStream.rangeClosed(1,1000000).forEach
,可能有些人对这个不太熟悉,这个代码的就等于 for 循环了100W次。
你先自己理解下,看看觉得有没有什么问题?第一反应好像没问题,你看着涨工资就一个线程执行着,这比工资也没有修改值,看起来好像没啥毛病?没有啥并发资源的竞争,也用 volatile 修饰了保证了可见性。
让我们来看一下结果,我截取了一部分。
可以看到首先有 log 打出来就已经不对了,其次打出来的值竟然还相等!有没有出乎你的意料之外?有同学可能下意识就想到这就raiseSalary
在修改,所以肯定是线程安全问题来给raiseSalary
加个锁!
请注意只有一个线程在调用raiseSalary
方法,所以单给raiseSalary
raiseSalary
est en cours de modification, il doit donc s'agir d'un problème de sécurité des threads pour raiseSalary
Ajouter un verrou ! 🎜🎜Veuillez noter qu'il n'y a qu'un seul fil de discussion appelant raiseSalary
, alors donnez simplement raiseSalary
ne sert à rien. 🎜C'est en fait le problème d'atomicité que j'ai mentionné ci-dessus. Imaginez que le fil d'augmentation de salaire termine son exécutionyesSalary++
n'a pas encore été exécutéyourSalary++
, le thread de salaire s'exécute simplement versyesSalary != yourSalary
Est-ce vraiment vrai ? C'est pourquoi le journal est imprimé. yesSalary++
还未执行yourSalary++
时,比工资线程刚好执行到yesSalary != yourSalary
是不是肯定是 true ?所以才会打印出 log。
再者由于用 volatile 修饰保证了可见性,所以当打 log 的时候,可能yourSalary++
已经执行完了,这时候打出来的 log 才会是yesSalary == yourSalary
。
所以最简单的解决办法就是把raiseSalary()
和 compareSalary()
都用 synchronized 修饰,这样涨工资和比工资两个线程就不会在同一时刻执行,因此肯定就安全了!
看起来锁好像也挺简单,不过这个 synchronized 的使用还是对于新手来说还是有坑的,就是你要关注 synchronized 锁的究竟是什么。
比如我改成多线程来涨工资。这里再提一下parallel
yourSalary++
a été exécuté et la sortie du journal à ce moment sera ouiSalaire == votre Salaire
. 🎜🎜Donc la solution la plus simple est de mettre raiseSalary()
et compareSalary()
sont tous modifiés avec synchronisé, de sorte que les deux threads d'augmentation de salaire et de comparaison de salaire ne seront pas exécutés en même temps, donc c'est définitivement sûr ! 🎜🎜Il semble que le verrou soit assez simple, mais c'est synchronisé Il y a encore des pièges pour les novices qui l'utilisent, c'est-à-dire qu'il faut faire attention à ce que sont les verrous synchronisés. 🎜🎜Par exemple, je suis passé au multi-threading pour augmenter mon salaire. Permettez-moi de mentionner à nouveauparallèle
, cela utilise en fait l'opération de pool de threads ForkJoinPool, et le nombre de threads par défaut est le nombre de cœurs de processeur. 🎜En raison de l'instance raiseSalary()
加了锁,所以最终的结果是对的。这是因为 synchronized 修饰的是yesLockDemo
, il n'y a qu'une seule instance dans notre instance principale, donc ce qui équivaut à une compétition multithread est un verrou, donc les données finales calculées sont correctes.
Ensuite, je modifierai le code pour que chaque thread ait sa propre instance yesLockDemo pour augmenter le salaire.
Vous découvrirez pourquoi ce verrou ne sert à rien ? Le salaire annuel promis d'un million sera changé à 100 000 ? ? Heureusement, il vous reste encore 70w.
C'est parce que notre lock modifie une méthode non statique à ce moment-là, qui est un verrou au niveau de l'instance, et nous avons créé une instance pour chaque thread, donc ce pour quoi ces threads sont en compétition n'est pas du tout un verrou , et le code correct pour le calcul multithread ci-dessus est dû au fait que chaque thread utilise la même instance, il est donc en compétition pour un verrou. Si vous souhaitez que le code soit correct à ce moment-là, il vous suffit de changer le verrouillage au niveau de l'instance en un verrouillage au niveau de la classe.
C'est très simple. Transformez simplement cette méthode en méthode statique la modification synchronisée de la méthode statique est un verrou au niveau de la classe.
Une autre façon consiste à déclarer une variable statique, ce qui est plus recommandé, car transformer une méthode non statique en méthode statique équivaut en fait à changer la structure du code.
Résumons. Lors de l'utilisation de synchronisé, vous devez faire attention à ce qu'est le verrou Si vous modifiez des champs statiques et des méthodes statiques, il s'agit d'un verrou au niveau de la classe. -méthodes statiques, il s'agit d'un verrou au niveau de l'instance.
Je crois que tout le monde sait que Hashtable n'est pas recommandé. Si vous souhaitez l'utiliser, utilisez ConcurrentHashMap En effet, bien que Hashtable soit thread-safe, il traite toutes les méthodes du. de la même manière. Jetons un coup d'œil au code source.
À votre avis, qu'est-ce que le contenu a à voir avec la méthode de taille ? Pourquoi ne suis-je pas autorisé à ajuster la taille lorsque j'appelle contain ? C'est parce que la granularité du verrouillage est trop grossière. Nous devons l'évaluer. Différentes méthodes utilisent des verrous différents, afin d'améliorer la concurrence en termes de sécurité des threads.
Mais différents verrous pour différentes méthodes ne suffisent pas, car parfois certaines opérations dans une méthode sont en fait thread-safe, Seul le code impliquant une compétition pour les ressources de course doit être verrouillé. Surtout si le code qui ne nécessite pas de verrou prend beaucoup de temps, il occupera le verrou pendant longtemps et les autres threads ne peuvent qu'attendre en ligne, comme le code suivant.
Évidemment, le deuxième morceau de code est la manière normale d'utiliser la serrure, mais dans le code professionnel habituel, il n'est pas aussi facile à voir d'un coup d'œil que le sommeil affiché dans mon code Parfois, il faut le faire. être modifié. L'ordre d'exécution du code, etc., garantit que la granularité du verrouillage est suffisamment fine.
Parfois, nous devons nous assurer que le verrou est suffisamment épais, mais cette partie de la JVM sera détectée et elle nous aidera à l'optimiser, comme le code suivant.
Vous pouvez voir que la logique appelée dans une méthode est passée par 加锁-执行A-解锁-加锁-执行B-解锁
,很明显的可以看出其实只需要经历加锁-执行A-执行B-解锁
.
Ainsi, la JVM va durcir le verrou lors de la compilation juste à temps et étendre la portée du verrou, comme dans la situation suivante.
Et la JVM aura également des actions d'élimination de verrouillageIl est jugé par analyse d'échappement que l'objet d'instance est thread privé, il doit donc être thread-safe, donc l'action de verrouillage dans l'objet sera ignorée. et j'ai appelé directement.
Il divise un verrou en un verrou en lecture et un verrou en écriture, ce qui est particulièrement. convient aux situations où il y a plus de lecture et moins d'écriture. Utilisez , par exemple, implémentez vous-même un cache.
permet à plusieurs threads de lire des variables partagées en même temps, mais les opérations d'écriture s'excluent mutuellement, c'est-à-dire que l'écriture-écriture s'exclut mutuellement et la lecture et l'écriture s'excluent mutuellement. Pour parler franchement, lors de l'écriture, un seul thread peut écrire, et les autres threads ne peuvent pas lire ou écrire.
Jetons un coup d'œil à un petit exemple, qui comporte également un petit détail. Ce code sert à simuler la lecture du cache. Tout d'abord, le verrou de lecture est utilisé pour obtenir les données du cache. S'il n'y a pas de données dans le cache, le verrou de lecture est ensuite utilisé pour obtenir les données. données de la base de données, puis les données sont placées dans le cache et renvoyées. Le petit détail ici est de juger à nouveau, puis le cache est vide, donc ils se disputent tous le verrou en écriture. En fin de compte, un seul thread obtiendra le verrou en écriture en premier, puis remplira les données. dans le cache. data = getFromCache()
是否有值,因为同一时刻可能会有多个线程调用getData()
Bien sûr, tout le monde connaît le paradigme d'utilisation de Lock. Vous devez utiliser try-finally
pour vous assurer qu'il sera déverrouillé. Il y a un autre point important à noter concernant les verrous en lecture-écriture, à savoir que try- finally
,来保证一定会解锁。而读写锁还有一个要点需要注意,也就是说锁不能升级。什么意思呢?我改一下上面的代码。
但是写锁内可以再用读锁,来实现锁的降级,有些人可能会问了这写锁都加了还要什么读锁。
还是有点用处的,比如某个线程抢到了写锁,在写的动作要完毕的时候加上读锁,接着释放了写锁,此时它还持有读锁可以保证能马上使用写锁操作完的数据,而别的线程也因为此时写锁已经没了也能读数据。
其实就是当前已经不需要写锁这种比较霸道的锁!所以来降个级让大家都能读。
小结一下,读写锁适用于读多写少的情况,无法升级,但是可以降级。Lock 的锁需要配合 try- finally
les verrous ne peuvent pas être mis à niveau
Mais le verrou en lecture peut être réutilisé à l'intérieur du verrou en écriture . Pour réaliser le lock downgrade, certaines personnes peuvent se demander pourquoi les verrous en lecture sont nécessaires après l'ajout des verrous en écriture.
C'est toujours quelque peu utile. Par exemple, un thread saisit le verrou en écriture, ajoute un verrou en lecture lorsque l'action d'écriture est sur le point d'être terminée, puis libère le verrou en écritureÀ ce stade, il détient toujours le verrou en lecture. pour garantir que l'écriture peut être utilisée immédiatement Après avoir verrouillé les données, d'autres threads peuvent également lire les données car le verrou en écriture a disparu à ce moment-là.
En fait, il n'est pas nécessaire de recourir à des verrous plus autoritaires tels que les verrous en écriture ! Alors rétrogradons-le pour que tout le monde puisse le lire.Donc
dans les scénarios qui ne conviennent pas aux verrous en lecture-écriture, il est préférable d'utiliser directement les verrous mutex, car les verrous en lecture-écriture doivent également effectuer un jugement de déplacement sur l'état et d'autres opérations.
🎜🎜🎜🎜StampedLock🎜🎜🎜🎜🎜Je voudrais aussi mentionner un peu cette chose, elle est de 1,8 et son taux d'apparition ne semble pas être aussi élevé que ReentrantReadWriteLock. Il prend en charge les verrous en écriture, les verrous en lecture pessimistes et les lectures optimistes. Les verrous en écriture et les verrous en lecture pessimistes sont en fait les mêmes que les verrous en lecture-écriture dans ReentrantReadWriteLock, qui a une lecture optimiste supplémentaire. 🎜🎜D'après l'analyse ci-dessus, nous savons que le verrou en lecture-écriture est en fait incapable d'écrire lors de la lecture, 🎜et la lecture optimiste de StampedLock permet à un thread d'écrire🎜. La lecture optimiste est en fait la même que le verrouillage optimiste de la base de données que nous connaissons. Le verrouillage optimiste de la base de données est jugé par un champ de version, tel que le SQL suivant. 🎜StampedLock La lecture optimiste lui ressemble, jetons un coup d'œil à son utilisation simple.
C'est là qu'il se compare à ReentrantReadWriteLock. D'autres ne sont pas bons. Par exemple, StampedLock ne prend pas en charge la réentrance et ne prend pas en charge les variables de condition. Un autre point est que lorsque vous utilisez StampedLock, vous ne devez pas appeler d'opérations d'interruption, car cela entraînerait le CPU à 100% J'ai exécuté l'exemple fourni sur le site Web de Concurrent Programming et je l'ai reproduit.
Les raisons spécifiques ne seront pas détaillées ici. Un lien sera publié à la fin de l'article. Ce qui précède est très détaillé.
Donc, lorsque quelque chose qui semble puissant sort, vous devez vraiment le comprendre et le connaître afin d'être ciblé.
La copie sur écriture est également utilisée à de nombreux endroits, tels que processfork()
opération. Il est également très utile pour notre niveau de code métier car ses opérations de lecture ne bloquent pas les écritures et les opérations d'écriture ne bloquent pas les lectures. Convient aux scénarios où il y a beaucoup de lecture et un peu d'écriture. fork()
操作。对于我们业务代码层面而言也是很有帮助的,在于它的读操作不会阻塞写,写操作也不会阻塞读。适用于读多写少的场景。
例如 Java 中的实现 CopyOnWriteArrayList
,有人可能一听,这玩意线程安全读的时候还不会阻塞写,好家伙就用它了!
你得先搞清楚,写时复制是会拷贝一份数据,你的任何一个修改动作在CopyOnWriteArrayList
中都会触发一次Arrays.copyOf
CopyOnWriteArrayList
, certaines personnes peuvent entendre que cette chose ne bloque pas l'écriture lorsqu'elle est thread-safe à lire, alors les bons l'utilisent ! 🎜🎜Vous devez d'abord comprendre que 🎜la copie sur écriture copiera une copie des données🎜, et toutes vos actions de modification seront en CopyOnWriteArrayList
sera déclenché une foisArrays.copyOf
, puis modifiez-le sur la copie. S'il y a de nombreuses actions de modification et que les données copiées sont également volumineuses, ce sera un désastre ! 🎜Enfin, parlons de l'utilisation de conteneurs de sécurité simultanés, je prendrai comme exemple le ConcurrentHashMap relativement familier. Je pense que les nouveaux collègues semblent penser que tant qu'ils utilisent des conteneurs de sécurité simultanés, ils doivent être thread-safe. En fait, pas nécessairement, cela dépend de la manière dont on l’utilise.
Jetons d'abord un coup d'œil au code suivant. Pour faire simple, il utilise ConcurrentHashMap pour enregistrer le salaire de chacun, jusqu'à 100.
Le résultat final dépassera la norme, c'est-à-dire qu'il n'y a pas seulement 100 personnes enregistrées sur la carte. Alors, comment le résultat peut-il être correct ? C'est aussi simple que d'ajouter un verrou.
Quelqu'un a dit après avoir vu cela, pourquoi devrais-je utiliser ConcurrentHashMap si vous l'avez déjà verrouillé, je peux simplement ajouter un verrou à HashMap et tout ira bien ! Oui tu as raison ! Parce que notre scénario d'utilisation actuel est une opération composée, c'est-à-dire que nous jugeons d'abord la taille de la carte, puis exécutons la méthode put ConcurrentHashMap ne peut pas garantir que les opérations composées sont thread-safe !
Et ConcurrentHashMap ne convient qu'à Use. pour exposer des méthodes thread-safe au lieu d'opérations composées. Par exemple, le code suivant
Bien sûr, mon exemple n'est pas assez approprié. En fait, la raison pour laquelle ConcurrentHashMap a des performances supérieures à HashMap + lock est due au verrouillage segmenté, qui nécessite la réflexion de plusieurs opérations clés. Cependant, le point clé que je souhaite souligner est l'utilisation de Vous ne pouvez pas être négligent, et vous ne pouvez pas simplement penser que son utilisation le rendra thread-safe.
Aujourd'hui, nous avons parlé des sources des bugs de concurrence, à savoir trois problèmes majeurs : les problèmes de visibilité, les problèmes d'atomicité et les problèmes d'ordre. Ensuite, j'ai brièvement parlé des points clés du mot-clé synchronisé, c'est-à-dire que la modification de champs statiques ou de méthodes statiques est un verrou au niveau de la classe, tandis que la modification de champs non statiques et de méthodes non statiques est une classe au niveau de l'instance.
Parlons de la granularité des verrous. La définition de différents verrous dans différents scénarios ne peut pas se faire avec un seul verrou, et la granularité des verrous internes de la méthode doit être bonne. Par exemple, dans un scénario où il y a beaucoup de lecture mais peu d'écriture, des verrous en lecture-écriture, une copie sur écriture, etc. peuvent être utilisés.
En fin de compte, nous devons utiliser correctement les conteneurs de sécurité simultanés. Nous ne pouvons pas penser aveuglément que l'utilisation de conteneurs de sécurité simultanés doit être thread-safe.
Bien sûr, je viens d'en parler brièvement aujourd'hui. Il y a en fait de nombreux points sur la programmation simultanée. Il n'est pas facile d'écrire du code thread-safe, tout comme l'ensemble du processus de traitement des événements Kafka que j'ai analysé auparavant. la version originale était axée sur la concurrence et la sécurité contrôlées par divers verrous. Plus tard, les bogues n'ont pas pu être corrigés du tout, le débogage était difficile et la correction des bogues était également difficile.
Le module de traitement des événements Kafka a finalement été remplacé par le mode de file d'attente d'événements à thread unique, qui abstrait les accès liés à la concurrence des données partagées dans les événements, place les événements dans la file d'attente de blocage, puis les traite dans un seul thread. .
Alors avant d'utiliser une serrure, il faut y réfléchir, est-ce nécessaire ? Peut-on simplifier ? Sinon, vous saurez à quel point il sera pénible de l'entretenir plus tard.
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!