Concepts associés au modèle de mémoire
Trois concepts en programmation concurrente
Modèle de mémoire Java
Analyse approfondie duVolatilemot-clé
Scénarios d'utilisation du mot-clé volatile
Problème de cohérence du cache. Les variables accessibles par plusieurs threads sont généralement appelées variables partagées. C'est-à-dire que si une variable est mise en cache dans plusieurs processeurs (cela se produit généralement uniquement dans la programmation multi-thread). alors il peut y avoir des problèmes d'incohérence du cache
Afin de résoudre le problème d'incohérence du cache, il existe généralement deux solutions :
3. Trois concepts en programmation concurrente
En programmation concurrente, on le rencontre généralement Les trois problèmes suivants : l'atomicité. problème, problème de visibilité, problème d'ordre 3.1 Atomicité Atomicité : un processus dans lequel une ou plusieurs opérations sont soit toutes exécutées et exécutées. Il ne sera interrompu par aucun facteur, ou il ne sera interrompu. ne sera pas exécuté du tout. 3.2 Visibilité La visibilité signifie que lorsque plusieurs threads accèdent à la même variable, un thread modifie la valeur de la variable, les autres threads peuvent immédiatement voir la valeur modifiée <.>Ordre : c'est-à-dire que l'ordre d'exécution du programme est exécuté dans l'ordre du code, à en juger par la séquence de code, l'instruction 1 est avant l'instruction 2. Donc, quand la JVM est réellement. exécute ce code, garantira-t-il que l'instruction 1 sera exécutée avant l'instruction 2 ? Pas nécessairement. Pourquoi une duplication d'instructions peut-elle se produire ici ?
La réorganisation des instructions n'affectera pas l'exécution d'un seul thread, mais affectera l'exactitude de l'exécution simultanée des threads.
En d'autres termes, pour que les programmes concurrents s'exécutent correctement, l'atomicité, la visibilité et l'ordre doivent être garantis. Tant que l’un d’eux n’est pas garanti, cela peut entraîner un mauvais fonctionnement du programme.
4. Modèle de mémoire Java
Dans la spécification de la machine virtuelle Java, une tentative est faite pour définir un modèle de mémoire Java (Java Memory
Model, JMM) pour protéger divers plates-formes matérielles et opérations. Différences d’accès à la mémoire système pour obtenir des effets d’accès à la mémoire cohérents pour les programmes Java sur diverses plates-formes. Alors, que stipule le modèle de mémoire Java ? Il définit les règles d'accès aux variables du programme. Dans une plus large mesure, il définit l'ordre d'exécution du programme. Notez que afin d'obtenir de meilleures performances d'exécution, le modèle de mémoire Java n'empêche pas le moteur d'exécution d'utiliser les registres ou le cache du processeur pour améliorer la vitesse d'exécution des instructions, ni n'empêche le compilateur de réorganiser les instructions. En d’autres termes, dans le modèle de mémoire Java, il y aura également des problèmes de cohérence du cache et des problèmes de réorganisation des instructions.
4.1 AtomicitéEn Java, la lecture et l'affectation d'opérations à des variables du
type de données de basesont des opérations atomiques, c'est-à-dire que ces opérations ne peuvent pas être interrompues, ni exécutées. ou non exécuté.
En fait, seule l'instruction 1 est une opération atomique, et les trois autres instructions ne sont pas des opérations atomiques.
En d'autres termes, seules la simple lecture et l'affectation (et le nombre doit être attribué à une variable, l'affectation mutuelle entre variables n'est pas une opération atomique) sont des opérations atomiques.
Comme le montre ce qui précède, le modèle de mémoire Java garantit uniquement que la lecture et l'affectation de base sont des opérations atomiques. Si vous souhaitez obtenir l'atomicité pour une plus grande gamme d'opérations, vous pouvez y parvenir via la synchronisation et le verrouillage. .
Pour la visibilité, Java fournit le mot-clé volatile pour assurer la visibilité.
Lorsqu'une variable partagée est modifiée de manière volatile, elle garantira que la valeur modifiée sera immédiatement mise à jour dans la mémoire principale. Lorsque d'autres threads auront besoin de la lire, elle lira la nouvelle valeur de la mémoire.
Les variables partagées ordinaires ne peuvent pas garantir la visibilité, car après la modification d'une variable partagée ordinaire, il n'est pas certain quand elle sera écrite dans la mémoire principale. Lorsque d'autres threads la liront, la mémoire peut toujours être l'ancienne valeur d'origine. , la visibilité n'est donc pas garantie.
De plus, la visibilité peut également être garantie grâce à synchronisé et Lock. Synchronized et Lock peuvent garantir qu'un seul thread acquiert le verrou en même temps puis exécute le code de synchronisation, et la modification de la variable sera effectuée. vidé dans la mémoire principale avant de libérer le verrou. La visibilité est donc garantie.
Dans le modèle de mémoire Java, le compilateur et le processeur sont autorisés à réorganiser les instructions, mais le processus de réorganisation n'affectera pas l'exécution des programmes monothread, mais affectera l'exactitude d'exécution simultanée multithread.
En Java, vous pouvez utiliser le mot-clé volatile pour assurer un certain "ordre" (il peut interdire la réorganisation des instructions). De plus, l'ordre peut être assuré grâce à synchronisé et Lock. Évidemment, synchronisé et Lock garantissent qu'un thread exécute le code de synchronisation à chaque instant, ce qui équivaut à laisser les threads exécuter le code de synchronisation de manière séquentielle, ce qui garantit naturellement l'ordre.
De plus, le modèle de mémoire Java possède un certain « ordre » inné, c'est-à-dire un ordre qui peut être garanti sans aucun moyen. C'est ce qu'on appelle souvent le principe qui se produit avant. Si l’ordre d’exécution de deux opérations ne peut être déduit du principe de l’occurrence avant, alors leur ordre n’est pas garanti et la machine virtuelle peut les réordonner à volonté.
Ce qui suit est une introduction détaillée au principe qui se produit avant :
Règles de séquence de programme : dans un fil, selon l'ordre du code, écrivez devant L'opération se produit avant l'opération écrite après
Règle de verrouillage : Une opération de déverrouillage se produit avant la même opération de verrouillage après le même verrouillage
volatile Règles de variable : Une opération d'écriture dans une variable se produit en premier avant une opération de lecture ultérieure sur cette variable
Règle de propagation : Si l'opération A se produit avant l'opération B et que l'opération B se produit d'abord avant l'opération C, on peut conclure que l'opération A se produit avant l'opération C
Règle de démarrage du thread : La méthode start() de l'objet Thread se produit ici en premier Chaque action du thread
Règle d'interruption du thread : L'appel à la méthode thread interruption() se produit en premier lorsque le code du thread interrompu détecte l'occurrence de l'événement d'interruption
Règle de fin de thread : toutes les opérations dans un thread se produisent en premier lorsque le thread est terminé. Nous pouvons détecter que le thread s'est terminé via la méthode Thread.join() et la valeur de retour de Thread.isAlive().
La quatrième règle reflète en fait la nature transitive du principe qui se produit avant.
Une fois une variable partagée (variable membre d'une classe, class Une fois la variable membre statique) modifiée volatile, elle a deux niveaux de sémantique :
assure la visibilité lorsque différents threads opèrent sur cette variable, c'est-à-dire un thread modifie une certaine valeur d'une variable, la nouvelle valeur est immédiatement visible par les autres threads.
La réorganisation des instructions est interdite.
Concernant la visibilité, regardons d'abord un morceau de code Si le thread 1 s'exécute en premier et que le thread 2 s'exécute plus tard :
//线程1 boolean stop = false; while(!stop){ doSomething(); } //线程2 stop = true;
Ce code est très typique. morceau de code, de nombreuses personnes peuvent utiliser cette méthode de marquage lors de l'interruption d'un fil de discussion. Mais au fait, ce code fonctionnera-t-il tout à fait correctement ? Autrement dit, le fil sera-t-il interrompu ? Pas nécessairement, peut-être la plupart du temps, ce code peut interrompre le thread, mais il peut également empêcher le thread d'être interrompu (bien que cette possibilité soit très faible, mais une fois que cela se produit, cela provoquera une boucle infinie).
Ce qui suit explique pourquoi ce code peut empêcher l'interruption du fil de discussion. Comme expliqué précédemment, chaque thread a sa propre mémoire de travail pendant son exécution, donc lorsque le thread 1 est en cours d'exécution, il copiera la valeur de la variable stop et la placera dans sa propre mémoire de travail.
Ensuite, lorsque le thread 2 change la valeur de la variable stop, mais avant d'avoir le temps de l'écrire dans la mémoire principale, le thread 2 bascule pour faire autre chose, alors le thread 1 ne connaît pas le changement du stop variable par le thread 2, donc le cycle continuera.
Mais après avoir utilisé la modification volatile, cela devient différent :
Premièrement : l'utilisation du mot-clé volatile forcera l'écriture immédiate de la valeur modifiée dans la mémoire principale < 🎜 ; >
Il y a un malentendu ici. Le mot-clé volatile peut assurer la visibilité, mais l'erreur dans le programme ci-dessus est qu'il ne garantit pas l'atomicité. La visibilité ne peut garantir que la dernière valeur est lue à chaque fois, mais volatile ne peut pas garantir l'atomicité des opérations sur les variables.
public class Test { public volatile int inc = 0; public void increase() { inc++; } public static void main(String[] args) { final Test test = new Test(); for(int i=0;i<10;i++){ new Thread(){ public void run() { for(int j=0;j<1000;j++) test.increase(); }; }.start(); } while(Thread.activeCount()>1) //保证前面的线程都执行完 Thread.yield(); System.out.println(test.inc); } }
根源就在这里,自增操作不是原子性操作,而且volatile也无法保证对变量的任何操作都是原子性的。
把上面的代码改成以下任何一种都可以达到效果:
采用synchronized:
public class Test { public int inc = 0; public synchronized void increase() { inc++; } public static void main(String[] args) { final Test test = new Test(); for(int i=0;i<10;i++){ new Thread(){ public void run() { for(int j=0;j<1000;j++) test.increase(); }; }.start(); } while(Thread.activeCount()>1) //保证前面的线程都执行完 Thread.yield(); System.out.println(test.inc); } }
采用Lock:
public class Test { public int inc = 0; Lock lock = new ReentrantLock(); public void increase() { lock.lock(); try { inc++; } finally{ lock.unlock(); } } public static void main(String[] args) { final Test test = new Test(); for(int i=0;i<10;i++){ new Thread(){ public void run() { for(int j=0;j<1000;j++) test.increase(); }; }.start(); } while(Thread.activeCount()>1) //保证前面的线程都执行完 Thread.yield(); System.out.println(test.inc); } }
采用AtomicInteger:
public class Test { public AtomicInteger inc = new AtomicInteger(); public void increase() { inc.getAndIncrement(); } public static void main(String[] args) { final Test test = new Test(); for(int i=0;i<10;i++){ new Thread(){ public void run() { for(int j=0;j<1000;j++) test.increase(); }; }.start(); } while(Thread.activeCount()>1) //保证前面的线程都执行完 Thread.yield(); System.out.println(test.inc); } }
在java 1.5的java.util.concurrent.atomic包下提供了一些原子操作类,即对基本数据类型的 自增(加1操作),自减(减1操作)、以及加法操作(加一个数),减法操作(减一个数)进行了封装,保证这些操作是原子性操作。atomic是利用CAS来实现原子性操作的(Compare And Swap),CAS实际上是利用处理器提供的CMPXCHG指令实现的,而处理器执行CMPXCHG指令是一个原子性操作。
volatile能在一定程度上保证有序性。
volatile关键字禁止指令重排序有两层意思:
1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
举个例子:
//x、y为非volatile变量 //flag为volatile变量 x = 2; //语句1 y = 0; //语句2 flag = true; //语句3 x = 4; //语句4 y = -1; //语句5
由于flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句1、语句2前面,也不会讲语句3放到语句4、语句5后面。但是要注意语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的。
并且volatile关键字能保证,执行到语句3时,语句1和语句2必定是执行完毕了的,且语句1和语句2的执行结果对语句3、语句4、语句5是可见的。
这里探讨一下volatile到底如何保证可见性和禁止指令重排序的。
下面这段话摘自《深入理解Java虚拟机》:
“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
它会强制将对缓存的修改操作立即写入主存;
如果是写操作,它会导致其他CPU中对应的缓存行无效。
synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。通常来说,使用volatile必须具备以下2个条件:
对变量的写操作不依赖于当前值(比如++操作,上面有例子)
该变量没有包含在具有其他变量的不变式中
实际上,这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。
事实上,我的理解就是上面的2个条件需要保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行。
下面列举几个Java中使用volatile的几个场景。
状态标记量
volatile boolean flag = false; while(!flag){ doSomething(); } public void setFlag() { flag = true; }
volatile boolean inited = false; //线程1: context = loadContext(); inited = true; //线程2: while(!inited ){ sleep() } doSomethingwithconfig(context);
double check
class Singleton{ private volatile static Singleton instance = null; private Singleton() { } public static Singleton getInstance() { if(instance==null) { synchronized (Singleton.class) { if(instance==null) instance = new Singleton(); } } return instance; } }
至于为何需要这么写请参考:
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!