La programmation simultanée est l'une des compétences les plus importantes pour les programmeurs Java et l'une des compétences les plus difficiles à maîtriser. Il exige que les programmeurs aient une compréhension approfondie des principes de fonctionnement les plus bas de l'ordinateur, et en même temps, il oblige les programmeurs à avoir une logique claire et une pensée méticuleuse, afin qu'ils puissent écrire des programmes simultanés multipliés efficaces, sûrs et fiables. Cette série commencera de la nature de la coordination inter-thread (attendez, notive, notifyall), synchronisée et volatile, et expliquez en détail chaque outil de concurrence et mécanisme de mise en œuvre sous-jacent fourni par JDK. Sur cette base, nous analyserons davantage les classes d'outils du package java.util.concurrent, y compris son utilisation, la mise en œuvre du code source et les principes derrière. Cet article est le premier article de cette série et est la partie théorique la plus fondamentale de cette série. Les articles ultérieurs seront analysés et expliqués sur la base de cela.
1. Partager
Le partage de données est l'une des principales raisons de la sécurité des threads. Si toutes les données ne sont valables que dans le thread, il n'y a pas de problème de sécurité du thread, ce qui est l'une des principales raisons pour lesquelles nous n'avons souvent pas besoin de considérer la sécurité du fil lors de la programmation. Cependant, dans la programmation multithread, le partage de données est inévitable. Le scénario le plus typique est les données de la base de données. Afin d'assurer la cohérence des données, nous devons généralement partager les données dans la même base de données. Même dans le cas du maître et de l'esclave, les mêmes données sont accessibles. Le maître et l'esclave copient simplement les mêmes données pour l'efficacité de l'accès et de la sécurité des données. Nous démontrons maintenant les problèmes causés par le partage des données sous plusieurs threads à travers un exemple simple:
Extrait de code 1:
package com.paddx.test.concurrent; classe publique SharedAta {public static int count = 0; public static void main (String [] args) {final sharedata data = new SharedAta (); pour (int i = 0; i <10; i ++) {new thread (new Runnable () {@Override public void run () {try {// pause pendant 1 millisecond lors de la saisie pour augmenter les chances de problèmes concurrencés thread.sleep (1);} capture (interruptedException e) {e.printStackTrace ();} pour (int j = 0; j <100; j ++); data.addCount ();} System.out.print (Count + "");}}). START (); } essayez {// le programme principal est interrompu pendant 3 secondes pour s'assurer que l'exécution du programme ci-dessus est terminée Thread.Sleep (3000); } catch (InterruptedException e) {e.printStackTrace (); } System.out.println ("count =" + count); } public void addCount () {count ++; }}Le but du code ci-dessus est d'ajouter une opération pour compter et exécuter 1 000 fois, mais ici est implémenté via 10 threads, chaque thread exécute 100 fois et, dans des circonstances normales, 1 000 doivent être sortis. Cependant, si vous exécutez le programme ci-dessus, vous constaterez que le résultat n'est pas le cas. Voici le résultat d'exécution d'un certain temps (les résultats de chaque exécution peuvent ne pas être les mêmes, et parfois le résultat correct peut être obtenu):
On peut voir que pour les opérations variables partagées, divers résultats inattendus sont facilement visibles dans un environnement multithread.
2. Exclusion mutuelle
L'exclusion mutuelle des ressources signifie qu'un seul visiteur est autorisé à y accéder en même temps, ce qui est unique et exclusif. Nous permettons généralement à plusieurs threads de lire des données en même temps, mais un seul thread peut écrire des données en même temps. Nous divisons donc généralement des verrous en verrous partagés et en serrures exclusives, également appelées verrous en lecture et écris. Si les ressources ne s'excluent pas mutuellement, nous n'avons pas à nous soucier de la sécurité des threads même si ce sont des ressources partagées. Par exemple, pour le partage de données immuables, tous les threads ne peuvent que les lire, donc les problèmes de sécurité des threads ne sont pas nécessaires. Cependant, la rédaction des opérations pour les données partagées nécessite généralement une exclusion mutuelle. Dans l'exemple ci-dessus, des problèmes de modification des données se produisent en raison du manque d'exclusion mutuelle. Java fournit plusieurs mécanismes pour assurer l'exclusion mutuelle, le moyen le plus simple est d'utiliser la synchronisation. Maintenant, nous ajoutons synchronisés au programme ci-dessus et exécutez:
Code Snippet deux:
package com.paddx.test.concurrent; classe publique SharedAta {public static int count = 0; public static void main (String [] args) {final sharedata data = new SharedAta (); pour (int i = 0; i <10; i ++) {new thread (new Runnable () {@Override public void run () {try {// pause pendant 1 millisecond lors de la saisie pour augmenter les chances de problèmes concurrencés thread.sleep (1);} capture (interruptedException e) {e.printStackTrace ();} pour (int j = 0; j <100; j ++); data.addCount ();} System.out.print (Count + "");}}). START (); } essayez {// le programme principal est interrompu pendant 3 secondes pour s'assurer que l'exécution du programme ci-dessus est terminée Thread.Sleep (3000); } catch (InterruptedException e) {e.printStackTrace (); } System.out.println ("count =" + count); } / ** * Ajouter un mot-clé synchronisé * / public synchronisé void addCount () {count ++; }}Maintenant que le code ci-dessus est exécuté, vous constaterez que peu importe le nombre de fois que vous exécutez, le résultat final sera de 1000.
Iii. Atomicité
L'atomicité fait référence au fonctionnement des données comme un ensemble indépendant et indivisible. En d'autres termes, c'est une opération continue et sans interruption. La moitié de l'exécution des données n'est pas modifiée par d'autres threads. Le moyen le plus simple d'assurer l'atomicité est les instructions du système d'exploitation, c'est-à-dire que si une opération correspond à une instruction du système d'exploitation à la fois, elle assurera certainement l'atomicité. Cependant, de nombreuses opérations ne peuvent pas être terminées avec une seule instruction. Par exemple, pour les opérations de type long, de nombreux systèmes doivent être divisés en plusieurs instructions pour fonctionner respectivement sur les positions élevées et basses. Par exemple, le fonctionnement de l'entier I ++ que nous utilisons souvent doit réellement être divisé en trois étapes: (1) lire la valeur de l'entier I; (2) Ajouter une opération à i; (3) rédiger le résultat à la mémoire. Ce processus peut se produire dans le multithreading:
C'est également la raison pour laquelle le résultat de l'exécution du segment de code est incorrect. Pour cette opération de combinaison, le moyen le plus courant d'assurer l'atomicité est de verrouiller, comme synchronisé ou verrouiller en Java, peut être implémenté, et le segment de code 2 est implémenté par synchronisé. En plus des verrous, il existe un autre moyen de CAS (comparer et échanger), c'est-à-dire avant de modifier les données, comparer si les valeurs lues avant les précédentes sont cohérentes. S'ils sont cohérents, modifiez-les et s'ils sont incohérents, ils seront à nouveau exécutés. Il s'agit également du principe d'optimisation de l'implémentation de verrouillage. Cependant, CAS peut ne pas être efficace dans certains scénarios. Par exemple, un autre thread modifie d'abord une certaine valeur, puis le modifie à la valeur d'origine. Dans ce cas, CAS ne peut pas juger.
4. Visibilité
Pour comprendre la visibilité, vous devez avoir une certaine compréhension du modèle de mémoire de JVM. Le modèle de mémoire de JVM est similaire au système d'exploitation, comme indiqué sur la figure:
À partir de cette figure, nous pouvons voir que chaque thread a sa propre mémoire de travail (équivalent au tampon avancé du CPU. Le but est de réduire davantage la différence de vitesse entre le système de stockage et le CPU et d'améliorer les performances). Pour les variables partagées, chaque fois le thread lit une copie de la variable partagée dans la mémoire de travail. Lors de l'écriture, il modifie directement la valeur de la copie dans la mémoire de travail, puis synchronise la mémoire de travail avec la valeur dans la mémoire principale à un certain moment. Le problème que cela cause est que si le thread 1 modifie une certaine variable, le thread 2 peut ne pas voir les modifications apportées par le thread 1 à la variable partagée. Grâce au programme suivant, nous pouvons démontrer le problème invisible:
package com.paddx.test.concurrent; public class VisibilityTest {private static boolean ready; numéro d'int statique privé; classe statique privée ReaderThread étend Thread {public void run () {try {Thread.Sleep (10); } catch (InterruptedException e) {e.printStackTrace (); } if (! Ready) {System.out.println (Ready); } System.out.println (numéro); }} classe statique privée, écrivain, étend Thread {public void run () {try {thread.sleep (10); } catch (InterruptedException e) {e.printStackTrace (); } numéro = 100; Ready = true; }} public static void main (string [] args) {new WriterThread (). start (); new ReaderThread (). start (); }}Intuitivement, ce programme ne doit sortir que 100 et la valeur prête ne sera pas imprimée. En fait, si vous exécutez le code ci-dessus plusieurs fois, il peut y avoir de nombreux résultats différents. Voici les résultats de quelques courses:
Bien sûr, ce résultat ne peut être considéré comme possible en raison de la visibilité. Lorsque le thread d'écriture (WriterThread) est défini prêt = true, le ReaderThread ne peut pas voir le résultat modifié, donc FAUX sera imprimé. Pour le deuxième résultat, c'est-à-dire que le résultat du thread d'écriture n'a pas été lu lors de l'exécution si (! Prêt), mais le résultat de l'exécution du thread d'écriture est lu lors de l'exécution de System.out.println (Ready). Cependant, ce résultat peut également être causé par une exécution alternative des threads. La visibilité peut être assurée par synchronisé ou volatile en Java, et les détails spécifiques seront analysés dans les articles suivants.
5. Séquence
Pour améliorer les performances, le compilateur et le processeur peuvent réorganiser les instructions. Il existe trois types de réorganisation:
(1) Réorganisation optimisée par le compilateur. Le compilateur peut reprogrammer l'ordre d'exécution des déclarations sans modifier la sémantique d'un programme unique.
(2) Réorganiser le parallélisme au niveau de l'enseignement. Les processeurs modernes utilisent une technologie parallèle au niveau de l'instruction (ICP) pour chevaucher l'exécution de plusieurs instructions. S'il n'y a pas de dépendance aux données, le processeur peut modifier l'ordre d'exécution de l'instruction correspondant aux instructions de la machine.
(3) Réorganisation du système de mémoire. Étant donné que le processeur utilise des tampons de cache et de lecture / écriture, cela fait que les opérations de chargement et de stockage semblent être exécutées de commande.
Nous pouvons dire directement à la description des problèmes de réorganisation dans JSR 133:
(1) (2)
Examinons d'abord la partie du code source (1) dans l'image ci-dessus. À partir du code source, l'instruction 1 est exécutée en premier, soit l'instruction 3 est exécutée en premier. Si l'instruction 1 est exécutée en premier, R2 ne doit pas voir la valeur écrite dans l'instruction 4. Si l'instruction 3 est exécutée en premier, R1 ne doit pas voir la valeur écrite par l'instruction 2. Cependant, le résultat en cours peut avoir R2 == 2 et R1 == 1, qui est le résultat de la "réorganisation". La figure ci-dessus (2) est un résultat de compilation juridique possible. Après compilation, l'ordre de l'instruction 1 et l'instruction 2 peuvent être échangés. Par conséquent, le résultat de R2 == 2 et R1 == 1 apparaîtra. Synchronisé ou volatile peut également être utilisé en Java pour assurer la commande.
Six Résumé
Cet article explique la base théorique de la programmation simultanée Java, et certaines choses seront discutées plus en détail dans l'analyse ultérieure, telles que la visibilité, l'ordre, etc. Les articles suivants seront discutés sur la base du contenu de ce chapitre. Si vous pouvez bien comprendre le contenu ci-dessus, je pense que cela vous aidera une grande aide, qu'il s'agisse de comprendre d'autres articles de programmation simultanés ou dans votre travail de programmation quotidien simultané.