Résumé de la théorie de la concurrence JAVA

1. La source des bogues de programmation simultanés (visibilité, atomicité, ordre)

Les vitesses d'accès du processeur, de la mémoire et des périphériques d'E / S varient considérablement. Afin d'améliorer l'utilisation des performances de l'ordinateur, l'ordinateur a effectué les trois étapes suivantes:
1. Le processeur augmente le cache et équilibre la différence avec la mémoire.
2. Le système d'exploitation ajoute des processus, des threads et des processeurs multiplexés à temps partagé pour équilibrer la différence de vitesse entre les processeurs et les périphériques d'E / S.
3. Le compilateur optimise l'ordre d'exécution des instructions afin que le cache puisse être utilisé de manière plus raisonnable.Cela pose également trois problèmes pour les programmes concurrents: la visibilité, l'atomicité et l'ordre.

Visibilité: la modification des variables partagées par un thread peut être vue
par un autre thread. De nos jours, les ordinateurs sont à l'ère du multicœur. Chaque processeur a son propre cache, il y a donc un problème d'incohérence des données avec la mémoire. Lorsque le thread A change variables sur CPU1 Mettez-le dans le cache pour +1. En même temps, le thread B lit la variable dans le cache pour +1 sur CPU2. Notre espérance est la variable +2, mais le résultat final est la variable +1. Il s'agit d'un bug causé par le fait de ne pas tenir compte de la visibilité.

Atomicité: La fonction qu'une ou plusieurs opérations ne sont pas interrompues dans le CPU.
Le système d'exploitation comporte plusieurs threads et prend en charge le multiplexage en temps partagé. Un processus exécute une tranche de temps sur le processeur, la tranche de temps atteint un point, enregistre des données et change de thread. Cela pose le problème de l'atomicité. Le thread A lit la variable dans le cache, mais à ce moment le processeur change de mémoire, le thread A est bloqué, le thread B lit la variable et modifie la valeur de la variable, puis réveille le thread A. Le thread A ne sait pas que le variable a été modifiée. Continuez à modifier l'opération de la variable, un bug apparaît.

Ordre: le code du programme est exécuté selon l'ordre du
code. Afin d'améliorer les performances, l'ordre d'exécution du code sera parfois modifié lors de l'optimisation du code. Dans un seul thread, cela peut ne pas poser de problème, mais en cas de concurrence, cela peut causer Venez à la question. Par exemple, le thread A crée un objet. Dans notre compréhension, la nouvelle opération de l'objet est: 1 alloue une mémoire M, 2 initialise l'objet dans M et 3 attribue l'adresse de M à la variable, mais l'ordre deviendra : 1 allocation après optimisation par le compilateur d'une mémoire M, 2 attribue l'adresse de M à la variable, 3 initialise l'objet dans M. Si le thread A termine la deuxième étape et passe au thread B, le thread B voit que l'objet a été créé (en fait affecte simplement une adresse), effectue des opérations sur l'objet et un BUG apparaît. (Il peut y avoir des doutes sur l'ordre et l'atomicité. Si le compilateur n'est pas optimisé, le thread A exécute la deuxième étape, et la variable ne reçoit toujours pas d'adresse, même si elle est commutée sur le thread B, la variable ne sera pas exploitée )

2. Modèle de mémoire Java (résolution de la visibilité et de l’ordre)

La visibilité est un problème causé par la mise en cache, et l'ordre est un problème causé par l'optimisation de la compilation. La façon de les résoudre est de les désactiver, mais cela dégradera les performances et perdra la signification de la concurrence. Cela nécessite un modèle de mémoire

Le modèle de mémoire Java est quelques spécifications très complexes. En termes simples, il spécifie comment la JVM désactive la mise en cache et l'optimisation de la compilation à la demande. Ces méthodes sont violatiles, synchronisées et définitives, et six spécifications Happens-Before.

Synchronized peut modifier les blocs de code et les méthodes. (Théorie) La
variable finale modifiée est une constante, ce qui permet au compilateur de l'optimiser autant que possible. Après 1.5, tant que nous fournissons le bon constructeur sans provoquer "d'échappement", il n'y aura aucun problème avec la constante finale.
(Échapper: donnez l'objet à quelqu'un d'autre avant que l'initialisation du constructeur ne soit terminée)

La valeur de la variable modifiée par violet est la mise en cache désactivée, c'est-à-dire que la modification de la variable ne peut se faire qu'à partir du niveau mémoire. Mais cela posera aussi un problème: je ne peux pas utiliser la modification violette pour toutes les variables, ce qui perdra le sens de la mise en cache.
Dans le même temps, la variable modifiée par violation apportera également un problème qui est le problème de visibilité. (Cela ne signifie pas que la variable modifiée a des problèmes de visibilité).
Pour les programmeurs, une partie importante du modèle de mémoire est la spécification Happens-Before, qui concerne la visibilité. Séquentialité du programme
Happens-Before
La
modification d'une variable dans un thread est visible par le code suivant. v peut "voir" x = 3.
2. Règles de variables
volatiles Les opérations de lecture volatiles peuvent voir les opérations d'écriture précédentes sur cette variable.
3. Transitivité
Si A est visible par B et B est visible par C, alors A est visible par C. Le
code ci-dessus est visible , x est visible par v, et l'opération d'écriture de v dans write () est visible à la lecture opération de v dans reader (), puis reader (L'opération de lecture de v in) peut voir la valeur de x, c'est-à-dire lire vQuand c'est vrai, on peut voir x3. Que ce soit dans le cache ou non. Ceci est amélioré pour vioatile.
4. Les règles
du verrou dans le moniteur. L'opération de déverrouillage d'un verrou est vue par l'opération de verrouillage du même
verrou qu'un objet. Nous modifions la valeur du verrou dans le thread A, puis lorsque le thread B également va pour obtenir le verrou, On voit que la valeur de ce verrou a été modifiée.
5. Règle de début de thread () Le
thread A démarre le thread B en utilisant B.start (), puis après l'exécution de B démarre, B peut voir l'opération avant que le thread A ne démarre B.
6. Règle Thread join (). Le
thread A attend que le thread B ait fini d'utiliser B.join (). Lorsque le thread A est réveillé, vous pouvez voir le fonctionnement du thread B.
La spécification Happens-Before résout le problème de la visibilité.
Résolution synchronisée, finale et violatile ordonnée.

Trois, verrouillage d'exclusion mutuelle (résoudre l'atomicité)

Le principe est que le CPU change de thread. Si l'on s'assure qu'un thread n'est pas interrompu, le problème d'atomicité sera-t-il résolu? Évidemment non. C'est maintenant l'ère du multicœur. Pour garantir l'atomicité, nous devons nous assurer qu'un seul thread exécute un morceau de code à la fois. Il s'agit d'une exclusion mutuelle. Si nous pouvons garantir que les opérations de modification sur les variables partagées sont mutuellement exclusives, alors l'atomicité peut être garantie.

Le bloc de code qui doit être exécuté mutuellement exclusif devient la section critique, et le thread entrant dans la section critique doit occuper le verrou mutex correspondant à la section critique. Le verrou que nous choisissons doit avoir une relation correspondante avec la section critique. Dans le même temps, une zone critique est verrouillée à une serrure, tout comme un bon billet de siège de jeu, un siège avec une serrure, sinon il deviendra un capitaliste au cœur noir (bien sûr, des serrures imbriquées sont possibles). Un verrou peut verrouiller plusieurs ressources, mais les performances diminueront, en pensant que l'exécution du code distribué deviendra série, et utilisera différents verrous pour gérer différentes ressources afin d'obtenir une gestion fine. Ce type de verrou est appelé verrou à granularité fine .

Lorsque les ressources verrouillées sont importantes et liées, il se peut que nous devions choisir un verrou avec une granularité de verrouillage plus élevée.
(L'essence de l'atomicité est que l'état intermédiaire de l'opération est invisible pour le monde extérieur)

Il est préférable que le verrou soit constant, car si la valeur du verrou change, cela signifie également que le verrou a changé.
(L'essence du verrouillage est d'écrire l'ID de thread actuel dans l'en-tête d'objet de l'objet de verrouillage, de sorte que l'objet de verrouillage ne peut pas être modifié)

Java fournit une technologie de verrouillage de support synchronisé. Synchronized prend en charge les méthodes de verrouillage et les blocs de code. Une fois la
méthode modifiée par synchronized, elle deviendra mutuellement exclusive, c'est-à-dire qu'un seul thread peut utiliser cette méthode à la fois, quel est le verrou de cette méthode?
S'il s'agit d'un statique, le verrou est la classe actuelle. L'objet Class.
S'il s'agit d'une méthode non statique, le verrou est l'objet d'instance actuel this.
(Différents objets du même type peuvent mobiliser des méthodes d'exclusion mutuelle en même temps, car différents objets signifient également des verrous différents)

Quatrièmement, le problème de l'impasse

Introduit le mécanisme de verrouillage, il y aura alors des problèmes de blocage.

Nous avons mentionné précédemment que les verrous avec une granularité de verrouillage plus élevée ont des performances médiocres, mais l'atomicité doit être garantie. Ensuite, nous ajoutons différents verrous à différentes ressources dans la section critique, des verrous imbriqués, mais cela peut également causer des problèmes de blocage, tels que l'ordre de verrouillage est AB, mais les autres parties de l'ordre de verrouillage de code sont BA, alors si ces deux Si un une partie du code est exécutée en même temps, il se bloquera.

Blocage: un groupe de threads en compétition pour les ressources s'attend, conduisant à un phénomène de "blocage permanent".
Conditions de blocage: (en même temps)
exclusion mutuelle: les ressources partagées X et Y ne peuvent être occupées que par un thread.
Occuper et attendre: le thread T1 a acquis la ressource partagée X, et en attendant la ressource partagée Y, il ne libère pas la ressource partagée X.
Non-préemption: les autres threads ne peuvent pas préempter de force les ressources occupées par le thread T1.
Attente circulaire: le thread T1 attend la ressource occupée par le thread T2 et le thread T2 attend la ressource occupée par le thread T1, appelé attente circulaire.

Briser l'une des conditions peut sortir de l'impasse.

  1. Pour la condition «occuper et attendre», nous pouvons demander toutes les ressources à la fois, de sorte qu'il n'y ait pas d'attente.
  2. En ce qui concerne la condition de "non-préemption", lorsqu'un thread occupant certaines ressources s'applique en outre à d'autres ressources, s'il ne s'applique pas, il peut libérer activement les ressources qu'il occupe, de sorte que la condition de non-préemption sera détruite.
    (Le verrou fourni dans le package java.util.concurrent de Java)
  3. Pour la condition de "boucle en attente", elle peut être évitée en demandant des ressources dans l'ordre. L'application dite séquentielle signifie que les ressources sont dans un ordre linéaire. Lors de l'application, vous pouvez d'abord demander la ressource avec le numéro de série le plus petit, puis demander la ressource avec le numéro de série le plus grand, de sorte qu'il n'y ait pas de cycle. après linéarisation.

Nous avons donné précédemment un exemple du problème du verrouillage dans l'ordre AB et BA Selon le troisième élément de destruction, tant que nous choisissons un standard, l'ordre de verrouillage est fixe. C'est-à-dire que les deux parties de l'application de ressources A et B doivent être exécutées dans l'ordre A-> B, et il n'y a pas de boucle en attente.

Cinq, coopération de fil: notification d'attente

Dans le 1 qui détruit la condition de blocage, s'applique à toutes les ressources. Beaucoup de gens peuvent penser que le cycle est utilisé jusqu'à ce que toutes les ressources soient obtenues. Cependant, cette méthode consomme trop de ressources CPU. En fait, nous pouvons utiliser le producteur-consommateur familier modèle pour penser Get, attendre-notifier.

Notification en attente: le thread A acquiert d'abord le mutex, lorsque la condition n'est pas remplie, libère le mutex et entre dans la file d'attente; d'autres threads acquièrent le mutex, lorsque les conditions requises par le thread A sont remplies, d'autres threads réveillent le thread A, le thread A attend à nouveau le mutex.

La réalisation de l'attente-notification, synchronisée, attente, notification, notification fournie par Java permet de réaliser ce mécanisme (l'implémentation spécifique ne sera pas introduite)

Essayez d'utiliser notifyall pour éviter que certains threads ne soient jamais notifiés.
Il existe des ressources A, B, C, D, le thread 1 occupe AB, le thread 2 occupe le CD, le thread 3 s'applique à AB, entre en attente, le thread 4 s'applique au CD, entre en attente, le thread 1 termine l'exécution et le réveille. Si notifier est utilisé, l'un est sélectionné au hasard. Exécution du thread, si le thread 4 est sélectionné, le thread 4 n'a pas besoin d'AB, et continue d'attendre, alors le thread 3 n'aura plus la possibilité de se réveiller.

(Attendez va libérer le verrou, le sommeil ne le sera pas)

Nous avons rencontré de nombreux problèmes de thread, qui sont divisés en trois types: problèmes de sécurité, de vivacité et de performances.
Sécurité: la visibilité du code, l'atomicité et l'ordre que nous avons mentionnés précédemment. Tous les codes n'ont pas à déterminer s'ils respectent ces trois points, sinon la charge de travail montera en flèche. Ce n'est que lorsqu'il y a des données partagées et que les données changent, c'est-à-dire lorsque plusieurs threads lisent et modifient les mêmes données, ces trois points doivent être pris en compte.

    多个线程同时访问同一数据会发生两个问题,数据竞争,竞态执行。
    数据竞争我们可以采取加锁的方法,但不能避免所有问题,还有竞态执行,指的是程序的执行结果依赖线程的执行顺序(比拼运气)。两个线程完全同时执行(避开了简单加锁),那么两者的结果就只能指望它们最后的执行顺序,这明显不具有稳定性。因此,我们需要更加安全的加锁模式。

Vivacité: fait référence à certaines opérations qui ne peuvent pas être exécutées et qui ne sont pas "actives", comme le blocage mentionné précédemment, ainsi que le blocage de la vie et la famine.
Si l'impasse peut être vue comme deux personnes avides qui se battent constamment et ne veulent pas lâcher prise, alors le gâchis est que deux personnes humbles se donnent humblement et que deux fils continuent à "humblement" alors personne n'a de ressources à la fin.
La solution à livelock est également très simple, c'est-à-dire que pour définir une heure aléatoire, le thread autorise le temps aléatoire, puis essaie d'occuper des ressources.
La famine fait référence au fait que le thread n'a pas pu continuer car il ne peut pas accéder aux ressources requises. 1. Pour garantir des ressources suffisantes, 2 pour allouer des ressources équitablement et 3 pour éviter que les threads de longue durée ne détiennent des verrous. Vous pouvez utiliser des serrures équitables, c'est-à-dire des serrures premier arrivé, premier arrivé.
Problèmes de performances: le concept de verrous semble passer par la programmation simultanée, mais il entraîne également des problèmes de performances. Une utilisation excessive des écluses entraînera la transformation de Sichuan Airlines et entraînera des problèmes de performances. Le package d'accès concurrentiel JavaSDK résout les problèmes de performances dans certains domaines simultanés. 1. Utilisez des algorithmes et des structures de données sans verrouillage (stockage local des threads, copie sur écriture, verrouillage optimiste), 2. Réduisez le temps de maintien du verrou (ConcurrentHashMap dans le package simultané Java, verrou en lecture-écriture)
Norme de mesure des performances: débit, délai , Concurrence.

Six processus de contrôle de clé principale simultané

La méthode du système d'exploitation pour résoudre le problème de concurrence est le sémaphore, et Java choisit le moniteur qui est le plus facile à faire face à l'objet.
Le passage du tube et le sémaphore sont équivalents, le sémaphore peut réaliser le passage du tube, et le passage du tube peut également réaliser le sémaphore.

La gestion fait référence au processus opérationnel de gestion des variables partagées. (La gestion est en mode utilisateur dans le système d'exploitation)
Modèle de gestion MESA.
Le modèle MESA résout le problème de l'exclusion mutuelle: encapsule les variables partagées et leurs opérations sur les variables partagées de manière unifiée. Seules quelques méthodes sont fournies et des verrous sont ajoutés à ces méthodes. Les threads accèdent mutuellement au contenu des variables partagées via Ces méthodes. Semblable à l'encapsulation orientée objet, cela peut être la raison pour laquelle Java choisit de surveiller.

Le modèle MESA résout le problème de synchronisation: le thread obtient le verrou de l'entrée et entre dans le moniteur. Lorsque la condition de demande du thread A n'est pas remplie, le thread A entre dans la file d'attente de la variable de condition et libère le verrou d'entrée. Thread B entre dans le moniteur. Si la variable de condition du thread A est satisfaite, puis réveillez le thread dans la file d'attente des variables, le thread A quitte le moniteur et attend à nouveau le verrouillage d'entrée. Le thread A retourne au moniteur. Pendant ce laps de temps, les conditions requises par le thread A ne sont pas nécessairement remplies, et le thread A commencera l'exécution à l'endroit wait (), et non à la première ligne de code du moniteur cong. L'opération wait () doit être encadrée par une boucle while.

La solution de gestion interne Java ne peut placer qu'une seule variable de condition et le package Java SDK prend en charge plusieurs.
Les problèmes de base de la programmation simultanée, de l'exclusion mutuelle et de la synchronisation, peuvent être résolus par les moniteurs, les moniteurs sont donc les clés principales de la programmation simultanée en Java.

Seven, la réalisation du thread de concurrence Java

L'état de vie d'un thread Java:
NEW (état d'initialisation)
RUNNABLE ( état exécutable / en cours d'exécution)
BLOCKED (état de blocage)
WAITING (attente sans limite de temps)
TIMED_WAITING (attente avec limite de temps)
TERMINATED (état de fin)

Leur relation est la suivante:
Runnable-> Blocked: Thread attend la synchronisation
Runnable-> Wating: 1. Appelez la méthode wait () après avoir acquis le verrou. 2. En attente de la fin des autres threads (thread name.join ()), méthode 3LockSupport.park ()
Runnable-> Timed_Waiting: sleep (), wait (parameter), join (parameter),
LockSupport.parkNanos (Object blocker, longue échéance)
LockSupport.parkUntil ( longue échéance ).
Nouveau-> Exécutable: start ()
Exécutable-> Terminé: le thread en cours d'exécution se termine, stop (), interruption (). (La différence entre stop et interruption est que la méthode stop ignore les verrous partiels du thread sans le relâcher, et tue directement, ce qui empêche certains types de verrous d'être libérés. L'interruption consiste à notifier le thread, et le thread a un certain temps pour libérer le verrou)
(le vidage de thread peut être utilisé pour suivre le bogue de concurrence, et l'état du thread est une bonne base.)

Nous savons tous que la concurrence est obtenue par le biais de threads, alors combien de threads devons-nous créer?
L'essence des threads est d'améliorer les performances. Il existe deux façons d'améliorer les performances. La première est l'optimisation de l'algorithme. Deuxièmement, l'utilisation du processeur et des E / S.
Pour les calculs gourmands en CPU: essayez de définir le nombre de threads sur le nombre de cœurs CPU + 1.
Pour les calculs gourmands en E / S: le nombre de threads et le nombre de cœurs CPU * [1+ (temps d'E / S- consommatrice de temps / CPU)]

Le nombre de threads n'a pas de valeur directement calculée. Nous devons essayer en fonction du type de projet. La formule ne peut être utilisée que comme point d'ancrage pour le calcul, de sorte que nous sommes relativement proches de la solution optimale.

Nous avons mentionné précédemment que l'état intermédiaire de l'opération atomique essentielle n'est pas visible de l'extérieur, de sorte que les variables locales de la méthode sont également thread-safe.

La machine virtuelle Java place la méthode dans la pile de méthodes Java. Un thread est une pile et une méthode est un cadre de pile. Lorsque nous appelons la méthode B dans la méthode A et la méthode C dans la méthode B (similaire à la récursivité), alors chaque sera utilisée comme cadre de pile et insérée dans la pile, et les variables locales de la méthode sont les valeurs du cadre de pile. Les variables locales et les méthodes vivent et meurent ensemble. Dans le même temps, les variables locales ne dépasseront pas leur propre cadre de pile, donc des variables locales Pas de partage Comme il n'y a pas de partage, il n'y a pas de bogues simultanés.
L'objet que nous créons est sur le tas, et les variables locales sont sur la pile, car les objets sur le tas peuvent accéder aux variables locales sur le cadre de pile au-delà de la limite.

Cette idée s'appelle la fermeture de thread. Les variables n'existent que sur le thread dans lequel elles se trouvent et ne sont pas partagées avec d'autres variables, donc la sécurité des threads. Il existe également des applications pour cela dans JDBC. Le pool de connexions de base de données garantit qu'une fois qu'un objet Connection est capturé par un thread, il ne sera pas alloué à d'autres threads, de sorte que Connection n'aura pas de problèmes de concurrence.

Huit programmes concurrents orientés objet

En utilisant l'encapsulation, l'encapsulation de variable partagée est encapsulée comme un attribut d'objet à l'intérieur, et des stratégies d'accès simultanées (méthodes publiques d'accès mutuellement exclusives) sont spécifiées pour toutes les méthodes publiques (modèle de gestion MESA). C'est comme si la serrure était un billet de siège, et l'emplacement est une méthode ou un bloc de code, mais il y a toujours des agents de sécurité pour vérifier le billet de siège à la porte, sinon le billet perdra son sens.

Paramètres conditionnels entre les variables partagées, de nombreuses variables partagées ont des restrictions conditionnelles, telles que la limite inférieure de l'inventaire est inférieure à la limite supérieure. Ensuite, nous ajoutons la condition if lors de la conception du code, mais il y aura un problème d'exécution de course à cause de cela.
Par conséquent, lors de la conception de variables partagées, nous devons faire attention aux contraintes entre elles et éviter les exécutions concurrentes.

Spécifiez la stratégie d'accès simultané: 1. Évitez le partage 2. Mode invariant 3. Le moniteur et les autres outils concurrents
doivent faire attention lors de l'utilisation d'autres outils simultanés: 1. Utilisez des outils concurrents matures 2. Utilisez des primitives de synchronisation de bas niveau en dernier recours 3. Évitez optimisation prématurée.

Remarque: le
verrou doit être privé, immuable et non réutilisable.
Encapsulez les variables partagées et contrôlez les chemins d'accès simultanés, ce qui peut résoudre les conditions de concurrence.
Lorsque l'exception InterruptedException est déclenchée, la machine virtuelle Java efface le bit indicateur d'interruption du thread en même temps. interrompre à nouveau.

Je suppose que tu aimes

Origine blog.csdn.net/weixin_43372169/article/details/110449264
conseillé
Classement