Le but de la programmation simultanée est de faire fonctionner le programme plus rapidement, mais l'utilisation de la concurrence ne peut pas nécessairement faire fonctionner le programme plus rapidement. Les avantages de la programmation simultanée ne peuvent être reflétés que lorsque le nombre de programmes simultanés atteint un certain ordre de grandeur. Par conséquent, il est seulement significatif de parler de programmation simultanée lorsqu'il y a une concurrence élevée. Bien qu'aucun programme avec un volume de concurrence élevé n'a encore été développé, l'apprentissage de la concurrence consiste à mieux comprendre certaines architectures distribuées. Ensuite, lorsque le volume de concurrence du programme n'est pas élevé, comme un programme unique, l'efficacité d'exécution d'un seul file d'attente est plus élevée que celle d'un programme multithread. Pourquoi est-ce? Ceux qui connaissent le système d'exploitation doivent savoir que le CPU implémente le multi-lancement en allouant des tranches de temps à chaque thread. De cette façon, lorsque le CPU passe d'une tâche à une autre, l'état de la tâche précédente sera enregistré. Lorsque la tâche est exécutée, le CPU continuera d'exécuter l'état de la tâche précédente. Ce processus est appelé commutation de contexte.
Dans Java Multithreading, le mot-clé synchronisé du mot-clé volatil joue un rôle important. Ils peuvent tous implémenter la synchronisation des threads, mais comment est-il implémenté en bas?
volatil
Volatile ne peut assurer la visibilité des variables de chaque fil, mais ne peut garantir l'atomicité. Je ne dirai pas grand-chose sur la façon d'utiliser la langue java volatile. Ma suggestion est de l'utiliser dans toute autre situation, à l'exception de la bibliothèque de classe dans Package Java.util.concurrent.atomic. Voir cet article pour plus d'explications.
Introduction
Voir le code suivant
package org.go; classe publique Go {volatile int i = 0; private void inc () {i ++; } public static void main (string [] args) {go go = new go (); for (int i = 0; i <10; i ++) {new Thread (() -> {for (int j = 0; j <1000; j ++) go.inc ();}). start (); } while (thread.activeCount ()> 1) {thread.yield (); } System.out.println (go.i); }} Le résultat de chaque exécution du code ci-dessus est différent, et le nombre de sortie est toujours inférieur à 10000. En effet, lors de la performance inc (), I ++ n'est pas une opération atomique. Peut-être que certaines personnes suggéreraient d'utiliser synchronisé pour synchroniser Inc () ou d'utiliser le verrou sous package java.util.concurrent.locks pour contrôler la synchronisation du thread. Mais ils ne sont pas aussi bons que les solutions suivantes:
package org.go; import java.util.concurrent.atomic.atomicInteger; public class go {atomicInteger i = new atomicInteger (0); private void inc () {i.getandIncrement (); } public static void main (string [] args) {go go = new go (); for (int i = 0; i <10; i ++) {new Thread (() -> {for (int j = 0; j <1000; j ++) go.inc ();}). start (); } while (thread.activeCount ()> 1) {thread.yield (); } System.out.println (go.i); }} Pour le moment, si vous ne comprenez pas la mise en œuvre d'Atomic, vous soupçonnerez certainement que l'AtomicInteger sous-jacent peut être mis en œuvre à l'aide de verrous, de sorte qu'il peut ne pas être efficace. Alors qu'est-ce que c'est exactement, jetons un coup d'œil.
Implémentation interne des classes atomiques
Qu'il s'agisse d'atomicinteger ou de classe de nœud concurrentLinkedQueue, concurrentLinkEdQueue.Node, ils ont une variable statique
Private Static Final Sun. Je veux voir l'implémentation sous-jacente. Il se trouve que le code source de GCC4.8 est à portée de main. Par rapport au chemin local, il est très pratique de trouver le chemin de Github. Regardez ici.
Prenez l'exemple d'implémentation de l'interface getandincrement ()
AtomicInteger.java
Final statique privé UnsAve Usare = Unsenet.getUnSafe (); public final int GetandIncrement () {for (;;) {int current = get (); int next = courant + 1; if (comparabledset (courant, suivant)) Retour courant; }} public final boolean compaReANDSet (int attende, int update) {return usAve.compathendswapint (this, valuoffset, attend, update); } Faites attention à cela pour Loop, il ne reviendra que si ComparandDset réussit. Sinon, il sera toujours comparé.
L'implémentation de comparaison est appelée. Ici, j'ai remarqué que l'implémentation d'Oracle JDK est légèrement différente. Si vous regardez le SRC sous JDK, vous pouvez voir qu'Oracle JDK appelle dangetafe GetandIncrement (), mais je crois que lorsque Oracle JDK implémente dangetafe.java, il ne devrait appeler la comparaison et la définition des valeurs.
Dis. Java
Public Native Boolean CompareAndSwapInt (objet obj, long offset, int attend, int metted);
Implémentation de C ++ appelé via JNI.
natunsafe.cc
Jbooleansun :: Misc :: Unsene :: CompareAndSwapint (Jobject obj, jlong offset, jint attend, jint update) {jint * addr = (jint *) ((char *) obj + offset); return ComparandDswap (addr, attendre, mise à jour);} statique BoolCompareAndSwap (volatile jint * addr, jint old, jint new_val) {jboolean result = false; Spinlock Lock; if ((result = (* addr == old))) * addr = new_val; Retour Résultat;} Disap :: CompareAndSwapint appelle la fonction statique ComparendSwap. ComparandSwap utilise Spinlock comme verrouillage. Le Spinlock ici a le sens de Lockguard, qui est verrouillé pendant la construction et libéré pendant la destruction.
Nous devons nous concentrer sur Spinlock. Voici pour s'assurer que Spinlock est une véritable implémentation des opérations atomiques avant sa publication.
Qu'est-ce que Spinlock
Spinlock, une sorte d'occupation d'attente pour acquérir la serrure de la ressource. Contrairement à Mutex, le blocage du thread actuel et la libération des ressources du processeur pour attendre les ressources requises, Spinlock n'entrera pas le processus de suspension, en attendant que les conditions soient remplies et récompenseront le CPU. Cela signifie que Spinlock est meilleur que Mutex uniquement si le coût de l'attente du verrou est inférieur au coût du commutateur de contexte d'exécution de thread.
natunsafe.cc
classe Spinlock {STATIC VOLATILE OBJ_ADDR_T LOCK; public: Spinlock () {while (! Compare_and_swap (& Lock, 0, 1)) _jv_Threadield (); } ~ spinlock () {release_set (& lock, 0); }}; Utilisez une variable statique statique Volatile OBJ_ADDR_T LOCK; En tant que bit de drapeau, un gardien est implémenté via C ++ RAII, donc le soi-disant verrouillage est en fait le verrou de la variable de membre statique OBJ_ADDR_T. Le volatil en C ++ ne peut garantir la synchronisation. Ce qui est garanti, c'est le compare_and_swap appelé dans le constructeur et un verrou de variable statique. Lorsque cette variable de verrouillage est 1, vous devez attendre; Lorsqu'il est 0, vous le réglez à 1 à l'opération atomique, indiquant que vous avez obtenu la serrure.
C'est vraiment un accident d'utiliser une variable statique ici, ce qui signifie que toutes les structures sans verrouillage partagent la même variable (en fait size_t) pour distinguer s'il faut ajouter une serrure. Lorsque cette variable est définie sur 1, d'autres spinlock doivent être attendues. Pourquoi ne pas ajouter une variable privée Volatile OBJ_ADDR_T LOCK IN SUN :: Misc :: Discu et la transmettre à Spinlock en tant que paramètre du constructeur? Cela équivaut à partager un bit de drapeau pour chaque dangereuse. L'effet sera-t-il meilleur?
_JV_THRADEYIELD Dans le fichier suivant, la ressource CPU est abandonnée via l'appel système Sched_Ield (man 2 sched_yield). La macro haricède_sched_yield est définie dans Configure, ce qui signifie que si la définition n'est pas définie pendant la compilation, Spinlock est appelé le vrai verrouillage de spin.
posix-threads.h
inline void_jv_threadield (void) {# ifdef have_sched_yield sched_yield (); # endif / * have_sched_yield * /} Ce Lock.h a différentes implémentations sur différentes plates-formes. Nous prenons l'exemple de la plate-forme IA64 (Intel AMD X64). D'autres implémentations peuvent être vues ici.
ia64 / locks.h
typedef size_t obj_addr_t; en ligne static boolcompare_and_swap (volatile obj_addr_t * addr, obj_addr_t old, obj_addr_t new_val) {return __sync_bool_compare_and_swap (addition obj_addr_t * addr, obj_addr_t new_val) {__asm__ __volatile __ ("" ::: "mémoire"); * (addr) = new_val;}__SYNC_BOOL_COMPARE_AND_SWAP est une fonction GCC intégrée, et l'instruction d'assemblage "Memory" complète la barrière de mémoire.
En bref, le matériel garantit une synchronisation multi-core CPU et l'implémentation de dangereuse est aussi efficace que possible. GCC-Java est assez efficace, je crois qu'Oracle et OpenJDK ne seront pas pires.
Opérations atomiques et opérations atomiques intégrées GCC
Opération atomique
Les expressions Java et les expressions C ++ ne sont pas des opérations atomiques, ce qui signifie que vous êtes dans le code:
// Supposons que I soit une variable i ++ partagée entre les threads;
Dans un environnement multithread, j'accéder à l'accès non atomique et est en fait divisé en trois opérandes suivants:
Le compilateur modifie le moment de l'exécution, de sorte que le résultat d'exécution peut ne pas être attendu.
Opération atomique intégrée du GCC
GCC a des opérations atomiques intégrées, qui ont été ajoutées à partir de 4.1.2. Avant, ils ont été mis en œuvre en utilisant l'assemblage en ligne.
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, ...) Valeur, ...) 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 * ptr, Type Value, ...) Type __Sync_and_and_fetch (Type * ptr, Type Value, ...) Type __SYNC_AND_AND_FETCH (Type * ptr, Type Value, ...) Type __SYNC_NAND_AND_FETCH (Type * PTR, ...) __SYNC_BOOL_COMPARE_AND_SWAP (Type * PTR, Type OldVal Type NewVal, ...) Type __Sync_Val_Compare_and_Sswap (Type * PTR, Type OldVal Type NewVal __Sync_lock_release (Type * ptr, ...)
Ce qui doit être noté est:
Fichiers liés à OpenJDK
Vous trouverez ci-dessous quelques implémentations de l'opération atomique d'OpenJDK9 sur GitHub, dans l'espoir d'aider ceux qui ont besoin de savoir. Après tout, OpenJDK est plus largement utilisé que GCC. `` - mais après tout, il n'y a pas de code source pour Oracle JDK, bien qu'il soit dit que le code source entre OpenJDK et Oracle est très petit.
AtomicInteger.java
Disap.java:: CompareandexchangeObject
unsetafe.cpp :: unpae_compareandexchangeObject
oop.inline.hpp :: oopdesc :: atomic_compare_exchange_oop
atomic_linux_x86.hpp :: atomic :: cmpxchg
en ligne jlong atomic :: cmpxchg (jlong exchange_value, volatile jlong * dest, jlong compare_value, cmmpxchg_memory_order ordre) {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", "Memory"); return Exchange_value;} Ici, nous devons donner une invite aux programmeurs Java qui ne connaissent pas C / C ++. Le format d'instructions d'assemblage intégrées est la suivante
__asm__ [__volatile __] (Template d'assemblage // Template d'assemblage: [Liste de l'opérande de sortie] // Liste des entrées: [Liste de l'opérande d'entrée] // Liste de sortie: [Liste des Clobber]) // Liste de détruire
% 1,% 3,% 4 dans le modèle d'assemblage correspond à la liste des paramètres suivants {"r" (Exchange_value), "r" (dest), "r" (MP)}, et la liste des paramètres est séparée par des virgules et triée de 0. Le paramètre de sortie est placé à droite du premier côlon, et le paramètre de sortie est placé à droite du deuxième côlon. "R" signifie placer dans un registre général, "A" signifie enregistrer EAX et "=" signifie la sortie (réécrire). L'instruction CMPXCHG implique l'utilisation de registres EAX, c'est-à-dire le paramètre% 2.
D'autres détails ne seront pas répertoriés ici. L'implémentation de GCC consiste à transmettre le pointeur à échanger, et après une comparaison réussie, la valeur est attribuée directement (attribuant non atomique) et l'atomicité est garantie par Spinlock.
L'implémentation d'OpenJDK consiste à transmettre le pointeur à échanger et à attribuer directement des valeurs via l'instruction d'assemblage CMMPXCHGQ, et l'atomicité est garantie par l'instruction d'assemblage. Bien sûr, la couche sous-jacente de Spinlock de GCC est également garantie via CMMPXCHGQ.
Ce qui précède est tout le contenu de cet article. J'espère que cela sera utile à l'apprentissage de tous et j'espère que tout le monde soutiendra davantage Wulin.com.