English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية
Méthodes d'amélioration des performances des verrous en java
Nous nous efforçons de trouver des solutions à nos problèmes de produits, mais dans cet article, je vais partager avec vous plusieurs techniques couramment utilisées, y compris la séparation des verrous, les structures de données parallèles, la protection des données plutôt que du code, et la réduction de la portée des verrous, qui nous permettent de ne pas utiliser d'outils pour détecter les verrous mortels.
Les verrous ne sont pas la cause du problème, c'est la concurrence entre les verrous.
Généralement, lorsque l'on rencontre des problèmes de performance dans le code multithreadé, on se plaint généralement des problèmes de verrous. Après tout, il est connu que les verrous ralentissent la vitesse d'exécution du programme et qu'ils ont une faible extensibilité. Par conséquent, si l'on commence à optimiser le code avec cette 'connaissance', les résultats pourraient très bien être des problèmes de concurrence concurrents désagréables après coup.
Il est donc très important de comprendre la différence entre les verrous concurrents et les verrous non concurrents. Lorsqu'un thread essaie d'accéder à un bloc ou à une méthode synchronisée par un autre thread en cours d'exécution, une concurrence de verrou est déclenchée. Ce thread est forcé d'entrer dans un état d'attente jusqu'à ce que le premier thread termine le bloc synchronisé et libère le moniteur. Lorsque seules une seule thread essaie d'exécuter une zone de code synchronisée à un moment donné, le verrou reste dans un état non concurrent.
En réalité, dans le cas de non-concurrence et dans la plupart des applications, JVM a optimisé la synchronisation. Les verrous non concurrents ne génèrent pas de coûts supplémentaires pendant leur exécution. Par conséquent, vous ne devriez pas vous plaindre des verrous en raison de problèmes de performance, mais plutôt de la concurrence entre les verrous. Une fois cette compréhension acquise, regardons ce que nous pouvons faire pour réduire la probabilité de concurrence ou réduire la durée de la concurrence.
Protéger les données plutôt que le code
Une méthode rapide pour résoudre les problèmes de sécurité des threads consiste à verrouiller l'accès à l'ensemble de la méthode. Par exemple, dans cet exemple, il est tenté de créer un serveur de jeu de poker en ligne en utilisant cette méthode :
class GameServer { public Map<<String, List<Player>> tables = new HashMap<String, List<Player>>(); public synchronized void join(Player player, Table table) { if (player.getAccountBalance() > table.getLimit()) { List<Player> tablePlayers = tables.get(table.getId()); if (tablePlayers.size() < 9) { tablePlayers.add(player); } } } public synchronized void leave(Player player, Table table) {/*Corps omis pour alléger*/} public synchronized void createTable() {/*Corps omis pour alléger*/} public synchronized void destroyTable(Table table) {/*Corps omis pour alléger*/} }
L'intention de l'auteur est bonne - lorsque un nouveau joueur rejoint la table, il doit être assuré que le nombre de joueurs à la table ne dépasse pas le nombre total de joueurs que la table peut容纳9.
Mais cette solution doit en fait contrôler l'entrée des joueurs dans la table à tout moment - même lorsque la charge du serveur est faible, les threads en attente de la libération du verrou déclencheront fréquemment des événements de concurrence du système. Le bloc de verrouillage contenant des vérifications des soldes des comptes et des limites des tables pourrait augmenter considérablement les coûts des opérations d'appel, ce qui augmentera inévitablement la probabilité et la durée de la concurrence.
La première étape pour résoudre ce problème consiste à nous assurer que nous protégeons les données et non la déclaration de synchronisation déplacée de la déclaration de méthode à l'intérieur de la méthode. Pour cet exemple simple, cela pourrait ne pas changer grand-chose. Mais nous devons considérer l'interface de l'ensemble du service de jeu, et non pas simplement une méthode join().
class GameServer { public Map<String, List<Player>> tables = new HashMap<String, List<Player>>(); public void join(Player player, Table table) { synchronized (tables) { if (player.getAccountBalance() > table.getLimit()) { List<Player> tablePlayers = tables.get(table.getId()); if (tablePlayers.size() < 9) { tablePlayers.add(player); } } } } public void leave(Player player, Table table) {/* Corps omis pour alléger */} public void createTable() {/* Corps omis pour alléger */} public void destroyTable(Table table) {/* Corps omis pour alléger */} }
Ce qui pourrait être une petite modification, pourrait influencer le comportement de l'ensemble de la classe. Quoi qu'il en soit, lorsque le joueur rejoint la table, la méthode de synchronisation précédente verrouillait l'instance entière de GameServer, ce qui pouvait entraîner une concurrence avec les joueurs qui tentaient de quitter la table en même temps. Déplacer le verrou de la déclaration de méthode à l'intérieur de la méthode peut retarder le chargement du verrou, réduisant ainsi la probabilité de concurrence.
Réduire la portée de l'effet du verrou
Maintenant, une fois convaincu que ce que nous devons protéger est les données et non le programme, nous devons nous assurer que nous ne verrouillons que là où c'est nécessaire - par exemple, après que le code ci-dessus ait été refondu :
public class GameServer { public Map<String, List<Player>> tables = new HashMap<String, List<Player>>(); public void join(Player player, Table table) { if (player.getAccountBalance() > table.getLimit()) { synchronized (tables) { List<Player> tablePlayers = tables.get(table.getId()); if (tablePlayers.size() < 9) { tablePlayers.add(player); } } } } //Autres méthodes omises pour la concision }
Cette partie contient le code de vérification du solde du compte du joueur (peut entraîner des opérations IO) susceptible de provoquer des opérations longues, qui a été déplacé hors de la portée du contrôle des verrous. Notez que maintenant, le verrou n'est utilisé que pour empêcher le nombre de joueurs dépassant le nombre que peut容纳 le tableau, la vérification du solde du compte n'est plus une partie de cette mesure de protection.
Verrous distincts
Vous pouvez voir clairement à la dernière ligne de l'exemple ci-dessus : toute la structure de données est protégée par le même verrou. En considérant que dans ce type de structure de données, il peut y avoir des milliers de tables, et que nous devons protéger pour que le nombre de joueurs dans chaque table ne dépasse pas la capacité, il reste un risque élevé de concurrence dans cette situation.
Pour cela, une solution simple consiste à introduire des verrous distincts pour chaque table, comme dans l'exemple suivant :
public class GameServer { public Map<String, List<Player>> tables = new HashMap<String, List<Player>>(); public void join(Player player, Table table) { if (player.getAccountBalance() > table.getLimit()) { List<Player> tablePlayers = tables.get(table.getId()); synchronized (tablePlayers) { if (tablePlayers.size() < 9) { tablePlayers.add(player); } } } } //Autres méthodes omises pour la concision }
Maintenant, nous ne synchronisons que l'accès à une seule table au lieu de toutes les tables, ce qui réduit considérablement la probabilité de concurrence de verrous. Prenez un exemple concret, maintenant dans notre structure de données, il y a10S'il n'y a qu'une seule instance de table, la probabilité de concurrence est maintenant inférieure à celle de précédemment100 fois.
Utilisation de structures de données thread-safe
Un autre point d'amélioration possible serait de renoncer aux structures de données mono-threadées traditionnelles et de passer à des structures de données conçues explicitement pour être thread-safe. Par exemple, lorsque vous utilisez ConcurrentHashMap pour stocker vos instances de tables de jeu, le code pourrait être comme suit :
public class GameServer { public Map<String, List<Player>> tables = new ConcurrentHashMap<String, List<Player>>(); public synchronized void join(Player player, Table table) {/*Method body skipped for brevity*/} public synchronized void leave(Player player, Table table) {/*Method body skipped for brevity*/} public synchronized void createTable() { Table table = new Table(); tables.put(table.getId(), table); } public synchronized void destroyTable(Table table) { tables.remove(table.getId()); } }
The synchronized block inside the join() and leave() methods is still the same as the previous example, because we need to ensure the integrity of the single table data. ConcurrentHashMap does not help with this. However, we will still use ConcurrentHashMap to create and destroy new tables in the increaseTable() and destroyTable() methods, all of these operations are completely synchronized for ConcurrentHashMap, allowing us to add or reduce the number of tables in a parallel manner.
Other suggestions and tips
Reduce the visibility of locks. In the above example, the lock is declared as public (visible to the outside), which may allow some malicious individuals to disrupt your work by locking on the carefully designed monitor.
Let's take a look at the java.util.concurrent.locks API to see if there are other implemented lock strategies, and use them to improve the above solution.
Use atomic operations. The simple incrementing counter currently in use does not actually require locking. In the above example, it is more appropriate to use AtomicInteger instead of Integer as the counter.
Last but not least, whether you are using Plumber's automatic deadlock detection solution or manually obtaining information from thread dumps to find solutions, I hope this article can help you solve the problem of lock contention.
Thank you for reading, I hope it can help everyone, thank you for your support to this site!