English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية
Möglichkeiten zur Verbesserung der Leistung von Locks in Java
Wir versuchen, Lösungen für die Probleme unserer Produkte zu finden, aber in diesem Artikel werde ich einige häufig verwendete Techniken teilen, einschließlich der Trennung von Locks, paralleler Datenstrukturen, des Schutzes von Daten anstelle von Code, und der Einschränkung des Wirkungsbereichs von Locks. Diese Technologien ermöglichen es uns, ohne irgendwelche Werkzeuge Deadlocks zu erkennen.
Der eigentliche Grund für das Problem sind nicht die Locks selbst, sondern der Wettbewerb zwischen den Locks.
Oft wird, wenn es in Multithreading-Code um Leistung geht, von Problemen mit Locks geklagt. Schließlich ist es bekannt, dass Locks die Geschwindigkeit der Programmführung verringern und ihre geringere Skalierbarkeit. Daher kann es sehr wahrscheinlich sein, dass, wenn mit dieser 'Allgemeinwissen' optimiert wird, unerwünschte Synchronisationsprobleme in der Zukunft auftreten.
Es ist sehr wichtig, den Unterschied zwischen konkurrenzorientierten und nicht-konkurrenzorientierten Locks zu verstehen. Ein Lock-Wettbewerb wird ausgelöst, wenn ein Thread versucht, in einen Synchronisationsblock oder eine Methode einzutreten, die von einem anderen Thread ausgeführt wird. Dieser Thread wird gezwungen, in den Wartezustand zu gelangen, bis der erste Thread den Synchronisationsblock beendet hat und den Monitor freigegeben hat. Wenn zu jeder Zeit nur ein Thread versucht, einen synchronisierten Codebereich auszuführen, bleibt der Lock im nicht-konkurrenzorientierten Zustand.
Tatsächlich wurde die Synchronisation in JVMs in den meisten Fällen und unter nicht wettbewerbsorientierten Bedingungen optimiert. Nicht-konkurrierende Locks verursachen während der Ausführung keine zusätzlichen Kosten. Daher sollten Sie sich nicht über die Leistung von Locks beschweren, sondern über den Lock-Wettbewerb. Nachdem diese Erkenntnis vorliegen, lassen Sie uns sehen, was getan werden kann, um die Wahrscheinlichkeit des Wettbewerbs zu verringern oder die Dauer des Wettbewerbs zu verkürzen.
Daten anstatt von Code schützen
Eine schnelle Methode zur Lösung von Thread-Sicherheitsproblemen ist das Sperren des Zugriffs auf den gesamten Methodenbereich. Zum Beispiel versucht der folgende Beispielcode, auf diese Weise einen Online-Poker-Game-Server zu erstellen:
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) {/*body skipped for brevity*/} public synchronized void createTable() {/*body skipped for brevity*/} public synchronized void destroyTable(Table table) {/*body skipped for brevity*/} }
Der gute Wille des Autors ist offensichtlich - wenn ein neuer Spieler zu einem Tisch kommt, muss sichergestellt werden, dass die Anzahl der Spieler auf dem Tisch nicht die Gesamtzahl der Spieler übersteigt, die der Tisch aufnehmen kann9.
Diese Lösung muss jedoch tatsächlich immer die Kontrolle über den Zugang der Spieler zum Tisch übernehmen - selbst wenn die Serverlast gering ist, werden die Threads, die auf den Freigabe des Locks warten, regelmäßig Systemkonflikte auslösen. Der gesperrte Block, der die Überprüfung des Kontostands und der Tischbeschränkungen enthält, könnte die Kosten für die Aufrufe erheblich erhöhen und die Wahrscheinlichkeit und Dauer des Wettbewerbs erhöhen.
Der erste Schritt zur Lösung besteht darin sicherzustellen, dass wir Daten und nicht das Synchronisationsmerkmal, das von der Methodendeklaration in den Methodenkörper verschoben wurde, schützen. Für diesen einfachen Fall könnte dies möglicherweise keine großen Veränderungen bedeuten. Aber wir müssen uns auf die Schnittstelle des gesamten Game-Services konzentrieren und nicht nur auf die join() -Methode.
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) {/* body skipped for brevity */} public void createTable() {/* body skipped for brevity */} public void destroyTable(Table table) {/* body skipped for brevity */} }
Eine mögliche kleine Änderung könnte die Art und Weise, wie die Klasse insgesamt funktioniert, beeinflussen. Egal, wann ein Spieler zu einem Tisch kommt, die frühere Synchronisationsmethode sperrt den gesamten GameServer-Instanz, was zu einem Wettbewerb mit Spielern führt, die gleichzeitig versuchen, den Tisch zu verlassen. Das Verschieben des Locks von der Methodendeklaration in den Methodenkörper verzögert den Lock-Laden und verringert die Wahrscheinlichkeit von Lock-Konflikten.
Verkleinern des Schutzbereichs des Locks
Jetzt, wenn wir sicher sind, dass es sich um Daten und nicht um den Programmcode handelt, den wir schützen müssen, sollten wir sicherstellen, dass wir nur dort sperren, wo es notwendig ist - zum Beispiel nach der Umstrukturierung des obigen Codes:
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); } } } } //weitere Methoden aus Gründen der Kürze übersprungen }
Dieser Abschnitt enthält den Code zur Überprüfung des Kontostands des Spielers (kann IO-Operationen auslösen) und möglicherweise zeitaufwendige Operationen, der aus dem Bereich der Lock-Steuerung herausgenommen wurde. Beachten Sie, dass der Lock jetzt nur dazu dient, sicherzustellen, dass die Anzahl der Spieler nicht die Kapazität des Tisches übersteigt, und die Überprüfung des Kontostands ist nicht mehr Teil dieser Schutzmaßnahme.
Separate Locks
Man kann aus der letzten Zeile des obigen Beispiels klar erkennen: Die gesamte Datenstruktur wird von demselben Lock geschützt. Angesichts der Tatsache, dass in dieser Datenstruktur möglicherweise Tausende von Tischen vorhanden sein könnten und wir sicherstellen müssen, dass die Anzahl der Personen in jedem Tisch die Kapazität nicht überschreitet, besteht in diesem Fall immer noch ein hohes Risiko für Konkurrenzereignisse.
Eine einfache Lösung für dieses Problem wäre, für jeden Tisch einen separaten Lock einzuführen, wie im folgenden Beispiel gezeigt:
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); } } } } //weitere Methoden aus Gründen der Kürze übersprungen }
Jetzt synchronisieren wir nur den Zugriff auf einen einzigen Tisch, anstatt aller Tische, was die Wahrscheinlichkeit einer Konkurrenz erheblich verringert. Nimm ein konkretes Beispiel, in unserer Datenstruktur haben wir10Gibt es keine Tisch-Instanzen, so ist die Wahrscheinlichkeit einer Konkurrenz jetzt geringer als zuvor.100-fach.
Verwendung thread-sicherer Datenstrukturen
Eine andere Möglichkeit zur Verbesserung wäre es, die traditionelle einthreadige Datenstruktur abzulegen und stattdessen Datenstrukturen zu verwenden, die explizit für thread-sicher entworfen wurden. Zum Beispiel könnte der Code so aussehen, wenn Sie ConcurrentHashMap verwenden, um Ihre Tisch-Instanzen zu speichern:
public class GameServer { public Map<String, List<Player>> tables = new ConcurrentHashMap<String, List<Player>>(); public synchronized void join(Player player, Table table) {/*Methodenkörper wurde aus Gründen der Kürze weggelassen*/} public synchronized void leave(Player player, Table table) {/*Methodenkörper wurde aus Gründen der Kürze weggelassen*/} public synchronized void createTable() { Table table = new Table(); tables.put(table.getId(), table); } public synchronized void destroyTable(Table table) { tables.remove(table.getId()); } }
Der Synchronisationsblock innerhalb der Methoden join() und leave() ist wie in den vorherigen Beispielen, da wir die Integrität der einzigen Tischdaten gewährleisten müssen. ConcurrentHashMap hilft dabei nicht. Wir verwenden jedoch ConcurrentHashMap, um neue Tische in den Methoden increaseTable() und destroyTable() zu erstellen und zu zerstören, alle diese Operationen sind für ConcurrentHashMap vollständig synchronisiert und erlauben es uns, Tische parallel hinzuzufügen oder zu verringern.
Andere Empfehlungen und Tipps
Verringern Sie die Sichtbarkeit der Locks. In diesem Beispiel ist der Lock als public (öffentlich sichtbar) deklariert, was dazu führen könnte, dass einige mit bösartigen Absichten andere durch das Sperren auf Ihren sorgfältig entworfenen Monitor schädigen.
Betrachten wir die API von java.util.concurrent.locks, um zu sehen, ob es andere bereits implementierte Lock-Strategien gibt und wie wir die obige Lösung verbessern können.
Verwenden Sie atomare Operationen. Der oben genannte einfache Inkrementzähler erfordert tatsächlich keine Sperre. In diesem Beispiel ist es besser, AtomicInteger anstelle von Integer als Zähler zu verwenden.
Letzter Punkt: Egal, ob Sie die automatische Deadlock-Detektionslösung von Plumber verwenden oder Informationen zur Lösung aus dem Thread-Absturz manuell erhalten, ich hoffe, dass dieser Artikel Ihnen helfen kann, Probleme mit der Lock-Konkurrenz zu lösen.
Vielen Dank für das Lesen, ich hoffe, es kann Ihnen helfen, danke für Ihre Unterstützung dieser Website!