web-dev-qa-db-fra.com

Le thread! = Check thread est-il sûr?

Je sais que des opérations composées telles que i++ ne sont pas thread-safe car ils impliquent plusieurs opérations .

Mais la vérification de la référence avec elle-même est-elle une opération thread-safe?

a != a //is this thread-safe

J'ai essayé de programmer cela et d'utiliser plusieurs threads, mais cela n'a pas échoué. Je suppose que je n'ai pas pu simuler la course sur ma machine.

MODIFIER:

public class TestThreadSafety {
    private Object a = new Object();

    public static void main(String[] args) {

        final TestThreadSafety instance = new TestThreadSafety();

        Thread testingReferenceThread = new Thread(new Runnable() {

            @Override
            public void run() {
                long countOfIterations = 0L;
                while(true){
                    boolean flag = instance.a != instance.a;
                    if(flag)
                        System.out.println(countOfIterations + ":" + flag);

                    countOfIterations++;
                }
            }
        });

        Thread updatingReferenceThread = new Thread(new Runnable() {

            @Override
            public void run() {
                while(true){
                    instance.a = new Object();
                }
            }
        });

        testingReferenceThread.start();
        updatingReferenceThread.start();
    }

}

Ceci est le programme que j'utilise pour tester la sécurité des threads.

Comportement étrange

Comme mon programme démarre entre certaines itérations, j'obtiens la valeur du drapeau de sortie, ce qui signifie que la référence != la vérification échoue sur la même référence. MAIS après quelques itérations, la sortie devient une valeur constante false, puis l'exécution du programme pendant une longue période ne génère pas une seule sortie true.

Comme la sortie le suggère après quelques n (non fixes) itérations, la sortie semble être une valeur constante et ne change pas.

Sortie:

Pour certaines itérations:

1494:true
1495:true
1496:true
19970:true
19972:true
19974:true
//after this there is not a single instance when the condition becomes true
140
Narendra Pathai

En l'absence de synchronisation ce code

Object a;

public boolean test() {
    return a != a;
}

peut produire true. Ceci est le bytecode pour test()

    ALOAD 0
    GETFIELD test/Test1.a : Ljava/lang/Object;
    ALOAD 0
    GETFIELD test/Test1.a : Ljava/lang/Object;
    IF_ACMPEQ L1
...

comme nous pouvons le voir, il charge deux fois le champ a dans les variables locales, c'est une opération non atomique, si a a été modifié entre les deux par une autre comparaison de threads peut produire false.

En outre, le problème de visibilité de la mémoire est pertinent ici, il n'y a aucune garantie que les modifications apportées à a par un autre thread seront visibles par le thread actuel.

122
Evgeniy Dorofeev

Est-ce que le chèque a != a thread-safe?

Si a peut potentiellement être mis à jour par un autre thread (sans synchronisation appropriée!), Alors Non.

J'ai essayé de programmer cela et d'utiliser plusieurs threads, mais je n'ai pas échoué. Je suppose que je n'ai pas pu simuler la course sur ma machine.

Ça ne veut rien dire! Le problème est que si une exécution dans laquelle a est mise à jour par un autre thread est autorisée par le JLS, alors le code n'est pas un thread -sûr. Le fait que vous ne puissiez pas provoquer la condition de concurrence critique avec un cas de test particulier sur une machine particulière et une implémentation particulière Java, ne l'empêche pas de se produire dans d'autres circonstances.

Est-ce à dire que a! = A pourrait retourner true.

Oui, en théorie, dans certaines circonstances.

Alternativement, a != a pourrait renvoyer false même si a changeait simultanément.


Concernant le "comportement bizarre":

Comme mon programme démarre entre certaines itérations, j'obtiens la valeur de l'indicateur de sortie, ce qui signifie que la vérification de la référence! = Échoue sur la même référence. MAIS après quelques itérations, la sortie devient une valeur constante false, puis l'exécution du programme pendant une longue période ne génère pas une seule sortie vraie.

Ce comportement "bizarre" est compatible avec le scénario d'exécution suivant:

  1. Le programme est chargé et la JVM commence à interpréter les bytecodes. Puisque (comme nous l'avons vu à partir de la sortie javap) le bytecode effectue deux charges, vous voyez (apparemment) les résultats de la condition de concurrence, parfois.

  2. Après un certain temps, le code est compilé par le compilateur JIT. L'optimiseur JIT remarque qu'il y a deux chargements du même emplacement de mémoire (a) rapprochés et optimise le second. (En fait, il est possible qu'il optimise entièrement le test ...)

  3. Maintenant, la condition de concurrence ne se manifeste plus, car il n'y a plus deux charges.

Notez que c'est tout cohérent avec ce que le JLS permet à une implémentation de Java de faire.


@kriss a commenté ainsi:

Cela ressemble à ce que les programmeurs C ou C++ appellent "Comportement non défini" (dépendant de l'implémentation). On dirait qu'il pourrait y avoir quelques UB dans Java dans les cas d'angle comme celui-ci.

Le Java Memory Model (spécifié dans JLS 17.4 ) spécifie un ensemble de conditions préalables sous lesquelles un thread est garanti de voir les valeurs de mémoire écrites par un autre thread. Si un thread tente pour lire une variable écrite par une autre, et ces conditions préalables ne sont pas remplies, il peut y avoir un certain nombre d'exécutions possibles ... dont certaines sont probablement incorrectes (du point de vue des exigences de l'application). En d'autres termes, l'ensemble des comportements possibles (c'est-à-dire l'ensemble des "exécutions bien formées") est défini, mais nous ne pouvons pas dire lequel de ces comportements se produira .

Le compilateur est autorisé à combiner et réorganiser les charges et enregistrer (et faire d'autres choses) à condition que l'effet final du code soit le même:

  • lorsqu'il est exécuté par un seul thread, et
  • lorsqu'il est exécuté par différents threads qui se synchronisent correctement (selon le modèle de mémoire).

Mais si le code ne se synchronise pas correctement (et donc que les relations "qui se produisent avant" ne contraignent pas suffisamment l'ensemble des exécutions bien formées), le compilateur est autorisé à réorganiser les charges et les magasins de manière à donner des résultats "incorrects". (Mais cela signifie simplement que le programme est incorrect.)

47
Stephen C

Prouvé avec test-ng:

public class MyTest {

  private static Integer count=1;

  @Test(threadPoolSize = 1000, invocationCount=10000)
  public void test(){
    count = new Integer(new Random().nextInt());
    Assert.assertFalse(count != count);
  }

}

J'ai 2 échecs sur 10 000 invocations. Donc NON , c'est PAS sûr pour les threads

27
Arnaud Denoyelle

Non, ça ne l'est pas. Pour une comparaison, le Java VM doit mettre les deux valeurs à comparer sur la pile et exécuter l'instruction de comparaison (laquelle dépend du type de "a") ).

Le Java VM peut:

  1. Lire "a" deux fois, mettre chacun sur la pile puis comparer les résultats
  2. Lire "a" une seule fois, le mettre sur la pile, le dupliquer (instruction "dup") et lancer la comparaison
  3. Éliminez complètement l'expression et remplacez-la par false

Dans le 1er cas, un autre thread pourrait modifier la valeur de "a" entre les deux lectures.

La stratégie choisie dépend du Java et du moteur d'exécution Java (en particulier le compilateur JIT). Elle peut même changer pendant l'exécution de votre programme).

Si vous voulez vous assurer que la variable est accessible, vous devez la faire volatile (une soi-disant "demi-barrière de mémoire") ou ajouter une barrière de mémoire complète (synchronized). Vous pouvez également utiliser une API de niveau supérieur (par exemple AtomicInteger comme mentionné par Juned Ahasan).

Pour plus d'informations sur la sécurité des threads, lisez JSR 1 ( Java Memory Model ).

15
stefan.schwetschke

Tout cela a été bien expliqué par Stephen C.Pour le plaisir, vous pouvez essayer d'exécuter le même code avec les paramètres JVM suivants:

-XX:InlineSmallCode=0

Cela devrait empêcher l'optimisation effectuée par le JIT (il le fait sur le serveur hotspot 7) et vous verrez true pour toujours (je me suis arrêté à 2 000 000 mais je suppose que cela continue après cela).

Pour information, voici le code JIT. Pour être honnête, je ne lis pas l'assemblage assez couramment pour savoir si le test est réellement effectué ou d'où viennent les deux charges. (la ligne 26 est le test flag = a != a et la ligne 31 est l'accolade fermante de la while(true)).

  # {method} 'run' '()V' in 'javaapplication27/TestThreadSafety$1'
  0x00000000027dcc80: int3   
  0x00000000027dcc81: data32 data32 nop Word PTR [rax+rax*1+0x0]
  0x00000000027dcc8c: data32 data32 xchg ax,ax
  0x00000000027dcc90: mov    DWORD PTR [rsp-0x6000],eax
  0x00000000027dcc97: Push   rbp
  0x00000000027dcc98: sub    rsp,0x40
  0x00000000027dcc9c: mov    rbx,QWORD PTR [rdx+0x8]
  0x00000000027dcca0: mov    rbp,QWORD PTR [rdx+0x18]
  0x00000000027dcca4: mov    rcx,rdx
  0x00000000027dcca7: movabs r10,0x6e1a7680
  0x00000000027dccb1: call   r10
  0x00000000027dccb4: test   rbp,rbp
  0x00000000027dccb7: je     0x00000000027dccdd
  0x00000000027dccb9: mov    r10d,DWORD PTR [rbp+0x8]
  0x00000000027dccbd: cmp    r10d,0xefc158f4    ;   {oop('javaapplication27/TestThreadSafety$1')}
  0x00000000027dccc4: jne    0x00000000027dccf1
  0x00000000027dccc6: test   rbp,rbp
  0x00000000027dccc9: je     0x00000000027dcce1
  0x00000000027dcccb: cmp    r12d,DWORD PTR [rbp+0xc]
  0x00000000027dcccf: je     0x00000000027dcce1  ;*goto
                                                ; - javaapplication27.TestThreadSafety$1::run@62 (line 31)
  0x00000000027dccd1: add    rbx,0x1            ; OopMap{rbp=Oop off=85}
                                                ;*goto
                                                ; - javaapplication27.TestThreadSafety$1::run@62 (line 31)
  0x00000000027dccd5: test   DWORD PTR [rip+0xfffffffffdb53325],eax        # 0x0000000000330000
                                                ;*goto
                                                ; - javaapplication27.TestThreadSafety$1::run@62 (line 31)
                                                ;   {poll}
  0x00000000027dccdb: jmp    0x00000000027dccd1
  0x00000000027dccdd: xor    ebp,ebp
  0x00000000027dccdf: jmp    0x00000000027dccc6
  0x00000000027dcce1: mov    edx,0xffffff86
  0x00000000027dcce6: mov    QWORD PTR [rsp+0x20],rbx
  0x00000000027dcceb: call   0x00000000027a90a0  ; OopMap{rbp=Oop off=112}
                                                ;*aload_0
                                                ; - javaapplication27.TestThreadSafety$1::run@2 (line 26)
                                                ;   {runtime_call}
  0x00000000027dccf0: int3   
  0x00000000027dccf1: mov    edx,0xffffffad
  0x00000000027dccf6: mov    QWORD PTR [rsp+0x20],rbx
  0x00000000027dccfb: call   0x00000000027a90a0  ; OopMap{rbp=Oop off=128}
                                                ;*aload_0
                                                ; - javaapplication27.TestThreadSafety$1::run@2 (line 26)
                                                ;   {runtime_call}
  0x00000000027dcd00: int3                      ;*aload_0
                                                ; - javaapplication27.TestThreadSafety$1::run@2 (line 26)
  0x00000000027dcd01: int3   
6
assylias

Non, a != a n'est pas thread-safe. Cette expression se compose de trois parties: charger a, charger a à nouveau et exécuter !=. Il est possible qu'un autre thread obtienne le verrou intrinsèque sur le parent de a et change la valeur de a entre les 2 opérations de chargement.

Un autre facteur est cependant de savoir si a est local. Si a est local, aucun autre thread ne doit y avoir accès et doit donc être thread-safe.

void method () {
    int a = 0;
    System.out.println(a != a);
}

devrait également toujours afficher false.

Déclarer a comme volatile ne résoudrait pas le problème si a est static ou une instance. Le problème n'est pas que les threads ont des valeurs différentes de a, mais qu'un thread charge a deux fois avec des valeurs différentes. Cela peut en fait rendre le cas moins sûr pour les threads. Si a n'est pas volatile alors a peut être mis en cache et un changement dans un autre thread n'affectera pas le cache valeur.

5
DoubleMx2

Concernant le comportement bizarre:

Étant donné que la variable a n'est pas marquée comme volatile, à un moment donné, la valeur de a peut être mise en cache par le thread. as de a != a est alors la version mise en cache et donc toujours la même (ce qui signifie que flag est désormais toujours false).

3
Walter Laan

Même une lecture simple n'est pas atomique. Si a est long et n'est pas marqué comme volatile, alors sur les machines virtuelles Java 32 bits long b = a n'est pas thread-safe.

0
ZhekaKozlov