Der Schwerpunkt dieses Artikels liegt auf Leistungsproblemen von Multithread -Anwendungen. Wir werden zuerst Leistung und Skalierbarkeit definieren und dann die Amdahl -Regel sorgfältig untersuchen. In den folgenden Inhalten werden wir untersuchen, wie verschiedene technische Methoden verwendet werden, um den Sperrwettbewerb zu reduzieren und mit Code zu implementieren.
1. Leistung
Wir alle wissen, dass Multithreading verwendet werden kann, um die Programmleistung zu verbessern, und der Grund dafür ist, dass wir Multi-Core-CPUs oder mehrere CPUs haben. Jeder CPU -Kern kann die Aufgaben für sich ausführen. Daher kann die Gesamtleistung des Programms eine Reihe kleiner Aufgaben verbessern, die unabhängig voneinander ausgeführt werden können. Sie können ein Beispiel geben. Zum Beispiel gibt es ein Programm, das die Größe aller Bilder in einem Ordner auf der Festplatte verändert, und die Anwendung der Multi-Threading-Technologie kann seine Leistung verbessern. Wenn Sie einen einzelnen Thread -Ansatz verwenden, können Sie nur alle Bilddateien nacheinander durchqueren und Modifikationen durchführen. Wenn unsere CPU mehrere Kerne hat, besteht kein Zweifel daran, dass sie nur einen von ihnen verwenden kann. Unter Verwendung von Multi-Threading können wir ein Produzenten-Thread des Dateisystems scannen, um jedes Bild einer Warteschlange hinzuzufügen, und dann mehrere Worker-Threads zum Ausführen dieser Aufgaben verwenden. Wenn die Anzahl der Arbeiter -Threads mit der Gesamtzahl der CPU -Kerne übereinstimmt, können wir sicherstellen, dass jeder CPU -Kern die Arbeit hat, bis alle Aufgaben ausgeführt werden.
Für ein weiteres Programm, für das mehr IO-Warten erforderlich sind, kann die Gesamtleistung auch mithilfe von Multi-Threading-Technologien verbessert werden. Angenommen, wir möchten ein solches Programm schreiben, das wir benötigen, um alle HTML -Dateien einer bestimmten Website zu kriechen und sie auf lokaler Festplatte zu speichern. Das Programm kann von einer bestimmten Webseite aus starten, dann alle Links zu dieser Website auf dieser Webseite analysieren und diese Links dann nacheinander kragen, damit es sich wiederholt. Da es eine Weile dauert, bis wir ab dem Zeitpunkt, an dem wir eine Anfrage an die Remote -Website einleiten, bis zu dem Zeitpunkt, an dem wir alle Webseitendaten erhalten, können wir diese Aufgabe für die Ausführung an mehrere Threads übergeben. Lassen Sie einen oder ein wenig mehr Thread die empfangene HTML -Seite analysieren und den gefundenen Link in die Warteschlange eingeben, sodass alle anderen Threads für die Anforderung der Seite verantwortlich sind. Im Gegensatz zum vorherigen Beispiel können Sie in diesem Beispiel auch dann, wenn Sie mehr Threads als die Anzahl der CPU -Kerne verwenden.
Die beiden oben genannten Beispiele zeigen, dass hohe Leistung in kurzer Zeit so viele Dinge wie möglich durchführen soll. Dies ist natürlich die klassischste Erklärung für den Begriff Auftritt. Gleichzeitig kann die Verwendung von Threads auch die Reaktionsgeschwindigkeit unserer Programme gut verbessern. Stellen Sie sich vor, wir haben eine so grafische Schnittstellenanwendung mit einem Eingabefeld oben und einer Schaltfläche "Prozess" unter dem Eingabefeld. Wenn der Benutzer diese Taste drückt, muss die Anwendung den Status der Taste erneut rendern (die Schaltfläche wird angezeigt und kehrt zu ihrem ursprünglichen Status zurück, wenn die linke Maustaste veröffentlicht wird) und starten Sie die Eingabe des Benutzers. Wenn diese Aufgabe zeitaufwändig ist, um Benutzereingaben zu verarbeiten, kann ein einzelnes Programm nicht weiter auf andere Benutzereingänge reagieren, z.
Skalierbarkeit bedeutet, dass Programme die Möglichkeit haben, eine höhere Leistung zu erzielen, indem Rechenressourcen hinzugefügt werden. Stellen Sie sich vor, wir müssen die Größe vieler Bilder anpassen, da die Anzahl der CPU -Kerne unserer Maschine begrenzt ist und die Anzahl der Threads die Leistung nicht immer entsprechend verbessert. Im Gegenteil, da der Scheduler für die Erstellung und Herstellung von weiteren Threads verantwortlich sein muss, wird er auch CPU -Ressourcen belegen, was die Leistung verringern kann.
1.1 Amdahl -Regel
In dem vorherigen Absatz wurde erwähnt, dass in einigen Fällen zusätzliche Rechenressourcen die Gesamtleistung des Programms verbessern können. Um zu berechnen, wie viel Leistungsverbesserung wir erhalten können, wenn wir zusätzliche Ressourcen hinzufügen, müssen Sie überprüfen, welche Teile des Programms seriell (oder synchron) ausgeführt werden und welche Teile parallel ausgeführt werden. Wenn wir den Anteil des Codes quantifizieren, der synchron mit B ausgeführt werden muss (z. B. die Anzahl der Codezeilen, die synchron ausgeführt werden müssen) und die Gesamtzahl der Kerne der CPU als n aufzeichnen, ist nach Angaben des Amdahl -Gesetzes die obere Grenze der Leistungsverbesserungen, die wir erhalten können,:
Wenn n neigt, um unendlich zu sein, konvergiert (1-b)/N zu 0. Daher können wir den Wert dieses Ausdrucks ignorieren, sodass die Bitzahl der Leistungsverbesserung zu 1/b konvergiert, wobei B den Anteil des Codes repräsentiert, der synchron ausgeführt werden muss. Wenn B gleich 0,5 ist, bedeutet dies, dass die Hälfte des Codes des Programms nicht parallel ausgeführt wird und der Gegenstand von 0,5 2 beträgt. Selbst wenn wir unzählige CPU -Kerne hinzufügen, erhalten wir maximal 2x -Leistungsverbesserungen. Angenommen, wir haben das Programm jetzt geändert, und nach der Änderung darf nur 0,25 Code synchron ausgeführt werden. Jetzt 1/0,25 = 4 bedeutet, dass unser Programm, wenn unser Programm auf Hardware mit einer großen Anzahl von CPUs ausgeführt wird, etwa 4-mal schneller sein wird als auf Single-Core-Hardware.
Andererseits können wir durch das Amdahl -Gesetz auch den Anteil des Synchronisationscodes berechnen, dass das Programm auf dem von uns erhaltenen Geschwindigkeitsziel basieren sollte. Wenn wir eine 100 -fache Beschleunigung erreichen möchten und 1/100 = 0,01 bedeutet, dass die maximale Anzahl von Code, die unser Programm synchron ausführt, 1%nicht überschreiten darf.
Um das Amdahl -Gesetz zusammenzufassen, können wir feststellen, dass die maximale Leistungsverbesserung, die wir durch das Hinzufügen einer zusätzlichen CPU erhalten, davon abhängt, wie klein der Anteil des Programms einen Teil des Codes synchron ausführt. Obwohl es in Wirklichkeit nicht immer einfach ist, dieses Verhältnis zu berechnen, geschweige denn mit einigen großen kommerziellen Systemanwendungen ausgesetzt ist, gibt das AMDAHL -Gesetz uns eine wichtige Inspiration, dh den Code, der synchron ausgeführt werden muss, und versuchen, diesen Teil des Codes zu reduzieren.
1.2 Auswirkung auf die Leistung
Wie der Artikel hier schreibt, haben wir einen Punkt gemacht, dass das Hinzufügen weiterer Threads die Programmleistung und die Reaktionsfähigkeit verbessern kann. Andererseits ist es jedoch nicht einfach, diese Vorteile zu erzielen, und es erfordert auch einen Preis. Die Verwendung von Threads wirkt sich auch auf die Leistungsverbesserung aus.
Erstens kommt der erste Einfluss aus der Zeit der Thread -Erstellung. Während der Erstellung von Threads muss das JVM entsprechende Ressourcen aus dem zugrunde liegenden Betriebssystem anwenden und die Datenstruktur im Scheduler initialisieren, um die Reihenfolge der Ausführungs -Threads zu bestimmen.
Wenn Ihre Anzahl von Threads mit der Anzahl der CPU -Kerne die gleiche entspricht, wird jeder Thread auf einem Kern ausgeführt, sodass er möglicherweise nicht häufig unterbrochen wird. Wenn Ihr Programm ausgeführt wird, verfügt das Betriebssystem jedoch auch über einige seiner eigenen Vorgänge, die von der CPU bearbeitet werden müssen. Selbst in diesem Fall wird Ihr Thread unterbrochen und warten, bis das Betriebssystem seinen Betrieb wieder aufnimmt. Wenn Ihre Fadenzahl die Anzahl der CPU -Kerne überschreitet, kann die Situation schlechter werden. In diesem Fall unterbricht der Prozessplaner des JVM bestimmte Threads, damit andere Threads ausgeführt werden können. Wenn Threads umgeschaltet werden, muss der aktuelle Status des laufenden Threads gespeichert werden, damit der Datenzustand das nächste Mal wiederhergestellt werden kann. Darüber hinaus aktualisiert der Scheduler auch seine eigene interne Datenstruktur, für die auch CPU -Zyklen erforderlich sind. All dies bedeutet, dass der Kontextwechsel zwischen Threads CPU -Computerressourcen verbraucht und so den Leistungsaufwand im Vergleich zu einem einzelnen Thread -Fall bringt.
Ein weiterer Overhead, der von Multithread -Programmen mitgebracht wurde, stammt aus dem synchronen Zugriffsschutz gemeinsamer Daten. Wir können das synchronisierte Schlüsselwort zum Schutz des Synchronisationsschutzes verwenden oder das volatile Schlüsselwort verwenden, um Daten zwischen mehreren Threads zu teilen. Wenn mehr als ein Thread auf eine gemeinsam genutzte Datenstruktur zugreifen möchte, tritt eine Auseinandersetzung auf. Zu diesem Zeitpunkt muss der JVM entscheiden, welcher Prozess zuerst ist und welcher Prozess zurückliegt. Wenn der zu ausgeführte Thread nicht der aktuell ausgeführte Thread ist, erfolgt ein Thread -Switching. Der aktuelle Thread muss warten, bis er das Schlossobjekt erfolgreich erwirbt. Die JVM kann entscheiden, wie diese "Wartezeit" durchgeführt werden kann. Wenn die JVM erwartet, das verschlossene Objekt erfolgreich zu erwerben, kann der JVM aggressive Warteverfahren verwenden, z. B. ständig zu versuchen, das verschlossene Objekt zu erwerben, bis es erfolgreich ist. In diesem Fall kann diese Methode effizienter sein, da es immer noch schneller ist, den Prozesskontext zu vergleichen. Wenn Sie einen wartenden Thread zurück in die Ausführungswarteschlange verschieben, bringt er auch zusätzlichen Overhead.
Daher müssen wir unser Bestes geben, um einen durch den Schlosswettbewerb verursachten Kontextwechsel zu vermeiden. Der folgende Abschnitt erläutert zwei Möglichkeiten, das Auftreten eines solchen Wettbewerbs zu verringern.
1.3 Schlosswettbewerb
Wie im vorherigen Abschnitt erwähnt, bringt der konkurrierende Zugriff auf das Schloss durch zwei oder mehr Threads zusätzlichen Rechenaufwand mit sich, da der Wettbewerb stattfindet, um den Scheduler zu zwingen, in einen aggressiven Wartezustand einzutreten, oder um einen Wartezustand durchzuführen, was zu zwei Kontextschaltern führt. Es gibt einige Fälle, in denen die Konsequenzen des Schlosswettbewerbs gemindert werden können durch:
1. Reduzieren Sie den Umfang der Schlösser;
2. Reduzieren Sie die Häufigkeit von Schlössern, die erworben werden müssen;
3.. Versuchen Sie, optimistische Sperrvorgänge zu verwenden, die von Hardware unterstützt werden, anstatt zu synchronisiert.
4.. Versuchen Sie, so wenig wie möglich synchronisiert zu verwenden.
5. Reduzieren Sie die Verwendung von Objektcache
1.3.1 Reduzierung der Synchronisationsdomäne
Wenn der Code die Sperre für mehr als erforderlich hält, kann diese erste Methode angewendet werden. Normalerweise können wir eine oder mehrere Codezeilen aus dem Synchronisationsbereich verschieben, um die Zeit zu verkürzen, die der aktuelle Thread das Sperre hält. Der weniger Code läuft im Synchronisationsbereich, je früher der aktuelle Thread die Sperre veröffentlicht, sodass andere Threads das Sperre früher erwerben können. Dies steht im Einklang mit dem Amdahl -Gesetz, da dies die Menge an Code verringert, die synchron ausgeführt werden muss.
Für ein besseres Verständnis sehen Sie sich den folgenden Quellcode an:
öffentliche Klasse ReducelockDuration implementiert Runnable {private statische endgültige int number_of_threads = 5; private statische endgültige Karte <String, Integer> map = new Hashmap <String, Integer> (); public void run () {für (int i = 0; i <10000; i ++) {synchronized (map) {uUid randomuuid = uUid.randomuuid (); Integer Value = Integer.Valueof (42); String key = randomuuid.toString (); map.put (Schlüssel, Wert); } Thread.yield (); }} public static void main (String [] args) löst InterruptedException aus {Thread [] threads = neuer Thread [number_of_threads]; für (int i = 0; i <number_of_threads; i ++) {threads [i] = neuer Thread (new ReducelockDuration ()); } long startMillis = system.currentTimemillis (); für (int i = 0; i <number_of_threads; i ++) {threads [i] .Start (); } für (int i = 0; i <number_of_threads; i ++) {threads [i] .Join (); } System.out.println ((System.currentTimemillis ()-startMillis)+"ms"); }}Im obigen Beispiel lassen wir fünf Threads um den Zugriff auf die gemeinsam genutzte Karteninstanz konkurrieren. Damit nur ein Thread gleichzeitig auf die MAP -Instanz zugreifen kann, geben wir den Betrieb von Schlüssel/Wert in die MAP in den synchronisierten geschützten Codeblock ein. Wenn wir uns diesen Code sorgfältig ansehen, können wir sehen, dass die wenigen Codesätze, die den Schlüssel und den Wert berechnen, nicht synchron ausgeführt werden müssen. Der Schlüssel und der Wert gehören nur zu dem Thread, der diesen Code derzeit ausführt. Es ist nur für den aktuellen Thread von Bedeutung und wird nicht durch andere Threads geändert. Daher können wir diese Sätze aus dem Synchronisationsschutz verschieben. wie folgt:
public void run () {für (int i = 0; i <10000; i ++) {uUid randomuuid = uUid.randomuuid (); Integer Value = Integer.Valueof (42); String key = randomuuid.toString (); synchronisiert (map) {map.put (Schlüssel, Wert); } Thread.yield (); }}Der Effekt der Reduzierung des Synchronisationscode ist messbar. Auf meiner Maschine wurde die Ausführungszeit des gesamten Programms von 420 ms auf 370 ms reduziert. Schauen Sie sich an, dass Sie nur drei Codezeilen aus dem Synchronisationsschutzblock verschieben können, um die Laufzeit um 11%zu verkürzen. Der Code von Thread.yield () besteht darin, den Thread -Kontext -Switching zu induzieren, da dieser Code dem JVM mitteilt, dass der aktuelle Thread die aktuell verwendeten Rechenressourcen übergeben möchte, sodass andere Threads, die auf ausgeführt werden, ausgeführt werden können. Dies führt auch zu mehr Schlosswettbewerb, da ein Thread, wenn dies nicht der Fall ist, einen bestimmten Kern länger einnimmt, wodurch der Kontext des Threads verringert wird.
1.3.2 Spless -Lock
Eine andere Möglichkeit, den Schlosswettbewerb zu verringern, besteht darin, einen Block mit dem Sperrgeschützten in eine Reihe kleinerer Schutzblöcke zu verteilen. Diese Methode funktioniert, wenn Sie ein Sperre in Ihrem Programm verwenden, um mehrere verschiedene Objekte zu schützen. Nehmen wir an, wir möchten einige Daten über ein Programm zählen und eine einfache Zählklasse implementieren, um mehrere verschiedene statistische Indikatoren aufzunehmen und sie mit einer grundlegenden Zählvariablen (langer Typ) darzustellen. Da unser Programm Multi-Threaded ist, müssen wir die Operationen, die auf diese Variablen zugreifen, synchron schützen, da diese Aktionen aus verschiedenen Threads stammen. Der einfachste Weg, dies zu erreichen, besteht darin, das synchronisierte Schlüsselwort zu jeder Funktion hinzuzufügen, die auf diese Variablen zugreift.
öffentliche statische Klasse Gegenanschluss implementiert Zähler {private long CustomerCount = 0; private lange Versandcount = 0; public synchronisierte void IncrementCustomer () {CustomerCount ++; } public synchronisierte void Incrementshiping () {ShippingCount ++; } public synchronisierte lange getCustomerCount () {return CustomerCount; } public synchronisierte long getshippingCount () {return ShippingCount; }}Dies bedeutet, dass jede Änderung dieser Variablen die Sperre an andere Gegeninstanzen verursacht. Wenn andere Threads die Inkrementmethode auf einer anderen unterschiedlichen Variablen aufrufen möchten, können sie nur darauf warten, dass der vorherige Thread die Sperrsteuerung freigibt, bevor sie die Chance haben, ihn zu vervollständigen. In diesem Fall verbessert die Verwendung eines separaten synchronisierten Schutzes für jede verschiedene Variable die Ausführungseffizienz.
public static class conterparatelock implementiert conter {private static endgültige Objekt CustomerLock = New Object (); private statische endgültige Objekt ShippingLock = neues Objekt (); private long CustomerCount = 0; private lange Versandcount = 0; public void IncrementCustomer () {synchronized (CustomerLock) {CustomerCount ++; }} public void IncrementshipPing () {synchronized (ShippingLock) {ShippingCount ++; }} public Long getCustomerCount () {synchronized (CustomerLock) {return CustomerCount; }} public Long GetScharpingCount () {synchronized (ShippingLock) {return ShippingCount; }}}Diese Implementierung führt für jede Zählmetrik ein separates synchronisiertes Objekt ein. Wenn ein Thread die Kundenzahl erhöhen möchte, muss er auf einen anderen Thread warten, der die Anzahl der Kunden erhöht, anstatt auf einen anderen Thread zu warten, der die Versandanzahl erhöht.
Mit den folgenden Klassen können wir die Leistungsverbesserungen, die durch geteilte Schlösser eingeführt werden, leicht berechnen.
öffentliche Klasse lockSplitting implementiert runnable {private statische endgültige int number_of_threads = 5; privater Zähler; public interface counter {void IncrementCustomer (); void Incrementshiping (); lange GetCustomerCount (); lange GetScharingCount (); } public statische Klasse Gegenanschluss implementiert Zähler {...} public statische Klasse conterparatelock implementiert Counter {...} public sperrsprung (counter) {this.counter = counter; } public void run () {für (int i = 0; i <100000; i ++) {if (threadLocalrandom.current (). Nextboolean ()) {counter.incrementCustomer (); } else {counter.incrementshipPing (); }}} public static void main (String [] args) löst unterbrochene Ausnahme {thread [] threads = new Thread [number_of_threads]; Zähler = neuer Gegenanschluss (); für (int i = 0; i <number_of_threads; i ++) {threads [i] = neuer Thread (neuer sperrung (Zähler)); } long startMillis = system.currentTimemillis (); für (int i = 0; i <number_of_threads; i ++) {threads [i] .Start (); } für (int i = 0; i <number_of_threads; i ++) {threads [i] .Join (); } System.out.println ((System.currentTimemillis () - startMillis) + "ms"); }}Auf meiner Maschine dauert die Implementierungsmethode eines einzelnen Schlosses durchschnittlich 56 ms und die Implementierung von zwei separaten Sperren beträgt 38 ms. Die zeitaufwändige Verringerung wird um etwa 32%reduziert.
Eine andere Möglichkeit, sich zu verbessern, besteht darin, dass wir sogar noch weiter gehen können, um das Lesen und schreiben mit verschiedenen Schlössern zu schützen. Die ursprüngliche Gegenklasse bietet Methoden zum Lesen und Schreiben von Zählindikatoren. Tatsächlich erfordern Lesevorgänge jedoch keinen Synchronisationsschutz. Wir können sicher sein, dass mehrere Threads den Wert der aktuellen Anzeige parallel lesen können. Gleichzeitig müssen Schreibvorgänge synchron geschützt werden. Das Java.util.Concurrent -Paket bietet eine Implementierung der ReadWriteLock -Schnittstelle, die diese Unterscheidung leicht erreichen kann.
Die Implementierung von ReentranTreadWriteLock behält zwei verschiedene Schlösser bei, schützt den Lesevorgang und der andere schützt den Schreibbetrieb. Beide Schlösser haben Operationen, um Schlösser zu erwerben und zu veröffentlichen. Ein Schreibschloss kann nur erfolgreich erhalten werden, wenn niemand ein Leseschloss erwirbt. Umgekehrt kann das Leseschloss gleichzeitig von mehreren Threads erfasst werden, solange das Schreibschloss erfasst wird. Um diesen Ansatz zu demonstrieren, verwendet die folgende Gegenklasse ReadWriteLock wie folgt:
öffentliche statische Klasse CounterreadWriteLock Implements Counter {private endgültige ReentranTreadWriteLock CustomerLock = new ReentranTreadWriteLock (); Private Final Lock CustomerWriteLock = CustomerLock.WriteLock (); Private Final Lock CustomerReadlock = CustomerLock.readlock (); Private Final ReentranTreadWriteLock ShippingLock = New ReentranTreadWriteLock (); Private Final Lock ShippingWriteLock = ShippingLock.WriteLock (); Private Final Lock ShippingReadlock = ShippingLock.readlock (); private long CustomerCount = 0; private lange Versandcount = 0; public void IncrementCustomer () {CustomerWriteLock.lock (); CustomerCount ++; customerWriteLock.unlock (); } public void IncrementshipPing () {ShippingWriteLock.lock (); Versandcount ++; ShippingWriteLock.unlock (); } public long getCustomerCount () {CustomerReadlock.lock (); Long Count = CustomerCount; CustomerReadlock.unlock (); Rückgabezahl; } public Long GetScharpingCount () {ShippingReadlock.lock (); Long Count = ShippingCount; ShippingReadlock.unlock (); Rückgabezahl; }}Alle Lesevorgänge werden durch Lesesperrs geschützt und alle Schreibvorgänge werden durch Schreibresseln geschützt. Wenn die im Programm ausgeführten Lesevorgänge viel größer sind als die Schreibvorgänge, kann diese Implementierung mehr Leistungsverbesserungen als der vorherige Abschnitt bringen, da die Lesevorgänge gleichzeitig durchgeführt werden können.
1.3.3 Trennschloss
Das obige Beispiel zeigt, wie ein einzelnes Sperre in mehrere separate Sperren trennen kann, sodass jeder Thread nur die Sperre des Objekts erhalten kann, die er ändern soll. Andererseits erhöht diese Methode auch die Komplexität des Programms und kann bei unangemessener Implementierung von Decken verursachen.
Eine Ablösungsschloss ist eine ähnliche Methode wie eine Ablösungsschloss. Eine Ablagerungsschloss besteht jedoch darin, eine Sperre zum Schutz verschiedener Codeausschnitte oder -objekte hinzuzufügen, während eine Ablagerungsschloss ein anderes Schloss zum Schutz verschiedener Wertebereiche verwenden soll. Concurrenthashmap im Java.util.Concurrent -Paket von JDK verwendet diese Idee, um die Leistung von Programmen zu verbessern, die stark auf HashMap beruhen. In Bezug auf die Implementierung verwendet Concurrenthashmap 16 verschiedene Sperren intern, anstatt eine synchron geschützte HashMap zu entschlossen. Jeder der 16 Schlösser ist für den Schutz des synchronen Zugangs zu einem Zehntel der Eimerbits (Eimer) verantwortlich. Auf diese Weise werden die entsprechenden Operationen durch verschiedene Tätigkeiten in verschiedene Segmente einfügen, wenn sie Tasten in verschiedene Segmente einfügen möchten. Es wird aber auch einige schlechte Probleme mit sich bringen, wie zum Beispiel die Fertigstellung bestimmter Vorgänge benötigt jetzt mehrere Sperren anstelle einer Schloss. Wenn Sie die gesamte Karte kopieren möchten, müssen alle 16 Schlösser erhalten werden, um abzuschließen.
1.3.4 Atombetrieb
Eine andere Möglichkeit, den Schlosswettbewerb zu verringern, besteht darin, atomare Operationen zu nutzen, die die Prinzipien in anderen Artikeln näher erläutern. Das Java.util.Concurrent -Paket bietet atomisch eingekapselte Klassen für einige häufig verwendete grundlegende Datentypen. Die Implementierung der Atombetriebsklasse basiert auf der vom Prozessor bereitgestellten "Vergleichskpermutation" -Funktion (CAS). Die CAS -Operation führt nur eine Aktualisierungsoperation durch, wenn der Wert des aktuellen Registers dem alten Wert entspricht, der durch die Operation bereitgestellt wird.
Dieses Prinzip kann verwendet werden, um den Wert einer Variablen optimistisch zu erhöhen. Wenn unser Thread den aktuellen Wert kennt, wird versucht, den CAS -Betrieb zu verwenden, um den Inkrementvorgang durchzuführen. Wenn andere Threads in diesem Zeitraum den Wert der Variablen geändert haben, unterscheidet sich der vom Thread bereitgestellte sogenannte Stromwert vom realen Wert. Zu diesem Zeitpunkt versucht der JVM, den aktuellen Wert wiederzugewinnen und es erneut zu versuchen, um ihn erneut zu wiederholen, bis er erfolgreich ist. Obwohl Schleifvorgänge einige CPU -Zyklen verschwenden, ist der Vorteil, dass wir keine Form der Synchronisationsregelung benötigen.
Die Implementierung der nachstehenden Zählerklasse verwendet Atomoperationen. Wie Sie sehen können, wird kein synchronisierter Code verwendet.
public statische Klassen -Gegenatom -Geräte Gegenstände {private atomiclong CustomerCount = new Atomiclong (); Private Atomiclong ShippingCount = New Atomiclong (); public void IncrementCustomer () {CustomerCount.incrementandget (); } public void IncrementshipPing () {ShippingCount.incrementandget (); } public long getCustomerCount () {return CustomerCount.get (); } public Long GetScharpingCount () {return ShippingCount.get (); }}Im Vergleich zur konterseparaten Klasse wurde die durchschnittliche Laufzeit von 39 ms auf 16 ms reduziert, was etwa 58%beträgt.
1.3.5 Hotspot -Code -Segmente vermeiden
Eine typische List -Implementierung erfasst die Anzahl der in der Liste selbst enthaltenen Elemente, indem eine Variable im Inhalt beibehalten wird. Jedes Mal, wenn ein Element aus der Liste gelöscht oder hinzugefügt wird, ändert sich der Wert dieser Variablen. Wenn die Liste in einer Einzelanwendung verwendet wird, ist diese Methode verständlich. Jedes Mal, wenn Sie Size () aufrufen, können Sie den Wert nach der letzten Berechnung einfach zurückgeben. Wenn diese Zählvariable nicht intern durch die Liste aufrechterhalten wird, führt jeder Aufruf von Size () dazu, dass die Liste die Anzahl der Elemente neu überträgt und berechnet.
Diese Optimierungsmethode, die von vielen Datenstrukturen verwendet wird, wird zu einem Problem, wenn sie sich in einer Umgebung mit mehreren Threads befindet. Nehmen wir an, wir teilen eine Liste zwischen mehreren Threads und mehrere Threads, die gleichzeitig Elemente in die Liste hinzufügen oder löschen, und fragen Sie die große Länge ab. Zu diesem Zeitpunkt wird die Zählvariable in der Liste zu einer gemeinsam genutzten Ressource, sodass der gesamte Zugriff darauf synchron verarbeitet werden muss. Daher werden Zählvariablen zu einem Hotspot in der gesamten List -Implementierung.
Das folgende Code -Snippet zeigt dieses Problem:
public statische Klasse carrpositoryWithCounter implementiert Carrpository {private map <String, Car> cars = new Hashmap <String, Car> (); Private Karte <String, Car> Trucks = New HashMap <String, Car> (); privates Objekt CarCountSync = neues Objekt (); private int carbount = 0; public void addcar (Auto Car) {if (car.getLicenceplate (). startsWith ("c")) {synchronisierte (cars) {car foundcar = cars.get (car.getLicenceplate ()); if (foundcar == null) {cars.put (car.getLicenceplate (), car); synchronisiert (CarCountSync) {carCount ++; }}}} else {synchronized (trucks) {car foundcar = trucks.get (car.getLicenceplate ()); if (foundcar == null) {trucks.put (car.getLicencePlate (), car); synchronisiert (CarCountSync) {carCount ++; }}}}}} public int getCarCount () {synchronized (carCountSync) {return carbount; }}}Die obige Implementierung von Carrpository enthält zwei Listenvariablen im Inneren, einer wird verwendet, um das Autowaschelement zu platzieren, und das andere wird verwendet, um das LKW -Element zu platzieren. Gleichzeitig bietet es eine Methode, um die Gesamtgröße dieser beiden Listen abzufragen. Die verwendete Optimierungsmethode ist, dass jedes Mal, wenn ein Autoelement hinzugefügt wird, der Wert der internen Zählvariablen erhöht wird. Gleichzeitig wird der inkrementierte Betrieb durch synchronisiert geschützt, und das Gleiche gilt für die Rückgabe des Zählwerts.
Um diesen zusätzlichen Code -Synchronisationsaufwand zu vermeiden, siehe eine weitere Implementierung von Carrpository unten: Sie verwendet keine interne Zählvariable mehr, zählt jedoch diesen Wert in Echtzeit in der Methode zur Rückgabe der Gesamtzahl der Autos. wie folgt:
public statische Klasse carrpositoryWithoutcounter implementiert Carrpository {private map <String, Car> cars = new Hashmap <String, Car> (); Private Karte <String, Car> Trucks = New HashMap <String, Car> (); public void addcar (Auto Car) {if (car.getLicenceplate (). startsWith ("c")) {synchronisierte (cars) {car foundcar = cars.get (car.getLicenceplate ()); if (foundcar == null) {cars.put (car.getLicenceplate (), car); }}} else {synchronized (trucks) {car foundcar = trucks.get (car.getLicenceplate ()); if (foundcar == null) {trucks.put (car.getLicencePlate (), car); }}}}} public int getCarCount () {synchronized (cars) {synchronized (Trucks) {return cars.size () + Trucks.size (); }}}}Jetzt, nur nach der Methode von GetCarCount (), muss der Zugriff auf die beiden Listen einen Synchronisationsschutz benötigen. Wie bei der vorherigen Implementierung gibt es jedes Mal, wenn ein neues Element hinzugefügt wird, der Synchronisationsaufwand nicht mehr.
1.3.6 Vermeiden Sie die Wiederverwendung von Objekt -Cache
In der ersten Version von Java VM ist der Overhead der Verwendung des neuen Schlüsselworts zum Erstellen neuer Objekte relativ hoch, so dass viele Entwickler es gewohnt sind, den Objekt -Wiederverwendungsmodus zu verwenden. Um immer wieder eine wiederholte Erstellung von Objekten zu vermeiden, behalten Entwickler einen Pufferpool bei. Nach jeder Erstellung von Objektinstanzen können sie im Pufferpool gespeichert werden. Wenn Sie das nächste Mal andere Threads verwenden müssen, können sie direkt aus dem Pufferpool abgerufen werden.
Auf den ersten Blick ist diese Methode sehr vernünftig, dieses Muster kann jedoch Probleme in Multithread -Anwendungen verursachen. Da der Pufferpool von Objekten unter mehreren Threads gemeinsam genutzt wird, müssen alle Threads Operationen beim Zugriff auf Objekte in ihnen synchronen Schutz benötigen. Der Overhead dieser Synchronisation ist größer als die Erstellung des Objekts selbst. Natürlich erhöht das Erstellen von zu vielen Objekten die Belastung der Müllsammlung, aber selbst wenn dies berücksichtigt wird, ist es immer noch besser, die Leistungsverbesserungen zu vermeiden, die durch die Synchronisierung des Codes mitgebracht werden, als den Objekt -Cache -Pool zu verwenden.
Die in diesem Artikel beschriebenen Optimierungsschemata zeigen erneut, dass jede mögliche Optimierungsmethode sorgfältig bewertet werden muss, wenn sie tatsächlich angewendet wird. Eine unreife Optimierungslösung scheint auf der Oberfläche sinnvoll zu sein, aber tatsächlich wird sie wahrscheinlich wiederum zu einem Leistungspunkt.