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.
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.
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
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.
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:
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.
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 ...)
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:
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.)
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
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:
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 ).
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
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.
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. a
s 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
).
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.