résumé
Cette série est basée sur le cours du raffinage des nombres en or, et afin de mieux apprendre, une série de disques a été réalisée. Cet article présente principalement: 1. Idées et méthodes d'optimisation des verrous 2. Optimisation de verrouillage dans la machine virtuelle 3. Un cas de mauvaise utilisation des verrous 4. Threadlocal et son analyse de code source
1. Idées et méthodes d'optimisation des verrous
Le niveau de concurrence est mentionné dans l'introduction de [Haute concurrence Java 1].
Une fois qu'une serrure est utilisée, cela signifie que cela bloque, donc la concurrence est généralement un peu inférieure à la situation sans serrure.
L'optimisation des verrouillage mentionnée ici fait référence à la façon d'empêcher les performances de devenir trop pauvres en cas de blocage. Mais peu importe comment vous l'optimisez, les performances seront généralement un peu pires que la situation sans serrure.
Il convient de noter ici que le Trylock dans ReentrantLock mentionné dans [High Concurrency Java V] JDK concurrence le package 1 a tendance à être une méthode sans serrure car elle ne s'accrochera pas lorsque les juges Trylock.
Pour résumer les idées et les méthodes d'optimisation des verrous, il existe les types suivants.
1.1 Réduisez le temps de maintien de verrouillage
public synchronisé void syncMethod () {autreCode1 (); MuteXtMethod (); autresCode2 (); } Comme le code ci-dessus, vous devez obtenir le verrou avant d'entrer dans la méthode, et les autres threads doivent attendre à l'extérieur.
Le point d'optimisation ici consiste à réduire le temps d'attente des autres threads, il n'est donc utilisé que pour ajouter des verrous sur les programmes avec des exigences de sécurité du fil.
public void syncMethod () {autreCode1 (); synchronisé (this) {mutextMethod (); } autreCode2 (); }1.2 Réduisez la taille des particules de verrouillage
Diviser les grands objets (cet objet peut être accessible par de nombreux threads) en petits objets, augmentant considérablement le parallélisme et réduisant la concurrence de verrouillage. Ce n'est qu'en réduisant la concurrence pour les verrous et en biaisant vers les serrures, le taux de réussite des verrous légers sera amélioré.
Le cas le plus typique de la réduction de la granularité de verrouillage est simultané. Ceci est mentionné dans [Haute concurrence Java V] JDK Package de concurrence 1.
1.3 Séparation de verrouillage
La séparation de verrouillage la plus courante est la lecture de verrouillage en lecture, qui est séparée en verrous en lecture-écriture et écrivez des verrous en fonction de la fonction. De cette façon, la lecture et la lecture ne s'excluent pas mutuellement, la lecture et l'écriture s'excluent mutuellement, ce qui garantit la sécurité des fils et améliore les performances. Pour plus de détails, veuillez consulter [High Concurrence Java V] JDK Package de concurrence 1.
L'idée de séparation de la lecture et de l'écriture peut être prolongée, et tant que les opérations ne s'affichent pas, la serrure peut être séparée.
Par exemple, LinkedBlockingQueue
Sortez-le de la tête et mettez les données de la queue. Bien sûr, il est également similaire au vol de travail dans Forkjoinpool mentionné dans [Haute concurrence Java VI] JDK Package de concurrence 2.
1,4 verrouillage de verrouillage
D'une manière générale, afin d'assurer une concurrence efficace entre plusieurs threads, chaque thread sera nécessaire pour maintenir la serrure aussi courte que possible, c'est-à-dire que le verrou doit être libéré immédiatement après avoir utilisé des ressources publiques. Ce n'est que de cette manière que d'autres threads en attente de ce verrou peuvent obtenir des ressources pour exécuter les tâches dès que possible. Cependant, tout a un diplôme. Si le même verrou est constamment demandé, synchronisé et libéré, il consommera des ressources précieuses du système, ce qui n'est pas propice à l'optimisation des performances.
Par exemple:
public void DeMomeThod () {synchronisé (verrouillage) {// do sth. } // faire d'autres travaux de synchronisation indésirables, mais peuvent être exécutés rapidement synchronisés (verrouillage) {// do sth. }} Dans ce cas, selon l'idée de verrouiller le brouillage, il devrait être fusionné
public void DeMomeThod () {// Intégrer dans une demande de verrouillage synchronisée (verrouillage) {// do sth. // fait d'autres travaux de synchronisation indésirables, mais il peut être exécuté rapidement}} Bien sûr, il y a une condition préalable, le travail au milieu qui ne nécessite pas de synchronisation sera exécuté rapidement.
Permettez-moi de vous donner un autre exemple extrême:
pour (int i = 0; i <cercle; i ++) {synchronisé (lock) {}} Les verrous doivent être obtenus en boucle. Bien que JDK optimise ce code en interne, il est préférable de l'écrire directement
synchronisé (lock) {for (int i = 0; i <cercle; i ++) {}} Bien sûr, s'il est nécessaire de dire qu'une telle boucle est trop longue et que vous devez donner d'autres fils pour ne pas attendre trop longtemps, alors vous ne pouvez l'écrire que comme ce qui précède. S'il n'y a pas de tels exigences similaires, il est préférable de l'écrire directement dans la suivante.
1,5 Élimination des verrous
L'élimination des verrouillage est une chose au niveau du compilateur.
Dans le compilateur instantané, si les objets qui ne sont pas possibles à partager sont trouvés, le fonctionnement de verrouillage de ces objets peut être éliminé.
Peut-être que vous trouverez étrange que certains objets ne sont pas accessibles par plusieurs threads, pourquoi devrais-je ajouter des verrous? Ne serait-il pas préférable de ne pas ajouter de verrous lors de l'écriture de code.
Mais parfois, ces serrures ne sont pas écrites par des programmeurs. Certains d'entre eux ont des verrous dans des implémentations JDK, telles que des classes telles que Vector et StringBuffer. Beaucoup de leurs méthodes ont des serrures. Lorsque nous utilisons des méthodes de ces classes sans sécurité de thread, lorsque certaines conditions sont remplies, le compilateur supprimera le verrou pour améliorer les performances.
Par exemple:
public static void main (String args []) lève InterruptedException {long start = System.currenttimemillis (); pour (int i = 0; i <2000000; i ++) {CreatestringBuffer ("JVM", "Diagnostic"); } long buffercost = System.currenttimemillis () - start; System.out.println ("CraeTestringBuffer:" + BufferCost + "MS"); } public static String CreatestringBuffer (String S1, String S2) {StringBuffer SB = new StringBuffer (); SB.APPEND (S1); SB.APPEND (S2); return sb.toString (); } Le StringBuffer.APPEND dans le code ci-dessus est une opération synchrone, mais le StringBuffer est une variable locale, et la méthode ne renvoie pas le StringBuffer, il est donc impossible pour plusieurs threads d'y accéder.
Ensuite, l'opération de synchronisation dans StringBuffer n'a pas de sens pour le moment.
L'annulation de verrouillage est définie sur les paramètres JVM, bien sûr, il doit être en mode serveur:
-Server -xx: + DOSCAPEAnalysis -xx: + Eliminatelocks
Et activer l'analyse d'évasion. La fonction de l'analyse d'évasion est de voir si la variable est susceptible d'échapper à la portée.
Par exemple, dans le stringbuffer ci-dessus, le retour de CraeTestringBuffer dans le code ci-dessus est une chaîne, donc cette variable locale StringBuffer ne sera pas utilisée ailleurs. Si vous changez Craetestringbuffer
public static StringBuffer CraeTestringBuffer (String S1, String S2) {StringBuffer SB = new StringBuffer (); SB.APPEND (S1); SB.APPEND (S2); retourner sb; } Ensuite, une fois que STRINGBUFFER est renvoyé, il peut être utilisé ailleurs (par exemple, la fonction principale renverra le résultat et le mettra en carte, etc.). Ensuite, l'analyse JVM Escape peut être analysée que cette variable locale StringBuffer échappe à sa portée.
Par conséquent, sur la base de l'analyse d'évasion, le JVM peut juger que si la variable locale StringBuffer n'échappe pas à sa portée, il peut être déterminé que le StringBuffer ne sera pas accessible par plusieurs threads, puis ces verrous supplémentaires peuvent être supprimés pour améliorer les performances.
Lorsque les paramètres JVM sont:
-Server -xx: + DOSCAPEAnalysis -xx: + Eliminatelocks
Sortir:
Craetestringbuffer: 302 ms
Les paramètres JVM sont:
-Server -xx: + DOSCAPEAnalysis -xx: -eliminatelocks
Sortir:
Craetestringbuffer: 660 ms
De toute évidence, l'effet d'élimination des serrures est encore très évident.
2. Optimisation de verrouillage dans la machine virtuelle
Tout d'abord, nous devons introduire l'en-tête de l'objet. Dans le JVM, chaque objet a un en-tête d'objet.
Mark Word, marqueur pour l'en-tête d'objet, 32 bits (système 32 bits).
Décrivez le hachage, les informations de verrouillage, les étiquettes de collecte des ordures, l'âge
Il enregistrera également un pointeur sur l'enregistrement de verrouillage, un pointeur vers le moniteur, un identifiant de filetage de verrouillage biaisé, etc.
En termes simples, l'en-tête d'objet consiste à enregistrer certaines informations systématiques.
2.1 verrouillage positif
Le soi-disant biais est l'excentricité, c'est-à-dire que la serrure tend vers le fil qui possède actuellement le verrou.
Dans la plupart des cas, il n'y a pas de concurrence (dans la plupart des cas, un bloc de synchronisation n'a pas plusieurs threads en même temps le verrouillage de compétition), donc les performances peuvent être améliorées par la polarisation. Autrement dit, lorsqu'il n'y a pas de concurrence, lorsque le fil qui a précédemment obtenu le verrou obtient à nouveau le verrou, il déterminera si le verrouillage me pointe vers moi, donc le fil n'aura plus besoin d'obtenir le verrou et peut entrer directement le bloc de synchronisation.
L'implémentation du verrouillage de biais consiste à définir la marque de la marque d'en-tête d'objet comme biaisé et à écrire l'ID de thread sur la marque d'en-tête d'objet.
Lorsque d'autres threads demandent le même serrure, le mode biais se termine
JVM permet le verrouillage de biais par défaut -xx: + usebiasEdLocking
Dans une concurrence féroce, le verrouillage biaisé augmentera la charge du système (le jugement de savoir s'il est biaisé est ajouté à chaque fois)
Exemple de verrouillage biaisé:
Test de package; import java.util.list; import java.util.vector; public class test {public static list <Integer> numberList = new Vector <Integer> (); public static void main (String [] args) lève InterruptedException {long begin = system.currenttimemillis (); int count = 0; int startNum = 0; while (count <10000000) {nombreList.add (startNum); startnum + = 2; Count ++; } long fin = System.currentTimemillis (); System.out.println (end - begin); }} Vector est une classe en filetage qui utilise le mécanisme de verrouillage en interne. Chaque fois qu'ajoutez, une demande de verrouillage sera faite. Le code ci-dessus n'a qu'un seul thread principal, puis ajoute à plusieurs reprises la demande de verrouillage.
Utilisez les paramètres JVM suivants pour définir le verrouillage de biais:
-Xx: + usebiasedLocking -xx: biasedLockingStartupDelay = 0
BiasEdLockingStartupDelay signifie que le verrouillage de biais est activé après le début du système pendant quelques secondes. La valeur par défaut est de 4 secondes, car lorsque le système démarre, la compétition générale de données est relativement féroce. Les verrous de biais activés à ce moment réduiront les performances.
Puisque ici, afin de tester les performances du verrouillage de biais, le temps de verrouillage du biais de retard est défini sur 0.
Pour le moment, la sortie est 9209
Éteignez le verrou de biais ci-dessous:
-Xx: -UsebiasEd blocking
La sortie est 9627
Généralement, lorsqu'il n'y a pas de concurrence, les performances des verrous de biais d'activation seront améliorées d'environ 5%.
2.2 verrouillage léger
La sécurité multi-thread de Java est mise en œuvre en fonction du mécanisme de verrouillage, et les performances du verrouillage ne sont souvent pas satisfaisantes.
La raison en est que le surveillant et le monitorexit, deux primitives bytecode qui contrôlent la synchronisation multithread, sont implémentées par JVM reposent sur mutex pour le système d'exploitation.
Mutex est une opération relativement consommatrice de ressources qui fait accrocher le fil et doit être reportée au fil d'origine dans un court laps de temps.
Afin d'optimiser le mécanisme de verrouillage de Java, le concept de verrouillage léger a été introduit depuis Java6.
Le verrouillage léger est destiné à réduire les risques de lancement multi-threading Mutex, et non à remplacer Mutex.
Il utilise le CPU Primitive Compare-and-Swap (CAS, instruction d'assemblage CMPXCHG) et essaie de remédier avant d'entrer dans le mutex.
Si le verrouillage de polarisation échoue, le système effectuera une opération de verrouillage légère. Le but de son existence est d'éviter d'utiliser autant que possible le Mutex au niveau du système d'exploitation, car ces performances seront relativement médiocres. Parce que JVM lui-même est une application, j'espère résoudre le problème de synchronisation du thread au niveau de l'application.
Pour résumer, le verrouillage léger est une méthode de verrouillage rapide. Avant d'entrer Mutex, utilisez les opérations CAS pour essayer d'ajouter des verrous. Essayez de ne pas utiliser Mutex au niveau du système d'exploitation pour améliorer les performances.
Ensuite, lorsque le verrouillage du biais échoue, les étapes du verrouillage léger:
1. Enregistrez le pointeur de marque de l'en-tête de l'objet dans l'objet verrouillé (l'objet ici fait référence à l'objet verrouillé, comme synchronisé (this) {}, c'est l'objet ici).
Lock-> set_displaced_header (Mark);
2. Définissez l'en-tête de l'objet en tant que pointeur sur le verrou (dans l'espace de pile de thread).
if (mark == (markoop) atomic :: cmpxchg_ptr (lock, obj () -> mark_addr (), mark)) {tevent (slow_enter: release stacklock); retour ; } Le verrou est situé dans la pile de threads. Par conséquent, pour déterminer si un fil conserve ce verrou, déterminez simplement si l'espace pointé par l'en-tête de l'objet se trouve dans l'espace d'adressage de la pile de threads.
Si le verrouillage léger échoue, cela signifie qu'il y a une concurrence et une mise à niveau vers un verrouillage poids lourd (verrouillage ordinaire), qui est la méthode de synchronisation au niveau du système d'exploitation. En l'absence de compétition de verrouillage, les verrous légers réduisent les pertes de performances causées par les verrous traditionnels à l'aide de mutex du système d'exploitation. Lorsque la concurrence est très féroce (les verrous légers échouent toujours), les verrous légers effectuent beaucoup d'opérations supplémentaires, ce qui entraîne une dégradation des performances.
2.3 verrouillage de spin
Lorsque la concurrence existe, parce que la tentative de verrouillage légère échoue, elle peut être directement mise à niveau vers un verrou de poids lourd pour utiliser l'exclusion mutuelle au niveau du système d'exploitation. Il est également possible d'essayer à nouveau le verrou de spin.
Si le fil peut obtenir le verrouillage rapidement, vous ne pouvez pas accrocher le fil à la couche du système d'exploitation, laissez le fil faire plusieurs opérations vides (spin) et essayer constamment d'obtenir le verrou (similaire à Trylock). Bien sûr, le nombre de boucles est limité. Lorsque le nombre de boucles arrivera, il sera toujours mis à niveau vers une serrure poids lourd. Par conséquent, lorsque chaque filetage a peu de temps pour maintenir le verrou, le verrouillage de spin peut essayer d'éviter que les fils soient suspendus à la couche OS.
Jdk1.6 -xx: + usespinning est activé
Dans JDK1.7, supprimez ce paramètre et changez-le en une implémentation intégrée.
Si le bloc de synchronisation est très long et que le spin échoue, les performances du système seront dégradées. Si le bloc de synchronisation est très court et que le spin est réussi, il économise le temps de commutation de suspension du thread et améliore les performances du système.
2.4 verrouillage positif, verrouillage léger, résumé de verrouillage de spin
Le verrouillage ci-dessus n'est pas une méthode d'optimisation de verrouillage au niveau du langage Java, mais est intégré à la JVM.
Tout d'abord, les verrous de polarisation consiste à éviter la consommation de performances d'un fil lorsqu'il acquiert / libère à plusieurs reprises le même verrou. Si le même thread acquiert toujours ce verrou, il entrera directement le bloc de synchronisation lorsque vous essayez de biaiser les verrous, et il n'est pas nécessaire d'obtenir à nouveau le verrou.
Les verrous légers et les verrous de spin sont tous deux destinés à éviter les appels directs vers les opérations Mutex au niveau du système d'exploitation, car la suspension des threads est une opération très consommatrice de ressources.
Afin d'éviter d'utiliser des serrures poids lourds (mutex au niveau du système d'exploitation), nous allons d'abord essayer une serrure légère. Le verrouillage léger essaiera d'utiliser l'opération CAS pour obtenir le verrou. Si le verrouillage léger ne parvient pas, cela signifie qu'il y a de la concurrence. Mais peut-être que vous obtiendrez bientôt la serrure et vous essairez des verrous à rotation, ferez quelques boucles vides sur le fil et essayerez d'obtenir les serrures à chaque boucle. Si le verrouillage de spin échoue également, il ne peut être mis à niveau qu'à une serrure poids lourd.
On peut voir que les serrures biaisées, les serrures légères et les verrous de spin sont tous des serrures optimistes.
3. Un cas d'utilisation des verrous incorrectement
classe publique IntegerLock {entier statique i = 0; La classe statique publique addThread étend Thread {public void run () {for (int k = 0; k <100000; k ++) {synchronisé (i) {i ++; }}}} public static void main (String [] args) lève InterruptedException {addThread t1 = new addThread (); AddThread t2 = new addThread (); t1.start (); t2.start (); t1.join (); t2.join (); System.out.println (i); }} Une erreur très fondamentale est que dans le modèle de conception de concurrence [Haute concurrence Java VII], l'interger est final inchangé, et après chaque ++, un nouvel interger sera généré et affecté à I, de sorte que les serrures concourées entre les deux fils sont différentes. Ce n'est donc pas un thread-safe.
4. ThreadLocal et son analyse de code source
Il peut être un peu inapproprié de mentionner ThreadLocal ici, mais ThreadLocal est un moyen de remplacer les verrous. Il est donc toujours nécessaire de le mentionner.
L'idée de base est que dans un fil multi-thread, les données conflictuelles doivent être verrouillées. Si ThreadLocal est utilisé, une instance d'objet est fournie pour chaque thread. Différents threads accédent uniquement à leurs propres objets, pas d'autres objets. De cette façon, il n'est pas nécessaire que le verrou existe.
Test de package; import java.text.parseException; import java.text.simpledateformat; import java.util.date; import java.util.concurrent.executorservice; import java.util.concurrent.execcutors; test public {yyyyyyyyyyyyyyy HH: MM: SS "); classe statique publique Parsedate implémente Runnable {int i = 0; Public Parsedate (int i) {this.i = i; } public void run () {try {date t = sdf.parse ("2016-02-16 17:00:" + i% 60); System.out.println (i + ":" + t); } catch (parseException e) {e.printStackTrace (); }}} public static void main (String [] args) {EMMIRCORORSERVICE ES = EMECTROVEURS.NEWFIXEDTHREADPOOL (10); pour (int i = 0; i <1000; i ++) {es.execute (nouveau parsedate (i)); }}} Étant donné que SimpledateFormat n'est pas un filetage, le code ci-dessus est mal utilisé. Le moyen le plus simple est de définir vous-même une classe et de l'envelopper avec synchronisé (similaire à Collections.SynchronizedMap). Cela causera des problèmes lorsque vous le faites à une grande concurrence. L'affirmation sur la synchronisation ne se traduit par un seul fil qui entre à la fois, et le volume de concurrence est très faible.
Ce problème est résolu en utilisant du threadlocal pour encapsuler SimpledateFormat.
Test de package; Importer java.text.parseException; import java.text.simpledateformat; import java.util.date; import java.util.concurrent.executorservice; import java.util.concurrent.execcutors; test public class {static threadLocal <SimpledateForat> tl = new ThreadLocal <simpledate> (); classe statique publique Parsedate implémente Runnable {int i = 0; Public Parsedate (int i) {this.i = i; } public void run () {try {if (tl.get () == null) {tl.set (new SimpledateFormat ("yyyy-mm-dd hh: mm: ss")); } Date t = tl.get (). Parse ("2016-02-16 17:00:" + i% 60); System.out.println (i + ":" + t); } catch (parseException e) {e.printStackTrace (); }}} public static void main (String [] args) {EMMIRCORORSERVICE ES = EMECTROVEURS.NEWFIXEDTHREADPOOL (10); pour (int i = 0; i <1000; i ++) {es.execute (nouveau parsedate (i)); }}}Lorsque chaque thread est en cours d'exécution, il déterminera si le thread actuel a un objet SimpleDateFormat.
if (tl.get () == null)
Sinon, un nouveau simpletateformat sera lié au thread actuel
tl.set (new SimpledateFormat ("yyyy-mm-dd hh: mm: ss"));
Ensuite, utilisez le simple thread actuel pour analyser
tl.get (). Parse ("2016-02-16 17:00:" + i% 60);
Dans le code initial, il n'y avait qu'un seul simpletateFormat, qui utilisait ThreadLocal, et un SimpledateFormat était nouveau pour chaque thread.
Il convient de noter que vous ne devez pas définir un public SimpledateFormat à chaque threadlocal ici, car cela est inutile. Chacun doit être donné nouveau à un simpleformat.
Dans Hibernate, il existe des applications typiques pour le threadlocal.
Jetons un coup d'œil à l'implémentation du code source de ThreadLocal
Tout d'abord, il y a une variable de membre dans la classe de threads:
ThreadLocal.ThreadLocalmap ThreadLocals = null;
Et cette carte est la clé de l'implémentation de threadlocal
public void set (t valeur) {thread t = thread.currentThread (); ThreadLocalmap map = getmap (t); if (map! = null) map.set (this, valeur); else CreateMap (t, valeur); } Selon ThreadLocal, vous pouvez définir et obtenir la valeur correspondante.
L'implémentation ThreadLocalmap ici est similaire à HashMap, mais il existe des différences dans la gestion des conflits de hachage.
Lorsqu'un conflit de hachage se produit dans ThreadLocalmap, ce n'est pas comme HashMap d'utiliser les listes liées pour résoudre le conflit, mais pour mettre l'index ++ au prochain index pour résoudre le conflit.