140 votes

Est la != vérifier thread-safe?

Je sais que le composé opérations telles que l' i++ ne sont pas thread-safe, car elles comportent de multiples opérations.

Mais est la vérification de la référence à lui-même un "thread-safe" opération?

a != a //is this thread-safe

J'ai essayé de programmer et d'utiliser plusieurs threads, mais il ne l'a pas manqué. Je suppose que je ne pouvais pas simuler course sur ma machine.

EDIT:

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();
    }

}

C'est le programme que j'utilise pour tester le fil de sécurité.

Le comportement bizarre

Comme mon programme commence entre quelques itérations-je obtenir de l'indicateur de sortie de valeur, ce qui signifie que la référence != vérification échoue sur la même référence. MAIS après quelques itérations, la sortie devient la valeur de la constante false et d'exécuter le programme pour un long temps ne génère pas un seul true de la production.

Comme la sortie suggère après certains n (non fixe) itérations de la sortie semble être la valeur de la constante et ne change pas.

Sortie:

Pour quelques 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

123voto

Evgeniy Dorofeev Points 52031

En l'absence de synchronisation de ce code

Object a;

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

peut produire de vrai. C'est le pseudo-code d'essai()

    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 le champ a local vars deux fois, c'est un non-opération atomique, si a a été changé entre par un autre thread comparaison peut produire de faux.

Aussi la mémoire de la visibilité problème est toujours d'actualité ici, il n'y a aucune garantie que les modifications apportées à a faite par un autre thread sera visible par le thread courant.

47voto

Stephen C Points 255558

Sont le check - a != a "thread-safe"?

Si a peuvent éventuellement être mis à jour par un autre thread (sans synchronisation correcte!), alors Non.

J'ai essayé de programmer et d'utiliser plusieurs threads, mais ne l'a pas manqué. Je suppose que ne pouvait pas simuler course sur ma machine.

Cela ne veut pas dire n'importe quoi! Le problème est que si une exécution en ce qui a est mise à jour par un autre thread est autorisé par la JLS, le code n'est pas thread-safe. Le fait que vous ne pouvez pas vous causer de la condition de la course pour arriver avec un test sur une machine particulière et une implémentation de Java, cela ne l'empêche pas de se produire dans d'autres circonstances.

Est-ce à dire que != un pourrait revenir true.

Oui, en théorie, dans certaines circonstances.

Sinon, a != a pourrait revenir false même si a était en train de changer simultanément.


Concernant le "comportement bizarre":

Comme mon programme commence entre quelques itérations-je obtenir de l'indicateur de sortie de valeur, ce qui signifie que la référence != vérification échoue sur la même référence. MAIS après quelques itérations, la sortie devient la valeur de la constante de faux et d'exécuter le programme pour un long temps ne génère pas une seule vraie sortie.

Ce "bizarre" comportement est cohérent avec la suite de l'exécution du scénario:

  1. Le programme est chargé et la JVM commence l'interprétation du bytecode. Car (comme nous l'avons vu à partir de la javap de sortie) le bytecode n'deux charges, vous (apparemment) voir les résultats de la condition de la course, de temps en temps.

  2. Après un moment, le code est compilé par le compilateur JIT. L'équipe de l'optimiseur d'avis qu'il y a deux charges de même de l'emplacement de mémoire (a) proche, et optimise la deuxième de suite. (En fait, il y a une chance qu'il optimise le test de loin ...)

  3. Maintenant, la condition de la course n'est plus manifeste, car il n'y a plus de deux des charges.

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


@kriss a commenté ainsi:

Cela ressemble à ce pourrait être ce que le C ou le C++ programmeurs appelle "un Comportement Indéfini" (dépendant de l'implémentation). Semble comme il pourrait y avoir un peu d'UB en java en cas de coin comme celui-ci.

La Java du Modèle de Mémoire (spécifié dans JLS 17.4) spécifie un ensemble de conditions préalables en vertu de laquelle un thread est garanti de voir les valeurs de la mémoire écrite par un autre thread. Si un thread tente de lire une variable écrite par un autre, et ces conditions ne sont pas remplies, il peut y avoir un certain nombre de possibles exécutions ... dont certaines sont susceptibles d'être incorrect (du point de vue des besoins de l'application). En d'autres termes, l' ensemble des comportements (c'est à dire l'ensemble des "bien formé exécutions") est défini, mais on ne peut pas dire lequel de ces comportements vont se produire.

Le compilateur est permis de combiner et de réorganiser les charges et les enregistrer (et d'autres choses) fourni à la fin de l'effet du code est le même:

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

Mais si le code ne synchronise pas correctement (et donc la "passe avant" les relations ne sont pas suffisamment contraindre l'ensemble de la bien formé exécutions) le compilateur est permis de réorganiser les charges et les magasins de manière à donner de "mauvaises" des résultats. (Mais c'est vraiment juste pour dire que le programme est incorrect).

27voto

Arnaud Denoyelle Points 7276

Prouvé par 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 échoue sur 10 000 invocations. Donc NON, il n'est PAS thread-safe

15voto

stefan.schwetschke Points 5143

Non, il n'est pas. Pour une comparaison de la machine virtuelle Java doit mettre les deux valeurs à comparer sur la pile et exécuter le comparer à l'enseignement (dont l'un dépend du type de "un").

La machine virtuelle Java peut:

  1. Lire "une" à deux reprises, de mettre chacun sur la pile, puis de comparer les résultats
  2. Lire "une" une seule fois, le mettre sur la pile, le dupliquer ("dup" de l'enseignement) et de la course les comparer
  3. De supprimer l'expression complètement et de le remplacer avec de l' false

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

La stratégie qui est choisi dépend du compilateur Java et Java Runtime (surtout le compilateur JIT). Il peut même changer au cours de l'exécution de votre programme.

Si vous voulez vous assurer que la façon dont la variable est accessible, vous devez faire c' volatile (ce qu'on appelle "la moitié de la mémoire de la barrière") ou d'ajouter une barrière de mémoire complète (synchronized). Vous pouvez également utiliser certaines hgiher niveau de l'API (par exemple, AtomicInteger comme mentionné par Juned Ahasan).

Pour plus de détails sur la sécurité des threads, lire la JSR 133 (Java Modèle de Mémoire).

6voto

assylias Points 102015

Cela a été bien expliqué par Stephen C. Pour le plaisir, vous pouvez essayer d'exécuter le même code à la suite de la JVM paramètres:

-XX:InlineSmallCode=0

Cela devrait permettre d'éviter l'optimisation effectuée par le JIT (il n'sur hotspot 7) et vous verrez true jamais (je me suis arrêté à 2 000 000 de mais je suppose qu'il continue après).

Pour plus d'informations, ci-dessous est le JIT ed code. Pour être honnête, je n'ai pas lu l'assemblée couramment assez pour savoir si le test est effectué ou lorsque les deux charges viennent. (la ligne 26 est le test flag = a != a et 31 ligne est l'accolade de fermeture de l' 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   

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X