À partir d’aujourd’hui, j’étudierai en profondeur avec vous le principe de la synchronisation. La partie principe comprendra deux articles :
- Le processus de mise à niveau des verrous biaisés vers des verrous légers
- Le processus de mise à niveau d’une serrure légère vers une serrure lourde
Aujourd'hui, nous allons d'abord apprendre le processus de mise à niveau des verrous biaisés vers des verrous légers . Étant donné qu'un grand nombre de codes sources HotSpot sont impliqués, il y aura un article séparé sur la version annotée du code source.
A travers cet article, que répondez-vous sur les questions synchronisées ? Les problèmes suivants sont comptabilisés dans les statistiques :
- Décrire en détail le principe de mise en œuvre de la synchronisation
- Pourquoi la synchronisation est-elle un verrou réentrant ?
- Décrire en détail le processus de mise à niveau (extension) du verrou synchronisé
- Qu'est-ce qu'un verrou biaisé ? Comment la synchronisation implémente-t-elle des verrous biaisés ?
- Après Java 8, quelles optimisations synchronisées ont-elles apportées ?
Préparation
Avant de commencer officiellement à analyser le code source synchronisé, faisons quelques préparatifs :
- Préparation du code source HotSpot : Ouvrez JDK 11 ;
- Outil de bytecode, le plug-in jclasslib est recommandé ;
- Le package jol-core pour suivre l’état des objets .
Conseils:
- Vous pouvez utiliser la commande javap et l'outil de bytecode fourni avec IDEA ;
- L'avantage de jclasslib est qu'il peut accéder directement au site officiel des commandes associées.
exemple de code
Préparez un exemple de code simple :
public class SynchronizedPrinciple {
private int count = 0;
private void add() {
synchronized (this) {
count++;
}
}
}
Grâce à l'outil, nous pouvons obtenir le bytecode suivant :
aload_0
dup
astore_1
monitorenter // 1
aload_0
dup
getfield #2 <com/wyz/keyword/synckeyword/SynchronizedPrinciple.count : I>
iconst_1
iadd
putfield #2 <com/wyz/keyword/synckeyword/SynchronizedPrinciple.count : I>
aload_1
monitorexit // 2
goto 24 (+8)
astore_2
aload_1
monitorexit // 3
aload_2
athrow
return
Le bloc de code modifié synchronisé est compilé en deux instructions :
- monitorenter : entrez le moniteur de l'objet ;
- monitorexit : Quitte le moniteur de l'objet.
Nous avons remarqué que monitorexit apparaissait deux fois. La partie de la note 2 est l'exécution normale du programme, et la partie de la note 3 est l'exécution anormale du programme. L'équipe Java a même considéré pour vous la situation anormale du programme, il a vraiment, j'ai pleuré à mort.
Conseils:
- La raison pour laquelle le bloc de code modifié synchronisé est utilisé comme exemple est qu'il n'est pas intuitif de définir uniquement l'indicateur ACC_SYNCHRONIZED dans access_flag lors de la modification de la méthode ;
- Java ne quitte pas seulement le moniteur via monitorexit, Java a déjà fourni une méthode d'entrée et de sortie du moniteur dans la classe Unsafe.
Unsafe.getUnsafe.monitorEnter(obj);
Unsafe.getUnsafe.monitorExit(obj);
Java 8 peut être utilisé et Java 11 a été supprimé. Je ne connais pas la version spécifique supprimée.
Exemple d'utilisation de jol
L'état de l'objet peut être suivi par jol-core . Maven dépend de :
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.16</version>
</dependency>
Exemple d'utilisation :
public static void main(String[] args) {
Object obj = new Object();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
démarrer à partir du moniteur
Dans HotSpot, la commande monitorenter correspond à ces deux types de méthodes d'analyse :
- Interpréteur de bytecode : bytecodeInterpreter
- Interpréteur de modèle : templateTable_x86#monitorenter
Puisque bytecodeInterpreter s'est fondamentalement retiré de la scène de l'histoire, nous prenons l'interpréteur de modèles X86 pour implémenter templateTable_x86 comme exemple.
Conseils:
- Par convention, le code source n'affiche que le contenu clé ;
- Je recommande "Analyse approfondie du HotSpot de la machine virtuelle Java" du professeur Yang Yi .
La méthode d'exécution de monitorenter est templateTable_x86#monitorenter . Dans cette méthode, il suffit de prêter attention au __lock_object(rmon) exécuté à la ligne 4438 et d'appeler la méthode interp masm_x86#lock_object :
void InterpreterMacroAssembler::lock_object(Register lock_reg) {
if (UseHeavyMonitors) {// 1
// 重量级锁逻辑
call_VM(noreg, CAST_FROM_FN_PTR(address, InterpreterRuntime::monitorenter), lock_reg);
} else {
Label done;
Label slow_case;
if (UseBiasedLocking) {// 2
// 偏向锁逻辑
biased_locking_enter(lock_reg, obj_reg, swap_reg, tmp_reg, false, done, &slow_case);
}
// 3
bind(slow_case);
call_VM(noreg, CAST_FROM_FN_PTR(address, InterpreterRuntime::monitorenter), lock_reg);
bind(done);
......
}
Les parties de Note 1 et Note 2 sont deux paramètres JVM :
// 启用重量级锁
-XX:+UseHeavyMonitors
// 启用偏向锁
-XX:+UseBiasedLocking
Note 1 et Note 3, InterpreterRuntime::monitorenter
méthode d'appel, Note 1 est une configuration qui utilise directement des verrous lourds, vous pouvez donc deviner que Note 3 est la logique de mise à niveau du verrou vers un verrou lourd après avoir échoué à obtenir un verrou biaisé.
En-tête d'objet (markOop)
Avant de commencer officiellement, comprenons l'en-tête de l'objet ( markOop ). En fait, les commentaires de markOop ont révélé son « secret » :
Le markOop décrit l'en-tête d'un objet.
…..
Format binaire d'un en-tête d'objet (le plus significatif en premier, disposition big endian ci-dessous) :
64 bits :
inutilisé:25 hachage:31 -->| inutilisé : 1 âge : 4 biaisé_lock : 1 verrou : 2 (objet normal)
JavaThread : 54 époque : 2 inutilisé : 1 âge : 4 biaisé lock : 1 verrou : 2 (objet biaisé)
……
[JavaThread_ | époque | âge | 1 | 01] le verrou est orienté vers un thread donné
[0 | époque | âge | 1 | 01] le verrouillage est un biais anonyme
Le commentaire décrit en détail la structure de l'en-tête de l'objet Java en mode big-endian 64 bits :
Conseils:
- J'ai également décrit la structure de markOop 32 bits, je ne l'ai pas tenu ~~
- Énumération des indicateurs de verrouillage markOop
La majeure partie de la structure de l’en-tête de l’objet est facile à comprendre, mais qu’est-ce qu’une époque ?
Les commentaires décrivent l'époque comme "utilisée pour soutenir le verrouillage biaisé". La synchronisation dans le wiki OpenJDK décrit l'époque comme ceci :
Une valeur d'époque dans la classe agit comme un horodatage qui indique la validité du biais.
Epoch est similaire à un horodatage, indiquant la validité du verrou biaisé . Il est mis à jour pendant la phase de rebias groupé ( biaisedLocking#bulk_revoke_or_rebias_at_safepoint ) :
static BiasedLocking::Condition bulk_revoke_or_rebias_at_safepoint(oop o, bool bulk_rebias, bool attempt_rebias_of_object, JavaThread* requesting_thread) {
{
if (bulk_rebias) {
if (klass->prototype_header()->has_bias_pattern()) {
klass->set_prototype_header(klass->prototype_header()->incr_bias_epoch());
}
}
}
}
La JVM utilise l'époque pour déterminer si elle est adaptée à un verrou biaisé. Une fois le seuil dépassé, la JVM met à niveau le verrou biaisé. JVM fournit des paramètres pour ajuster ce seuil.
// 批量重偏向阈值
-XX:BiasedLockingBulkRebiasThreshold=20
// 批量撤销阈值
-XX:BiasedLockingBulkRevokeThreshold=40
Astuces : La mise à jour est l'époque de la classe.
Verrouillage biaisé (biasedLocking)
Lorsque le système aura ouvert le verrou biaisé, il entrera dans la méthode macroAssembler_x86#biased_locking_enter . La méthode récupère d'abord le markOop de l'objet :
Address mark_addr (obj_reg, oopDesc::mark_offset_in_bytes());
Address saved_mark_addr(lock_reg, 0);
Je vais diviser le processus suivant en 5 branches, et analyser avec vous la logique de mise en œuvre des verrous biaisés selon l'ordre d'exécution.
Conseils:
- Il suffit de comprendre le processus de verrouillage de biais, donc le diagramme est le principal et l'analyse du code source est placée dans l'analyse du code source de verrouillage de biais ;
- L'analyse du code source du verrou de biais est principalement basée sur des commentaires, et chaque branche est marquée en détail ;
- Cette partie comprend en fait deux étiquettes de saut pour undo et rebias , qui sont expliquées dans le diagramme de branche ;
- Le code source utilise la technologie bitmask. Afin de faciliter la distinction, le nombre binaire commence par 0B et est rempli de 4 chiffres.
Branche 1 : Est-ce biaisé ?
La logique de la condition préalable du verrouillage biaisé est très simple, jugez l'indicateur de verrouillage de l'objet actuel markOop , s'il a été mis à niveau, exécutez le processus de mise à niveau ; sinon continuez à exécuter vers le bas.
Astuces : La logique de la ligne pointillée se retrouve dans d'autres classes.
Branche 2 : Y a-t-il un biais réentrant ?
À l'heure actuelle, la JVM sait que l'indicateur de verrouillage de markOop est 0B0101, qui est dans un état biaisé, mais il n'est pas clair s'il a été biaisé ou pas encore. HotSopt utilise anonymement pour décrire l'état qui peut être biaisé mais pas biaisé en faveur d'un certain thread, et cet état est appelé biais anonyme . À ce stade, l'en-tête de l'objet est le suivant :
La chose à faire à ce stade est relativement simple, pour déterminer s'il faut réintroduire le verrouillage de biais pour le thread actuel . S'il s'agit d'une rentrée, sortez directement ; sinon, continuez à exécuter vers le bas.
Astuces : Je suis tombé sur un article aujourd'hui. Javaer et C++ ont débattu des verrous réentrants et des verrous récursifs. Si cela vous intéresse, vous pouvez lire un article pour comprendre les verrous dans la programmation concurrente. J'ai brièvement expliqué la relation entre les verrous réentrants et les verrous récursifs. .
Branche 3 : Est-ce encore biaisé ?
Les commentaires décrivent le cas de verrous biaisés non réentrants :
À ce stade, nous savons que l’en-tête présente le modèle de biais et que nous ne sommes pas propriétaires du biais à l’époque actuelle. Nous devons trouver plus de détails sur l'état de l'en-tête afin de savoir quelles opérations peuvent être légalement effectuées sur l'en-tête de l'objet.
Il peut y avoir deux situations à ce stade :
- Il n'y a pas de concurrence et un fil de discussion est réorienté ;
- Il y a une course, essayez de la défaire.
La partie révocation partielle du verrouillage est un peu plus compliquée. Utilisez le markOop de la classe d'objet pour remplacer le markOop de l'objet. La technologie clé est CAS .
Branche 4 : L'époque est-elle expirée ?
L'état actuel du verrou biaisé est biaisable et biaisé vers d'autres threads . À ce stade, la logique nécessite uniquement de savoir si l'époque du fragment est valide.
Le re-biais peut être décrit en une phrase, construisez markOop pour remplacer CAS .
Branche 5 : Re-biaisé
L'état actuel du verrou biaisé est qu'il peut être biaisé, biaisé vers d'autres threads et que l'époque n'a pas expiré . Ce qu'il faut faire à ce stade est de définir le thread actuel dans markOop, qui est le processus de repolarisation du verrou de biais, qui est très similaire à la partie de la branche 4.
Annuler et Rebias
Après avoir échoué à obtenir le verrou biaisé, exécutez la méthode InterpreterRuntime::monitorenter, qui se trouve dans le fichier performerRuntime :
IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
if (UseBiasedLocking) {
// 完整的锁升级路径
// 偏向锁->轻量级锁->重量级锁
ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
} else {
// 跳过偏向锁的锁升级路径
// 轻量级锁->重量级锁
ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
}
IRT_END
ObjectSynchronizer::fast_enter位于synchronizer.cpp#fast_enter:
void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock, bool attempt_rebias, TRAPS) {
if (UseBiasedLocking) {
if (!SafepointSynchronize::is_at_safepoint()) {
// 撤销和重偏向
BiasedLocking::Condition cond = BiasedLocking::revoke_and_rebias(obj, attempt_rebias, THREAD);
if (cond == BiasedLocking::BIAS_REVOKED_AND_REBIASED) {
return;
}
} else {
BiasedLocking::revoke_at_safepoint(obj);
}
}
// 跳过偏向锁
slow_enter(obj, lock, THREAD);
}
BiasedLocking::revoke_and_rebias
La version commentée condensée de est placée dans la partie 2 de l'analyse du code source du verrouillage de biais .
Serrure légère (basicLock)
Si l'acquisition du verrou biaisé échoue, elle sera exécutée à ce moment-là.La ObjectSynchronizer::slow_enter
méthode se trouve dans synchroniser#slow_enter :
void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {
markOop mark = obj->mark();
// 无锁状态 ,获取偏向锁失败后有撤销逻辑,此时变为无锁状态
if (mark->is_neutral()) {
// 将对象的markOop复制到displaced_header(Displaced Mark Word)上
lock->set_displaced_header(mark);
// CAS将对象markOop中替换为指向锁记录的指针
if (mark == obj()->cas_set_mark((markOop) lock, mark)) {
// 替换成功,则获取轻量级锁
TEVENT(slow_enter: release stacklock);
return;
}
} else if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) {
// 重入情况
lock->set_displaced_header(NULL);
return;
}
// 重置displaced_header(Displaced Mark Word)
lock->set_displaced_header(markOopDesc::unused_mark());
// 锁膨胀
ObjectSynchronizer::inflate(THREAD, obj(), inflate_cause_monitor_enter)->enter(THREAD);
}
Citez directement le processus de verrouillage léger dans « The Art of Java Concurrent Programming » :
Avant que le thread n'exécute le bloc de synchronisation, la JVM créera d'abord un espace pour stocker l'enregistrement de verrouillage dans le cadre de pile du thread actuel et copiera le mot de marque dans l'en-tête de l'objet dans l'enregistrement de verrouillage, qui est officiellement appelé Mot de marque déplacé. . Ensuite, le thread essaie d'utiliser CAS pour remplacer le mot Mark dans l'en-tête de l'objet par un pointeur vers l'enregistrement de verrouillage. S'il réussit, le thread actuel acquiert le verrou. S'il échoue, cela signifie que d'autres threads se disputent le verrou et que le thread actuel essaie d'utiliser spin pour acquérir le verrou.
La logique des serrures légères est très simple et la technologie clé utilisée est également CAS . À l’heure actuelle, la structure de markOop est la suivante :
se termine à la sortie du moniteur
La logique de Monitorexit est très simple lorsqu'il s'agit d'un verrou biaisé ou d'un verrou léger. Avec l'expérience de monitorenter, nous pouvons facilement analyser la logique d'appel de monitorexit :
- templateTable_x86#monitorexit
- interp_masm_x86#un_lock
- logique de sortie de verrouillage
- Verrou biaisé : macroAssembler_x86#biased_locking_exit
- Verrouillage léger : interprèteRuntime#monitorexit
- ObjectSynchronizer#slow_exit
- ObjectSynchronizer#fast_exit
Le code est laissé à chacun d'explorer par lui-même, et voici ma compréhension.
Habituellement, je pense simplement que lorsque le verrou biaisé sort, rien ne doit être fait (c'est-à-dire que le verrou biaisé ne sera pas activement libéré) ; pour les verrous légers, au moins deux étapes sont nécessaires :
- réinitialiser déplacé_header ;
- Libérez l'enregistrement de verrouillage .
Par conséquent, en termes de logique de sortie, les performances des verrous légers sont légèrement inférieures à celles des verrous biaisés.
Résumer
Faisons un bref résumé du contenu de cette étape. La logique des verrous biaisés et des verrous légers n'est pas compliquée, en particulier les verrous légers.
La technologie clé des verrous biaisés et des verrous légers est CAS . Lorsque la compétition CAS échoue, cela signifie que d'autres threads tentent de l'arracher, ce qui conduit à des mises à niveau de verrouillage.
Le verrou biaisé enregistre le thread qui le détient pour la première fois dans l'objet markOop. Lorsque le thread continue de détenir le verrou biaisé, il n'a besoin que d'une simple comparaison. Il convient à la plupart des scénarios et à l'exécution monothread, mais parfois là peut être des scénarios de concurrence de threads.
Mais le problème est que si les threads sont alternativement maintenus et exécutés, la logique de révocation et de rebiaisation des verrous biaisés est complexe et les performances sont médiocres. Par conséquent, des verrous légers sont introduits pour garantir la sécurité dans de telles conditions de course « légères ».
Par ailleurs, il existe de nombreuses controverses autour des verrous biaisés, principalement sur deux points :
- La révocation des verrous biaisés a un impact plus important sur les performances ;
- Lorsqu’il y a une grande concurrence, les verrous biaisés sont de très mauvais goût.
En fait, les verrous biaisés ont été abandonnés dans Java 15 ( JEP 374 : Deprecate and Disable Biased Locking ), mais comme la plupart des applications fonctionnent toujours sur Java 8, nous devons encore comprendre la logique des verrous biaisés.
Enfin, réfutons la rumeur (sous peine de se faire gifler ?), il n'y a pas de logique de spin dans les locks légères .
Astuces : Il semble que l'annulation et la redirection du lot aient été manquées ~~
Si cet article vous est utile, merci de lui accorder beaucoup d’éloges et de soutien. S'il y a des erreurs dans l'article, veuillez critiquer et corriger. Enfin, tout le monde est invité à prêter attention à Wang Youzhi, un financier . À la prochaine fois !