Je sais que les opérations composées telles que i++
ne sont pas thread-safe car elles 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.
ÉDITER:
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 la sécurité des threads.
Comportement étrange
Lorsque 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 référence échoue sur la même référence. MAIS après quelques itérations, la sortie devient une valeur constante false
et l'exécution du programme pendant une longue période ne génère pas une seule true
sortie.
Comme la sortie le suggère après quelques n itérations (non fixes), la sortie semble être une valeur constante et ne change pas.
Production:
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
la source
1234:true
ne fracassent les uns des autres ). Un test de course nécessite une boucle intérieure plus serrée. Imprimez un résumé à la fin (comme quelqu'un l'a fait ci-dessous avec un cadre de test unitaire).Réponses:
En l'absence de synchronisation ce code
peut produire
true
. Ceci est le bytecode pourtest()
comme nous pouvons le voir, il charge
a
deux fois le champ dans les variables locales, c'est une opération non atomique, si elle aa
été modifiée entre les deux par une autre comparaison de threads peut produirefalse
.En outre, le problème de visibilité de la mémoire est pertinent ici, il n'y a aucune garantie que les modifications
a
apportées par un autre thread seront visibles pour le thread actuel.la source
!=
, qui consiste à charger séparément le LHS et le RHS. Et donc, si le JLS ne mentionne rien de spécifique sur les optimisations lorsque LHS et RHS sont syntaxiquement identiques, alors la règle générale s'appliquerait, ce qui signifie chargera
deux fois.Si
a
peut potentiellement être mis à jour par un autre thread (sans synchronisation appropriée!), Alors non.Cela 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 thread-safe. Le fait que vous ne pouvez pas provoquer la condition de concurrence avec un cas de test particulier sur une machine particulière et une implémentation Java particulière ne l'empêche pas de se produire dans d'autres circonstances.Oui, en théorie, dans certaines circonstances.
Alternativement,
a != a
pourrait revenirfalse
même s'ila
changeait simultanément.Concernant le "comportement bizarre":
Ce comportement "étrange" est cohérent 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, de temps en temps.
Après un certain temps, le code est compilé par le compilateur JIT. L'optimiseur JIT remarque qu'il y a deux charges du même emplacement de mémoire (
a
) proches l'une de l'autre et optimise la seconde. (En fait, il est possible que cela optimise entièrement le test ...)Maintenant, la condition de concurrence ne se manifeste plus, car il n'y a plus deux charges.
Notez que cela est tout conforme à ce que l'JLS permet une implémentation de Java faire.
@kriss a commenté ainsi:
Le modèle de mémoire Java (spécifié dans JLS 17.4 ) spécifie un ensemble de conditions préalables dans lesquelles un thread est assuré de voir les valeurs de mémoire écrites par un autre thread. Si un thread tente de lire une variable écrite par un autre, et que ces conditions préalables ne sont pas satisfaites, alors il peut y avoir un certain nombre d'exécutions possibles ... dont certaines sont susceptibles d'être 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 lesquels de ces comportements se produiront.
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 les relations «se produit 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 veut simplement dire que le programme est incorrect.)
la source
a != a
pourrait revenir vrai?Prouvé avec test-ng:
J'ai 2 échecs sur 10 000 invocations. Donc NON , ce n'est PAS thread-safe
la source
Random.nextInt()
partie est superflue. Vous auriez pu tester avecnew Object()
tout aussi bien.Non, ça ne l'est pas. Pour une comparaison, la machine virtuelle Java doit mettre les deux valeurs à comparer sur la pile et exécuter l'instruction de comparaison (laquelle dépend du type de "a").
La machine virtuelle Java 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 compilateur Java et du Java Runtime (en particulier le compilateur JIT). Il peut même changer pendant l'exécution de votre programme.
Si vous voulez vous assurer de la manière dont on accède à la variable, vous devez la créer
volatile
(une soi-disant «barrière de la moitié de la mémoire») ou ajouter une barrière de la mémoire pleine (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 133 ( modèle de mémoire Java ).
la source
a
commevolatile
impliquerait toujours deux lectures distinctes, avec la possibilité d'un changement entre les deux.Tout a été bien expliqué par Stephen C. Pour vous amuser, vous pouvez essayer d'exécuter le même code avec les paramètres JVM suivants:
Cela devrait empêcher l'optimisation faite par le JIT (c'est le cas sur le serveur hotspot 7) et vous verrez
true
pour toujours (je me suis arrêté à 2.000.000 mais je suppose que ça continue après ça).Pour information, vous trouverez ci-dessous le code JIT. Pour être honnête, je ne lis pas assez couramment l'assemblage pour savoir si le test est réellement fait ou d'où viennent les deux charges. (la ligne 26 est le test
flag = a != a
et la ligne 31 est l'accolade fermante duwhile(true)
).la source
0x27dccd1
à0x27dccdf
. Lejmp
dans la boucle est inconditionnel (puisque la boucle est infinie). Les deux seules autres instructions de la boucle sontadd rbc, 0x1
- qui s'incrémententcountOfIterations
(malgré le fait que la boucle ne sera jamais sortie, donc cette valeur ne sera pas lue: peut-être est-elle nécessaire au cas où vous y pénétriez dans le débogueur), .. .test
instruction bizarre , qui n'est en fait là que pour l'accès à la mémoire (notez que ceeax
n'est même jamais défini dans la méthode!): c'est une page spéciale qui est définie sur non lisible lorsque la JVM veut déclencher tous les threads pour atteindre un point de restauration, afin qu'il puisse effectuer gc ou une autre opération qui nécessite que tous les threads soient dans un état connu.instance. a != instance.a
sorti la comparaison de la boucle, et ne l'exécute qu'une seule fois, avant que la boucle ne soit entrée! Il sait qu'il n'est pas nécessaire de rechargerinstance
oua
qu'ils ne sont pas déclarés volatils et qu'il n'y a pas d'autre code qui peut les changer sur le même thread, donc il suppose simplement qu'ils sont les mêmes pendant toute la boucle, ce qui est autorisé par la mémoire modèle.Non,
a != a
n'est pas thread-safe. Cette expression se compose de trois parties: chargera
, charger àa
nouveau et exécuter!=
. Il est possible pour un autre thread d'obtenir le verrou intrinsèque sura
le parent de et de changer la valeur dea
entre les 2 opérations de chargement.Un autre facteur est cependant de savoir si
a
est local. Sia
est local, aucun autre thread ne doit y avoir accès et doit donc être thread-safe.devrait également toujours imprimer
false
.Déclarer
a
commevolatile
ne résoudrait pas le problème pour ifa
isstatic
ou instance. Le problème n'est pas que les threads ont des valeurs différentes dea
, mais qu'un thread se chargea
deux fois avec des valeurs différentes. Cela peut en fait rendre le cas moins sûr pour les threads. Si cea
n'est pas le cas,volatile
ila
peut être mis en cache et une modification dans un autre thread n'affectera pas la valeur mise en cache.la source
synchronized
est faux: pour que ce code soit garanti à imprimerfalse
, toutes les méthodes définiesa
devraient l'êtresynchronized
également.a
le parent de la méthode pendant l'exécution de la méthode, nécessaire pour définir la valeura
.Concernant le comportement étrange:
Étant donné que la variable
a
n'est pas marquée commevolatile
, à un moment donné, la valeur dea
peut être mise en cache par le thread. Les deuxa
s dea != a
sont alors la version mise en cache et donc toujours le même (sensflag
est maintenant toujoursfalse
).la source
Même une simple lecture n'est pas atomique. Si
a
estlong
et n'est pas marqué commevolatile
alors sur les JVM 32 bitslong b = a
n'est pas thread-safe.la source