Der Zweck der gleichzeitigen Programmierung besteht darin, das Programm schneller zu verzeichnen, aber die Verwendung von Parallelität kann das Programm möglicherweise nicht unbedingt schneller laufen lassen. Die Vorteile der gleichzeitigen Programmierung können nur reflektiert werden, wenn die Anzahl der gleichzeitigen Programme eine bestimmte Größenordnung erreicht. Daher ist es nur sinnvoll, über die gleichzeitige Programmierung zu sprechen, wenn eine hohe Parallelität vorliegt. Obwohl noch keine Programme mit hoher Parallelitätsvolumen entwickelt wurden, besteht das Lernen, einige verteilte Architekturen besser zu verstehen. Wenn das Parallelitätsvolumen des Programms nicht hoch ist, wie beispielsweise ein einsthreades Programm, ist die Ausführungseffizienz eines Einzel-Thread-Programms höher als das eines Multi-Thread-Programms. Warum ist das? Diejenigen, die mit dem Betriebssystem vertraut sind, sollten wissen, dass die CPU Multi-Threading implementiert, indem sie jedem Thread Zeitscheiben zuordnen. Auf diese Weise wird der Status der vorherigen Aufgabe gespeichert, wenn die CPU von einer Aufgabe zur anderen wechselt. Wenn die Aufgabe ausgeführt wird, wird die CPU weiterhin den Status der vorherigen Aufgabe ausführen. Dieser Prozess wird als Kontextschalter bezeichnet.
Bei Java Multithreading spielt das synchronisierte Schlüsselwort des volatilen Schlüsselworts eine wichtige Rolle. Sie können alle Threadsynchronisation implementieren, aber wie wird sie unten implementiert?
flüchtig
Flüchtigen können nur die Sichtbarkeit von Variablen für jeden Thread sicherstellen, kann jedoch nicht die Atomizität garantieren. Ich werde nicht viel darüber sagen, wie man die java -Sprache volatil verwendet. Mein Vorschlag ist, es in einer anderen Situation mit Ausnahme der Klassenbibliothek in Paket java.util.concurrent.atomic zu verwenden. Weitere Erläuterungen finden Sie in diesem Artikel.
Einführung
Siehe den folgenden Code
paket org.go; public class go {volatile int i = 0; private void inc () {i ++; } public static void main (String [] args) {Go go = new Go (); für (int i = 0; i <10; i ++) {neuer Thread (() -> {für (int j = 0; j <1000; j ++) go.inc ();}). start (); } while (thread.activeCount ()> 1) {thread.yield (); } System.out.println (go.i); }} Das Ergebnis jeder Ausführung des oben genannten Code ist unterschiedlich und die Ausgabennummer beträgt immer weniger als 10000. Dies liegt daran, dass I ++ bei der Durchführung von Inc () kein Atombetrieb ist. Möglicherweise würden einige Leute vorschlagen, synchronisiert zu synchronisieren Inc () oder die Lock unter Paket java.util.concurrent.locks zur Steuerung der Threadsynchronisation. Aber sie sind nicht so gut wie die folgenden Lösungen:
paket org.go; import java.util.concurrent private void inc () {i.getandIncrement (); } public static void main (String [] args) {Go go = new Go (); für (int i = 0; i <10; i ++) {neuer Thread (() -> {für (int j = 0; j <1000; j ++) go.inc ();}). start (); } while (thread.activeCount ()> 1) {thread.yield (); } System.out.println (go.i); }} Wenn Sie zu diesem Zeitpunkt die Implementierung von Atomic nicht verstehen, werden Sie auf jeden Fall vermuten, dass der zugrunde liegende Atomicinterer möglicherweise mit Schlösser implementiert wird, sodass er möglicherweise nicht effizient ist. Also, was genau ist, werfen wir einen Blick darauf.
Interne Implementierung von Atomklassen
Egal, ob es sich um eine Atomicinteger oder eine gleichzeitige Klassenklasse von gleichzeitiger Verbindung handelt.
Private Static Final Sun.Misc.unsafe unsicher; Diese Klasse ist eine Java -Kapselung von Sun :: misc :: unsicher, die atomare Semantik implementiert. Ich möchte die zugrunde liegende Implementierung sehen. Ich habe zufällig den Quellcode von GCC4.8 zur Hand. Im Vergleich zum lokalen Weg ist es sehr bequem, den Weg zum GitHub zu finden. Schau hier.
Nehmen Sie das Implementierungsbeispiel der Schnittstelle GetAndIncrement ()
Atomicinteger.java
private statische endgültige unsichere unsicher = unsicher.getUnsafe (); public Final int getandincrement () {for (;;) {int current = get (); int next = current + 1; if (vergleicheSet (aktuell, nächst) zurücksend; }} public Final Boolean Vergleiche (int erwart, int update) {return unafe.comPareAndswapint (this, ValueOffset, erwarten, update); } Achten Sie darauf für die Schleife, es wird nur zurückgegeben, wenn der Vergleichsset erfolgreich ist. Andernfalls wird es immer vergleichbar.
Die Implementierung des Vergleichs wird aufgerufen. Hier bemerkte ich, dass die Implementierung von Oracle JDK etwas anders ist. Wenn Sie sich die SRC unter JDK ansehen, können Sie sehen, dass Oracle JDK unsichere GetAndIncrement () nennt, aber ich glaube, wenn Oracle JDK unsicher.java implementiert, sollte es nur Vergleiche als Vergleiche aufrufen, da ein Vergleichssatz atomische Operationen implementieren kann, die zunehmend, abnehmen und Werte festlegen.
Unsicher.java
öffentliche native boolean vergleiche und wapint (Object OBJ, Long Offset, INT -Erwartung, int -Update);
Implementierung von C ++ durch JNI aufgerufen.
Natunsafe.cc
JBOOLEANSUN :: MISC :: UnSafe :: vergleicheSwapint (jobject obj, jlong offset, jint erwarten, jint update) {jint *addr = (jint *) ((char *) obj + offset); return vergleichErdswap (addr, erwarten, update);} statische Inline -BoolcompareAndswap (volatile jint *addr, jint alt, jint new_val) {jboolean result = false; Spinlock Lock; if ((result = ( *addr == alt))) *addr = new_val; Rückgabeergebnis;} Unsicher :: VergleicheDSwapint ruft die statische Funktion vergleichewap auf. Vergleichswap verwendet Spinlock als Lock. Das Spinlock hier hat die Bedeutung von LockGuard, das während der Bauarbeiten gesperrt und während der Zerstörung freigesetzt wird.
Wir müssen uns auf Spinlock konzentrieren. Hier wird sichergestellt, dass Spinlock eine echte Implementierung von Atomoperationen ist, bevor es veröffentlicht wird.
Was ist Spinlock?
Spinlock, eine Art beschäftigt, um das Schloss der Ressource zu erwerben. Im Gegensatz zu MUTEX blockiert das aktuelle Thread und die Freigabe von CPU-Ressourcen, um auf die erforderlichen Ressourcen zu warten. Spinlock tritt nicht in den Susprding ein, wartet nicht auf die Erfüllung der Bedingungen und ist die CPU erneut miteinander umsetzt. Dies bedeutet, dass Spinlock nur dann besser als Mutex ist, wenn die Kosten für das Warten auf das Schloss geringer sind als die Kosten für den Kontextschalter von Thread -Ausführung.
Natunsafe.cc
Klasse spinlock {static volatile obj_addr_t lock; public: spinlock () {while (! compare_and_swap (& lock, 0, 1)) _jv_Theadyield (); } ~ spinlock () {release_set (& lock, 0); }}; Verwenden Sie eine statische variable statische flüchtige obj_addr_t lock; Als Flaggenbit wird eine Wache durch C ++ Raii implementiert, sodass das sogenannte Schloss tatsächlich die statische Mitgliedsvariable OBJ_ADDR_T-Sperre ist. Das Volatil in C ++ kann die Synchronisation nicht garantieren. Was garantiert ist, ist das im Konstruktor aufgerufene Compare_And_Swap und ein statisches Variable -Sperre. Wenn diese Sperrvariable 1 ist, müssen Sie warten. Wenn es 0 ist, setzen Sie es durch den Atombetrieb auf 1, was darauf hinweist, dass Sie das Schloss erhalten haben.
Es ist wirklich ein Unfall, hier eine statische Variable zu verwenden, was bedeutet, dass alle schlossfreien Strukturen dieselbe Variable (eigentlich size_t) haben, um zu unterscheiden, ob ein Schloss hinzugefügt werden soll. Wenn diese Variable auf 1 gesetzt ist, muss ein anderes Spinlock gewartet werden. Warum fügen Sie nicht eine private variable, volatile OBJ_ADDR_T FLOCK in Sun :: Misc :: Unsicher hinzu und geben Sie es als Konstruktorparameter an Spinlock weiter? Dies entspricht dem Teilen eines Flaggenbits für jede Unsicherheit. Wird der Effekt besser sein?
_JV_Threadyield In der folgenden Datei wird die CPU -Ressource über das System Call ender_yield (Man 2 pled_yield) aufgegeben. Das Makro hat die Konfiguration definiert. Wenn die Definition während der Kompilierung nicht definiert ist, wird Spinlock als True Spin Lock bezeichnet.
pox-threads.h
inline void_jv_theadyield (void) {#ifdef HABE_SCHED_YIELD SCHLAD_YIELD ();#endif / * Have_Sched_yield * /} Diese Lock.h verfügt über unterschiedliche Implementierungen auf verschiedenen Plattformen. Wir nehmen die IA64 -Plattform (Intel AMD X64) als Beispiel. Andere Implementierungen sind hier zu sehen.
IA64/Locks.H
typedef size_t obj_addr_t; inline static boolcompare_and_swap (volatile obj_addr_t *addr, obj_addr_t Old, obj_addr_t new_val) {return __sync_bool_compare_and_sswap (addr, alte, alte, new, new, new_val); OBJ_ADDR_T *ADDR, OBJ_ADDR_T NEW_VAL) {__asm__ __volatile __ ("" ::: "memory"); *(addr) = new_val;}__sync_bool_compare_and_swap ist eine integrierte GCC-Funktion, und die Montage "Speicher" vervollständigt die Speicherbarriere.
Kurz gesagt, die Hardware sorgt für eine Multi-Core-CPU-Synchronisation, und die Implementierung von Unsicherheit ist so effizient wie möglich. GCC-Java ist ziemlich effizient, ich glaube, Oracle und OpenJDK werden nicht schlechter sein.
Atomoperationen und GCC-integrierte Atomoperationen
Atomoperation
Java -Ausdrücke und C ++ - Ausdrücke sind keine atomaren Operationen, was bedeutet, dass Sie sich im Code befinden:
// Angenommen, ich bin eine Variable i ++, die zwischen Threads geteilt wird;
In einer multitHhread-Umgebung ist ich nicht nach und ist nichtatomar und ist tatsächlich in die folgenden drei Operanden unterteilt:
Der Compiler ändert den Zeitpunkt der Ausführung, sodass das Ausführungsergebnis möglicherweise nicht zu erwarten ist.
GCC eingebauter Atombetrieb
GCC verfügt über integrierte Atomoperationen, die von 4,1,2 hinzugefügt wurden. Zuvor wurden sie mit der Inline -Montage implementiert.
type __sync_fetch_and_add (type *ptr, type value, ...)type __sync_fetch_and_sub (type *ptr, type value, ...)type __sync_fetch_and_or (type *ptr, type value, ...)type __sync_fetch_and_or (type *ptr, type value, ...)type __sync_fetch_and_and (type *ptr, type value, ...)type __sync_fetch_and_xor (type *ptr, type value, ...)type __sync_fetch_and_nand (type *ptr, type value, ...)type __sync_add_and_fetch (type *ptr, type value, ...)type __sync_sub_and_fetch (type *ptr, type value, ...)type __sync_or_and_fetch (type *ptr, type value, ...)type __sync_and_and_fetch (type *ptr, type value, ...)type __sync_and_and_fetch (type *ptr, type value, ...)type __sync_xor_and_fetch (type *ptr, type value, ...)type __sync_nand_and_fetch (type *ptr, type value, ...)bool __sync_bool_compare_and_swap (type *ptr, Typ OldVal Type Newval, ...) Typ __sync_val_compare_and_swap (type *ptr, Typ Oldval Type Newval, ...) __ Sync_Synchronize (...) Typ __sync_lock_test_andtest_andest_andest_andest_andest_andest_andest_andest_andest_andest_andest_andest_andest_andest_andest_andest_andest_ und type_test_ und type_ und type (type, type, type, type __sync_lock_release (Typ *ptr, ...)
Was beachtet werden sollte ist:
OpenJDK -verwandte Dateien
Im Folgenden finden Sie einige atomare Implementierungen von OpenJDK9 auf GitHub, in der Hoffnung, denjenigen zu helfen, die es wissen müssen. Immerhin wird OpenJDK häufiger als GCC verwendet. - - Aber schließlich gibt es keinen Quellcode für Oracle JDK, obwohl der Quellcode zwischen OpenJDK und Oracle sehr klein ist.
Atomicinteger.java
Unsicher.java:: compareandExchangeObject
unsicher.cpp :: unafe_compareandExchangeObject
oop.inline.hpp :: oopdesc :: atomic_compare_exchange_oop
atomic_linux_x86.hpp :: atomic :: cmpxchg
Inline Jlong Atomic :: cmpxchg (jlong Exchange_value, volatile jlong* dest, jlong compare_value, cmmpxchg_memory_order order) {bool mp = os :: is_mp (); __asm__ __volatile__ (lock_if_mp (%4) "cmpxchgq%1, (%3)": "= a" (Exchange_value): "R" (Exchange_value), "a" (compare_value), "R" (dest), "r" (MP): "cc"); return Exchange_Value;} Hier müssen wir Java -Programmierern, die mit C/C ++ nicht vertraut sind, eine Aufforderung geben. Das Format der Anweisungen der eingebetteten Montage ist wie folgt
__asm__ [__volatile __] (Montagevorlage // Assemblervorlage: [Ausgabe von Operandenliste] // Eingaberliste: [List der Operanden eingeben] // Ausgabeliste: [Clobber -Liste]) // LISTE ZETREIBEN
%1, %3, %4 in der Montagevorlage entspricht der folgenden Parameterliste {"R" (Exchange_value), "R" (dest), "r" (mp)} und die Parameterliste ist durch Kommas getrennt und sortiert von 0. Der Ausgangsparameter ist auf der rechten Seite des ersten Kolones sortiert, und der Ausgaberameter ist auf der rechten Dickdarmdicke auf der rechten Seite des Colons platziert. "R" bedeutet, in ein allgemeines Register eingebracht zu werden, "A" bedeutet Register EAX und "=" bedeutet Ausgabe (zurückschreiben). Die CMPXCHG -Anweisung impliziert die Verwendung von EAX -Registern, dh Parameter %2.
Weitere Details werden hier nicht aufgeführt. Die Implementierung von GCC besteht darin, den ausgetauschten Zeiger weiterzugeben, und nach erfolgreichem Vergleich wird der Wert direkt zugeordnet (Nicht-Atomic), und die Atomizität wird durch Spinlock garantiert.
Die Implementierung von OpenJDK besteht darin, den ausgetauschten Zeiger weiterzugeben und die Werte direkt über die Montageanweisung cmmpxchgq zuzuweisen, und die Atomizität wird durch die Montageanweisung garantiert. Natürlich wird die zugrunde liegende Schicht von GCCs Spinlock auch durch CMMPXchgq garantiert.
Das obige ist der gesamte Inhalt dieses Artikels. Ich hoffe, es wird für das Lernen aller hilfreich sein und ich hoffe, jeder wird Wulin.com mehr unterstützen.