L'objectif de cet article est sur les problèmes de performances des applications multithread. Nous allons d'abord définir les performances et l'évolutivité, puis étudierons soigneusement la règle d'Amdahl. Dans le contenu suivant, nous examinerons comment utiliser différentes méthodes techniques pour réduire la concurrence de verrouillage et comment la mettre en œuvre avec le code.
1. Performance
Nous savons tous que le multithreading peut être utilisé pour améliorer les performances du programme, et la raison derrière cela est que nous avons des processeurs multi-fond ou des processeurs multiples. Chaque noyau de processeur peut effectuer des tâches en soi, de sorte que la rupture d'une grande tâche en une série de petites tâches qui peuvent être exécutées indépendamment les unes des autres peuvent améliorer les performances globales du programme. Vous pouvez donner un exemple. Par exemple, il existe un programme qui modifie la taille de toutes les images dans un dossier sur le disque dur, et l'application de la technologie multi-threading peut améliorer ses performances. L'utilisation d'une seule approche threadée ne peut traverser que tous les fichiers d'image en séquence et effectuer des modifications. Si notre CPU a plusieurs cœurs, il ne fait aucun doute qu'il ne peut en utiliser qu'un. En utilisant le multi-threading, nous pouvons faire numériser un thread de producteur le système de fichiers pour ajouter chaque image à une file d'attente, puis utiliser plusieurs threads de travail pour effectuer ces tâches. Si le nombre de threads de travailleur est le même que le nombre total de cœurs CPU, nous pouvons nous assurer que chaque noyau de processeur a du travail à effectuer jusqu'à ce que toutes les tâches soient exécutées.
Pour un autre programme qui nécessite plus d'IO attend, les performances globales peuvent également être améliorées à l'aide de la technologie multi-threading. Supposons que nous voulons écrire un tel programme dont nous avons besoin pour ramper tous les fichiers HTML d'un certain site Web et les stocker sur le disque local. Le programme peut démarrer à partir d'une certaine page Web, puis analyser tous les liens vers ce site Web de cette page Web, puis ramper ces liens à tour de rôle, afin qu'il se répète. Parce qu'il faut un certain temps pour attendre à partir du moment où nous entamons une demande au site Web distant au moment où nous recevons toutes les données de la page Web, nous pouvons remettre cette tâche à plusieurs threads d'exécution. Laissez un ou un peu plus de thread analyser la page HTML reçue et mettre le lien trouvé dans la file d'attente, laissant tous les autres threads chargés de demander la page. Contrairement à l'exemple précédent, dans cet exemple, vous pouvez toujours obtenir des améliorations de performances même si vous utilisez plus de threads que le nombre de cœurs CPU.
Les deux exemples ci-dessus nous indiquent que les hautes performances consistent à faire autant de choses que possible dans une fenêtre de courte durée. C'est bien sûr l'explication la plus classique du terme performance. Mais en même temps, l'utilisation de threads peut également bien améliorer la vitesse de réponse de nos programmes. Imaginez que nous ayons une telle application d'interface graphique, avec une zone d'entrée ci-dessus et un bouton nommé "Processus" sous la zone d'entrée. Lorsque l'utilisateur appuie sur ce bouton, l'application doit renvoyer l'état du bouton (le bouton semble appuyé sur et il revient à son état d'origine lorsque le bouton de la souris gauche est libéré) et commencez à traiter l'entrée de l'utilisateur. Si cette tâche prend du temps pour traiter la saisie des utilisateurs, un programme unique ne pourra pas continuer à répondre aux autres actions d'entrée des utilisateurs, tels que l'utilisateur cliquant sur l'événement de souris ou le pointeur de souris en déplaçant l'événement transmis à partir du système d'exploitation, etc. Les réponses à ces événements doivent être un thread indépendant pour répondre.
L'évolutivité signifie que les programmes ont la capacité d'obtenir des performances plus élevées en ajoutant des ressources informatiques. Imaginez que nous devons ajuster la taille de nombreuses images, car le nombre de noyaux CPU de notre machine est limité, l'augmentation du nombre de threads n'améliore pas toujours les performances en conséquence. Au contraire, car le planificateur doit être responsable de la création et de la fermeture de plus de threads, il occupera également les ressources CPU, ce qui pourrait réduire les performances.
1.1 Règle Amdahl
Le paragraphe précédent a mentionné que, dans certains cas, l'ajout de ressources informatiques supplémentaires peut améliorer les performances globales du programme. Afin de calculer la quantité d'amélioration des performances que nous pouvons obtenir lorsque nous ajoutons des ressources supplémentaires, il est nécessaire de vérifier les parties du programme s'exécutent en série (ou de manière synchrone) et quelles pièces s'exécutent en parallèle. Si nous quantifions la proportion de code qui doit être exécutée de manière synchrone avec B (par exemple, le nombre de lignes de code qui doivent être exécutés de manière synchrone) et enregistrer le nombre total de cœurs du CPU comme n, puis selon la loi AMDAHL, la limite supérieure des améliorations des performances que nous pouvons obtenir est:
Si n tend à l'infini, (1-b) / n converge vers 0. Par conséquent, nous pouvons ignorer la valeur de cette expression, de sorte que le nombre de bits d'amélioration des performances converge vers 1 / b, où B représente la proportion de code qui doit être exécutée de manière synchrone. Si B est égal à 0,5, cela signifie que la moitié du code du programme ne peut pas fonctionner en parallèle, et le réciproque de 0,5 est de 2, donc même si nous ajoutons d'innombrables cœurs de processeur, nous obtenons un maximum d'amélioration des performances 2x. Supposons que nous ayons modifié le programme maintenant, et après modification, seul le code de 0,25 doit être exécuté de manière synchrone. Maintenant, 1 / 0,25 = 4 signifie que si notre programme s'exécute sur du matériel avec un grand nombre de CPU, il sera environ 4 fois plus rapide que sur du matériel monocœur.
D'un autre côté, grâce à la loi AMDAHL, nous pouvons également calculer la proportion du code de synchronisation selon lequel le programme devrait être basé sur la cible SpeedUp que nous voulons obtenir. Si nous voulons atteindre 100 fois la vitesse, et 1/100 = 0,01 signifie que le nombre maximum de code que notre programme s'exécute de manière synchrone ne peut pas dépasser 1%.
Pour résumer la loi AMDAHL, nous pouvons voir que l'amélioration des performances maximale que nous obtenons en ajoutant un processeur supplémentaire dépend de la taille de la proportion du programme qui exécute la partie du code de manière synchrone. Bien qu'en réalité, il n'est pas toujours facile de calculer ce ratio, sans parler de certaines grandes applications de système commercial, la loi AMDAHL nous donne une inspiration importante, c'est-à-dire que nous devons considérer le code qui doit être exécuté de manière synchrone et essayer de réduire cette partie du code.
1.2 Effet sur les performances
Comme l'article l'écrit ici, nous avons fait valoir que l'ajout de threads peut améliorer les performances et la réactivité du programme. Mais d'un autre côté, il n'est pas facile de réaliser ces avantages, et cela nécessite également un certain prix. L'utilisation de threads affectera également l'amélioration des performances.
Tout d'abord, le premier impact vient du moment de la création de threads. Pendant la création de threads, le JVM doit s'appliquer aux ressources correspondantes du système d'exploitation sous-jacente et initialiser la structure de données du planificateur pour déterminer l'ordre des threads d'exécution.
Si votre nombre de threads est le même que le nombre de noyaux de processeur, chaque fil fonctionnera sur un noyau afin qu'il ne soit pas interrompu souvent. Mais en fait, lorsque votre programme est en cours d'exécution, le système d'exploitation aura également certaines de ses propres opérations qui doivent être traitées par le CPU. Ainsi, même dans ce cas, votre fil sera interrompu et attendra que le système d'exploitation reprenne son fonctionnement. Lorsque votre nombre de discussions dépasse le nombre de cœurs de processeur, la situation peut s'aggraver. Dans ce cas, le planificateur de processus de JVM interrompra certains threads pour permettre à d'autres threads de s'exécuter. Lorsque les threads sont commutés, l'état actuel du thread en cours d'exécution doit être enregistré afin que l'état de données puisse être restauré la prochaine fois qu'il sera exécuté. Non seulement cela, le planificateur mettra également à jour sa propre structure de données interne, ce qui nécessite également des cycles CPU. Tout cela signifie que le changement de contexte entre les threads consomme des ressources informatiques CPU, apportant ainsi des frais généraux de performance par rapport à celui dans un seul cas fileté.
Une autre surcharge apportée par des programmes multithread provient de la protection d'accès synchrone des données partagées. Nous pouvons utiliser le mot-clé synchronisé pour la protection de la synchronisation, ou nous pouvons utiliser le mot-clé volatil pour partager les données entre plusieurs threads. Si plus d'un thread souhaite accéder à une structure de données partagée, une affirmation se produira. Pour le moment, le JVM doit décider quel processus est le premier et quel processus est en retard. Si le thread à exécuter n'est pas le thread en cours d'exécution, une commutation de thread se produit. Le thread actuel doit attendre qu'elle acquière avec succès l'objet de verrouillage. Le JVM peut décider comment effectuer cette "attente". Si le JVM s'attend à être plus court pour acquérir avec succès l'objet verrouillé, le JVM peut utiliser des méthodes d'attente agressives, telles que l'essayer constamment d'acquérir l'objet verrouillé jusqu'à ce qu'il réussit. Dans ce cas, cette méthode peut être plus efficace, car il est encore plus rapide de comparer la commutation de contexte de processus. Le déplacement d'un fil d'attente vers la file d'attente d'exécution apportera également des frais généraux supplémentaires.
Par conséquent, nous devons faire de notre mieux pour éviter la commutation de contexte causée par la concurrence de verrouillage. La section suivante expliquera deux façons de réduire la survenue d'une telle concurrence.
1.3 Concours de verrouillage
Comme mentionné dans la section précédente, l'accès concurrent au verrouillage de deux threads ou plus apportera des frais généraux de calcul supplémentaires, car la concurrence se produit pour forcer le planificateur à saisir un état d'attente agressif, ou à le permettre d'effectuer un état d'attente, provoquant deux commutateurs de contexte. Il y a des cas où les conséquences de la concurrence des verrouillage peuvent être atténuées par:
1. Réduire la portée des serrures;
2. Réduire la fréquence des verrous qui doivent être acquis;
3. Essayez d'utiliser les opérations de verrouillage optimistes prises en charge par le matériel plutôt que synchronisé;
4. Essayez d'utiliser le moins possible synchronisé;
5. Réduire l'utilisation du cache d'objet
1.3.1 Réduire le domaine de synchronisation
Si le code conserve le verrou pour plus que nécessaire, cette première méthode peut être appliquée. Habituellement, nous pouvons déplacer une ou plusieurs lignes de code hors de la zone de synchronisation pour réduire le temps que le fil actuel maintient le verrou. Plus le code s'exécute dans la zone de synchronisation, plus le thread actuel actuel libèrera le verrou, permettant à d'autres threads d'acquérir le verrouillage plus tôt. Cela est conforme à la loi AMDAHL, car cela réduit la quantité de code qui doit être exécutée de manière synchrone.
Pour une meilleure compréhension, regardez le code source suivant:
classe publique ReduceLockDuration implémente Runnable {private static final int number_of_threads = 5; Map final statique privé <String, Integer> map = new HashMap <String, Integer> (); public void run () {for (int i = 0; i <10000; i ++) {synchronisé (map) {uUid randomuuid = uuid.randomuuid (); Valeur entière = Integer.ValueOf (42); String key = randomuuid.toString (); map.put (clé, valeur); } Thread.yield (); }} public static void main (String [] args) lève InterruptedException {thread [] threads = new Thread [number_of_threads]; for (int i = 0; i <number_of_threads; i ++) {threads [i] = nouveau thread (new reduceLockDuration ()); } long startMillis = System.currentTimemillis (); pour (int i = 0; i <nombre_of_threads; i ++) {threads [i] .start (); } pour (int i = 0; i <number_of_threads; i ++) {threads [i] .join (); } System.out.println ((System.CurrenttimeMillis () - StartMillis) + "MS"); }}Dans l'exemple ci-dessus, nous avons laissé cinq threads rivaliser pour accéder à l'instance de carte partagée. Afin de ne qu'un seul thread peut accéder à l'instance de carte en même temps, nous mettons le fonctionnement de l'ajout de clé / valeur à la carte dans le bloc de code protégé synchronisé. Lorsque nous examinons attentivement ce code, nous pouvons voir que les quelques phrases de code qui calculent la clé et la valeur n'ont pas besoin d'être exécutées de manière synchrone. La touche et la valeur n'appartiennent qu'au thread qui exécute actuellement ce code. Il est seulement significatif pour le thread actuel et ne sera pas modifié par d'autres threads. Par conséquent, nous pouvons déplacer ces phrases hors de la protection de synchronisation. comme suit:
public void run () {for (int i = 0; i <10000; i ++) {uuid randomUuid = uuid.randomuuid (); Valeur entière = Integer.ValueOf (42); String key = randomuuid.toString (); synchronisé (map) {map.put (key, valeur); } Thread.yield (); }}L'effet de la réduction du code de synchronisation est mesurable. Sur ma machine, le temps d'exécution de l'ensemble du programme est passé de 420 ms à 370 ms. Jetez un œil, le simple fait de déplacer trois lignes de code hors du bloc de protection de synchronisation peut réduire le temps d'exécution du programme de 11%. Le code thread.yield () consiste à induire un commutateur de contexte de thread, car ce code indiquera au JVM que le thread actuel veut remettre les ressources de calcul actuellement utilisées afin que les autres threads en attente d'exécution puissent s'exécuter. Cela conduira également à plus de concurrence de verrouillage, car si ce n'est pas le cas, un fil occupera un certain noyau plus longtemps, réduisant ainsi la commutation de contexte de thread.
1.3.2 verrouillage divisé
Une autre façon de réduire la concurrence de verrouillage consiste à répandre un bloc de code protégée de verrouillage dans un certain nombre de blocs de protection plus petits. Cette méthode fonctionnera si vous utilisez un verrou dans votre programme pour protéger plusieurs objets différents. Supposons que nous voulons compter certaines données via un programme et implémentez une classe de comptage simple pour contenir plusieurs indicateurs statistiques différents et les représenter avec une variable de comptage de base (type long). Parce que notre programme est multi-thread, nous devons protéger de manière synchrone les opérations qui accèdent à ces variables, car ces actions proviennent de différents threads. Le moyen le plus simple d'y parvenir est d'ajouter le mot clé synchronisé à chaque fonction qui accède à ces variables.
Classe statique publique CounterNlock implémente le compteur {private long CustomCount = 0; SPRIVANT LONG PRIVÉ = 0; public synchronisé void incmenmentCustomer () {CustomCount ++; } public synchronisé void incrémentShipping () {ShippingCount ++; } public synchronisé long getCustomerCount () {return CustomAccount; } public synchronisé long getShippingCount () {return ShippingCount; }}Cela signifie que chaque modification de ces variables entraînera le verrouillage vers d'autres instances de comptoir. Si d'autres threads souhaitent appeler la méthode d'incrément sur une autre variable différente, ils ne peuvent qu'attendre que le thread précédent libère le contrôle de verrouillage avant d'avoir la possibilité de le terminer. Dans ce cas, l'utilisation d'une protection synchronisée distincte pour chaque variable différente améliorera l'efficacité de l'exécution.
Classe statique public CounterSeparateLock implémente le compteur {private static final objet clientLock = new object (); EXPRIMANCE FINAL STATIQUE STATIQUE PRIVATE = NOUVEAU objet (); Private Long CustomCount = 0; SPRIVANT LONG PRIVÉ = 0; public void incmenmentCustomer () {synchronisé (clientLock) {CustomCount ++; }} public void incrémentShipping () {Synchronized (ShippingLock) {ShippingCount ++; }} public long getCustomerCount () {synchronisé (CustomerLock) {return CustomCountnt; }} public long getShippingCount () {Synchronized (ShippingLock) {return ShippingCount; }}}Cette implémentation introduit un objet synchronisé distinct pour chaque métrique de comptage. Par conséquent, lorsqu'un fil veut augmenter le nombre de clients, il doit attendre un autre thread qui augmente le nombre de clients à terminer, plutôt que d'attendre un autre fil qui augmente le nombre d'expédition.
En utilisant les classes suivantes, nous pouvons facilement calculer les améliorations des performances apportées par les verrous fendus.
classe publique Lockspliting implémente Runnable {private static final int numéro_of_threads = 5; compteur privé; Interface publique Counter {void incmentCustomer (); vide incrémentShipping (); long getCustomerCount (); long getShippingCount (); } Classe statique publique Contrôle-Ornilock implémente le compteur {...} Classe statique publique CounterSeparatelock implémente COMPTOR {...} public Lockspliting (COMPTOR COMTER) {this.counter = compter; } public void run () {for (int i = 0; i <100000; i ++) {if (threadLocalrandom.current (). nextBoolean ()) {compter.incrementCustomer (); } else {compter.incrementShipping (); }}} public static void main (String [] args) lève InterruptedException {thread [] threads = new Thread [number_of_threads]; Compteur compteur = new CounterNlock (); for (int i = 0; i <nombre_of_threads; i ++) {threads [i] = nouveau thread (new Lockspliting (compteur)); } long startMillis = System.currentTimemillis (); pour (int i = 0; i <nombre_of_threads; i ++) {threads [i] .start (); } pour (int i = 0; i <number_of_threads; i ++) {threads [i] .join (); } System.out.println ((System.CurrenttimeMillis () - StartMillis) + "MS"); }}Sur ma machine, la méthode d'implémentation d'un seul verrou prend en moyenne 56 ms et l'implémentation de deux verrous distincts est de 38 ms. Le temps qui prend du temps est réduit d'environ 32%.
Une autre façon d'améliorer est que nous pouvons même aller plus loin pour protéger la lecture et l'écriture avec différentes serrures. La classe de comptoir d'origine fournit des méthodes pour la lecture et l'écriture des indicateurs de comptage respectivement. Cependant, en fait, les opérations de lecture ne nécessitent pas de protection de synchronisation. Nous pouvons être assurés que plusieurs threads peuvent lire la valeur de l'indicateur actuel en parallèle. Dans le même temps, les opérations d'écriture doivent être protégées de manière synchrone. Le package java.util.concurrent fournit une implémentation de l'interface readwritelock, qui peut facilement faire cette distinction.
L'implémentation ReentRanTreadWriteLock maintient deux verrous différents, l'un protège l'opération de lecture et l'autre protège l'opération d'écriture. Les deux verrous ont des opérations pour acquérir et libérer des verrous. Un verrouillage d'écriture ne peut être obtenu avec succès que lorsque personne n'acquiert un verrou de lecture. Inversement, tant que le verrouillage d'écriture n'est pas acquis, le verrouillage de lecture peut être acquis par plusieurs threads en même temps. Pour démontrer cette approche, la classe de comptoir suivante utilise ReadWriteLock, comme suit:
classe statique publique CounterReadWriteLock implémente le compteur {private final reentRentaDWriteLock clientLock = new ReentRanTreadWriteLock (); Lock final privé CustomerWriteLock = CustomerLock.WriteLock (); Verrou final privé CustomEreadLock = CustomerLock.readlock (); Finale privée reentRanTreadWriteLock ShippingLock = new ReentRanTreadWriteLock (); Lock final privé ShipingWriteLock = ShippingLock.WriteLock (); Lock final privé ShippingRlock = Shiplock.readlock (); Private Long CustomCount = 0; SPRIVANT LONG PRIVÉ = 0; public void incmenmentCustomer () {CustomerWriteLock.lock (); CustomAccount ++; CustomerWriteLock.Unlock (); } public void incrémentShipping () {ShippingWriteLock.lock (); SPRENDRE ++; ShippingWriteLock.Unlock (); } public long getCustomerCount () {CustomEreadLock.lock (); Long Count = CustomCount ( CustomErreadlock.unlock (); Return Count; } public long getShippingCount () {ShippingReadlock.lock (); Long Count = ShippingCount; SHIVERSREADLOCK.UNLOCK (); Return Count; }}Toutes les opérations de lecture sont protégées par des serrures de lecture, et toutes les opérations d'écriture sont protégées par des verrous d'écriture. Si les opérations de lecture effectuées dans le programme sont beaucoup plus importantes que les opérations d'écriture, cette implémentation peut apporter de plus grandes améliorations des performances que la section précédente car les opérations de lecture peuvent être effectuées simultanément.
1.3.3 Lock de séparation
L'exemple ci-dessus montre comment séparer un seul verrou dans plusieurs verrous séparés afin que chaque thread puisse simplement obtenir le verrou de l'objet qu'ils sont sur le point de modifier. Mais d'un autre côté, cette méthode augmente également la complexité du programme et peut provoquer des blocs de blocage s'ils sont mis en œuvre de manière inappropriée.
Un verrouillage de détachement est une méthode similaire à un verrouillage de détachement, mais un verrouillage de détachement consiste à ajouter un verrou pour protéger différents extraits de code ou objets, tandis qu'un verrouillage de détachement doit utiliser un verrou différent pour protéger différentes gammes de valeurs. ConcurrentHashMap dans Java.util.util.Util.Concurrent Package utilise cette idée pour améliorer les performances des programmes qui reposent fortement sur Hashmap. En termes de mise en œuvre, ConcurrentHashMap utilise 16 verrous différents en interne, au lieu d'encapsuler un hashmap protégé de manière synchrone. Chacune des 16 serrures est responsable de la protection de l'accès synchrone à un dixième des bits de seau (seaux). De cette façon, lorsque différents threads souhaitent insérer des touches dans différents segments, les opérations correspondantes seront protégées par différentes verrous. Mais cela apportera également de mauvais problèmes, tels que l'achèvement de certaines opérations nécessite désormais plusieurs verrous au lieu d'une serrure. Si vous souhaitez copier l'intégralité de la carte, les 16 verrous doivent être obtenus pour terminer.
1.3.4 Opération atomique
Une autre façon de réduire la concurrence de verrouillage consiste à utiliser les opérations atomiques, qui expliqueront les principes des autres articles. Le package java.util.concurrent fournit des classes encapsulées atomiquement pour certains types de données de base couramment utilisés. La mise en œuvre de la classe d'opération atomique est basée sur la fonction «Permutation de comparaison» (CAS) fournie par le processeur. L'opération CAS effectue une opération de mise à jour que lorsque la valeur du registre actuel est la même que l'ancienne valeur fournie par l'opération.
Ce principe peut être utilisé pour augmenter la valeur d'une variable de manière optimiste. Si notre thread connaît la valeur actuelle, il essaiera d'utiliser l'opération CAS pour effectuer l'opération d'incrément. Si d'autres threads ont modifié la valeur de la variable au cours de cette période, la valeur dite de courant fournie par le thread est différente de la valeur réelle. À l'heure actuelle, le JVM essaie de retrouver la valeur actuelle et de réessayer, la répéter à nouveau jusqu'à ce qu'elle réussit. Bien que les opérations en boucle gaspillent certains cycles de CPU, l'avantage de le faire est que nous n'avons besoin d'aucune forme de contrôle de synchronisation.
L'implémentation de la classe Counter ci-dessous utilise les opérations atomiques. Comme vous pouvez le voir, aucun code synchronisé n'est utilisé.
classe statique publique contre-atomique implémente compteur {private atomiclong coustomCount = new atomiclong (); Private Atomiclong ShippingCount = new atomiclong (); public void incmenmentCustomer () {CustomCountCount.incrementAndget (); } public void incrémentShipping () {ShippingCount.IncrementAndget (); } public long getCustomerCount () {return CustomCount.get (); } public long getShippingCount () {return ShippingCount.get (); }}Par rapport à la classe Counter-Paratelock, le temps d'exécution moyen a été réduit de 39 ms à 16 ms, soit environ 58%.
1.3.5 Évitez les segments de code hotspot
Une implémentation de liste typique enregistre le nombre d'éléments contenus dans la liste elle-même en maintenant une variable dans le contenu. Chaque fois qu'un élément est supprimé ou ajouté de la liste, la valeur de cette variable changera. Si la liste est utilisée dans une application unique, cette méthode est compréhensible. Chaque fois que vous appelez size (), vous pouvez simplement retourner la valeur après le dernier calcul. Si cette variable de nombre n'est pas maintenue en interne par liste, chaque appel à taille () entraînera la liste de rétravverse et calculera le nombre d'éléments.
Cette méthode d'optimisation utilisée par de nombreuses structures de données deviendra un problème lorsqu'elle est dans un environnement multithread. Supposons que nous partagions une liste entre plusieurs threads et que plusieurs threads ajoutent ou supprimons simultanément des éléments dans la liste et interrogez la grande longueur. À l'heure actuelle, la variable de nombre à l'intérieur de la liste devient une ressource partagée, donc tout accès doit être traité de manière synchrone. Par conséquent, les variables de comptage deviennent un point chaud dans toute la mise en œuvre de la liste.
L'extrait de code suivant montre ce problème:
classe statique publique CarRepositoryWithCounter implémente CarRepository {Private Map <String, Car> Cars = new HashMap <String, Car> (); Carte privée <String, Car> Trucks = new HashMap <String, Car> (); objet privé carCountSync = nouveau objet (); private int CarCount = 0; public void addCar (Car Car) {if (car.getLICECTPALL (). startSwith ("C")) {synchronisé (CARS) {Car CoundCar = Cars.get (car.getLICECTPAP ()); if (findCar == null) {cars.put (car.getLICECTAPLAT (), car); synchronisé (carCountSync) {CarCount ++; }}}} else {synchronisé (Trucks) {car CoundCar = Trucks.get (car.getLICECTPALL ()); if (findCar == null) {Trucks.put (car.getLICECTAPLAT (), car); synchronisé (carCountSync) {CarCount ++; }}}}}} public int getCarCount () {synchronisé (carCountSync) {return CarCount; }}}L'implémentation ci-dessus de Carrepository a deux variables de liste à l'intérieur, l'une est utilisée pour placer l'élément de lavage de voiture et l'autre est utilisé pour placer l'élément de camion. Dans le même temps, il fournit une méthode pour interroger la taille totale de ces deux listes. La méthode d'optimisation utilisée est que chaque fois qu'un élément de voiture est ajouté, la valeur de la variable de comptage interne augmentera. Dans le même temps, l'opération incrémentée est protégée par synchronisé, et il en va de même pour le renvoi de la valeur de comptage.
Pour éviter cette surcharge de synchronisation de code supplémentaire, consultez une autre implémentation de Carrepository ci-dessous: il n'utilise plus une variable de nombre interne, mais compte cette valeur en temps réel dans la méthode de renvoi du nombre total de voitures. comme suit:
Classe statique publique CarRepositoryWithoutCounter implémente CarRepository {Private Map <String, Car> Cars = new HashMap <String, Car> (); Carte privée <String, Car> Trucks = new HashMap <String, Car> (); public void addCar (Car Car) {if (car.getLICECTPALL (). startSwith ("C")) {synchronisé (CARS) {Car CoundCar = Cars.get (car.getLICECTPAP ()); if (findCar == null) {cars.put (car.getLICECTAPLAT (), car); }}} else {synchronisé (Trucks) {car CoundCar = Trucks.get (car.getLICECTAPLAT ()); if (findCar == null) {Trucks.put (car.getLICECTAPLAT (), car); }}}}} public int getCarCount () {synchronisé (CARS) {synchronisé (Trucks) {return Cars.size () + Trucks.size (); }}}}Maintenant, juste dans la méthode getCarcount (), l'accès des deux listes nécessite une protection de synchronisation. Comme l'implémentation précédente, la surcharge de synchronisation chaque fois qu'un nouvel élément est ajouté n'existe plus.
1.3.6 Évitez la réutilisation du cache d'objet
Dans la première version de Java VM, la surcharge de l'utilisation du nouveau mot-clé pour créer de nouveaux objets est relativement élevée, de nombreux développeurs sont habitués à utiliser le mode de réutilisation des objets. Afin d'éviter la création répétée d'objets encore et encore, les développeurs maintiennent un pool de tampons. Après chaque création d'instances d'objets, ils peuvent être enregistrés dans le pool de tampons. La prochaine fois que d'autres fils doivent les utiliser, ils peuvent être récupérés directement à partir du pool de tampons.
À première vue, cette méthode est très raisonnable, mais ce modèle peut causer des problèmes dans les applications multithread. Étant donné que le pool de tampons d'objets est partagé entre plusieurs threads, toutes les opérations de threads lors de l'accès à des objets en elles ont besoin d'une protection synchrone. Les frais généraux de cette synchronisation sont supérieurs à la création de l'objet lui-même. Bien sûr, la création de trop d'objets augmentera le fardeau de la collecte des ordures, mais même en tenant compte, il est encore préférable d'éviter les améliorations des performances apportées en synchronisant le code que d'utiliser le pool de cache d'objet.
Les schémas d'optimisation décrits dans cet article montrent à nouveau que chaque méthode d'optimisation possible doit être soigneusement évaluée lorsqu'elle est réellement appliquée. Une solution d'optimisation immature semble avoir un sens à la surface, mais en fait, elle devient probablement un goulot d'étranglement des performances à son tour.