Java Language Happens-before raisonnement appliqué à quelques exemples


Exemple

Nous présenterons quelques exemples pour montrer comment appliquer les événements-avant de raisonner pour vérifier que les écritures sont visibles lors des lectures suivantes.

Code mono-thread

Comme vous vous en doutez, les écritures sont toujours visibles lors des lectures suivantes dans un programme à thread unique.

public class SingleThreadExample {
    public int a, b;
    
    public int add() {
       a = 1;         // write(a)
       b = 2;         // write(b)
       return a + b;  // read(a) followed by read(b)
    }
}

Par Happens-Before Rule # 1:

  1. L'action write(a) se produit avant l'action write(b) .
  2. L'action write(b) se produit avant l'action read(a) .
  3. L'action read(a) se produit avant l'action read(a) .

Par Happens-Before Rule # 4:

  1. write(a) arrive-avant write(b) ET write(b) arrive-avant de read(a) IMPLIES write(a) arrive-avant de read(a) .
  2. write(b) arrive-avant read(a) AND read(a) arrive-avant read(b) IMPLIES write(b) arrive-avant read(b) .

En résumé:

  1. La relation write(a) arrive-avant read(a) signifie que l’instruction a a + b est garantie de voir la valeur correcte de a .
  2. La relation write(b) arrive avant read(b) signifie que l'instruction a a + b est garantie de voir la valeur correcte de b .

Comportement de 'volatile' dans un exemple avec 2 threads

Nous allons utiliser l'exemple de code suivant pour explorer certaines implications du modèle de mémoire pour `volatile.

public class VolatileExample {
    private volatile int a;
    private int b;         // NOT volatile
    
    public void update(int first, int second) {
       b = first;         // write(b)
       a = second;         // write-volatile(a)
    }

    public int observe() {
       return a + b;       // read-volatile(a) followed by read(b)
    }
}

Tout d'abord, considérez la séquence suivante d'instructions impliquant 2 threads:

  1. Une seule instance de VolatileExample est créée. appelez ça ve
  2. ve.update(1, 2) est appelé dans un thread, et
  3. ve.observe() est appelé dans un autre thread.

Par Happens-Before Rule # 1:

  1. L'action write(a) se produit avant l'action volatile-write(a) .
  2. L'action volatile-read(a) se produit avant l'action read(b) .

Par Happens-Before Rule # 2:

  1. L'action volatile-write(a) dans le premier thread se produit avant l'action volatile-read(a) dans le deuxième thread.

Par Happens-Before Rule # 4:

  1. L'action write(b) dans le premier thread arrive avant l'action read(b) dans le second thread.

En d'autres termes, pour cette séquence particulière, il est garanti que le deuxième thread verra la mise à jour de la variable non volatile b créée par le premier thread. Cependant, il devrait également être clair que si les affectations dans la méthode de update étaient inverses ou si la méthode observe() lisait la variable b avant a , alors la chaîne « passe avant» serait rompue. La chaîne serait également brisée si volatile-read(a) dans le deuxième thread n'était pas postérieur à la volatile-write(a) dans le premier thread.

Lorsque la chaîne est rompue, il n'y a aucune garantie que d' observe() verra la valeur correcte de b .

Volatile à trois fils

Supposons que nous ajoutions un troisième thread dans l'exemple précédent:

  1. Une seule instance de VolatileExample est créée. appelez ça ve
  2. update appels à deux threads:
    • ve.update(1, 2) est appelé dans un thread,
    • ve.update(3, 4) est appelé dans le deuxième thread,
  3. ve.observe() est ensuite appelé dans un troisième thread.

Pour analyser cela complètement, nous devons considérer tous les liens possibles entre les instructions du thread un et le thread deux. Au lieu de cela, nous n'en considérerons que deux.

Scénario n ° 1 - supposons que la update(1, 2) précède la update(3,4) nous obtenons cette séquence:

write(b, 1), write-volatile(a, 2)     // first thread
write(b, 3), write-volatile(a, 4)     // second thread
read-volatile(a), read(b)             // third thread

Dans ce cas, il est facile de voir qu'il y a une chaîne ininterrompue avant- write(b, 3) de write(b, 3) à read(b) . De plus, il n'y a pas d'écriture à b . Ainsi, pour ce scénario, le troisième thread est assuré de voir que b valeur 3 .

Scénario n ° 2 - supposons que la update(1, 2) et la update(3,4) chevauchent et que les ations soient entrelacées comme suit:

write(b, 3)                           // second thread
write(b, 1)                           // first thread
write-volatile(a, 2)                  // first thread
write-volatile(a, 4)                  // second thread
read-volatile(a), read(b)             // third thread

Maintenant, bien qu'il y ait une chaîne de passe-avant de write(b, 3) à read(b) , il y a une action write(b, 1) intermédiaire exécutée par l'autre thread. Cela signifie que nous ne pouvons pas être certains que la valeur read(b) sera visible.

(Mis à part: cela démontre que nous ne pouvons pas compter sur la volatile pour assurer la visibilité des variables non volatiles, sauf dans des situations très limitées.)