Pourquoi i++
N'est-il pas atomique en Java?
Pour aller un peu plus loin dans Java j'ai essayé de compter combien de fois la boucle dans les threads est exécutée.
Alors j'ai utilisé un
private static int total = 0;
dans la classe principale.
J'ai deux fils.
System.out.println("Hello from Thread 1!");
System.out.println("Hello from Thread 2!");
Et je compte les lignes imprimées par le fil 1 et le fil 2. Mais les lignes du fil 1 + les lignes du fil 2 ne correspondent pas au nombre total de lignes imprimées.
Voici mon code:
import Java.util.concurrent.ExecutorService;
import Java.util.concurrent.Executors;
import Java.util.logging.Level;
import Java.util.logging.Logger;
public class Test {
private static int total = 0;
private static int countT1 = 0;
private static int countT2 = 0;
private boolean run = true;
public Test() {
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
newCachedThreadPool.execute(t1);
newCachedThreadPool.execute(t2);
try {
Thread.sleep(1000);
}
catch (InterruptedException ex) {
Logger.getLogger(Test.class.getName()).log(Level.SEVERE, null, ex);
}
run = false;
try {
Thread.sleep(1000);
}
catch (InterruptedException ex) {
Logger.getLogger(Test.class.getName()).log(Level.SEVERE, null, ex);
}
System.out.println((countT1 + countT2 + " == " + total));
}
private Runnable t1 = new Runnable() {
@Override
public void run() {
while (run) {
total++;
countT1++;
System.out.println("Hello #" + countT1 + " from Thread 2! Total hello: " + total);
}
}
};
private Runnable t2 = new Runnable() {
@Override
public void run() {
while (run) {
total++;
countT2++;
System.out.println("Hello #" + countT2 + " from Thread 2! Total hello: " + total);
}
}
};
public static void main(String[] args) {
new Test();
}
}
i++
N'est probablement pas atomique dans Java car l'atomicité est une exigence spéciale qui n'est pas présente dans la majorité des utilisations de i++
. Cette exigence a une overhead: la réalisation d'une opération d'incrémentation est onéreuse, car elle implique une synchronisation aux niveaux logiciel et matériel qui ne doivent pas nécessairement figurer dans une incrémentation ordinaire.
Vous pouvez faire valoir que i++
Aurait dû être conçu et documenté comme effectuant spécifiquement un incrément atomique, de sorte qu'un incrément non atomique soit effectué à l'aide de i = i + 1
. Cependant, cela romprait la "compatibilité culturelle" entre Java, C et C++. En outre, cela enlèverait une notation pratique que les programmeurs habitués aux langages de type C prennent pour acquis, en lui donnant une signification spéciale qui ne s'applique que dans des circonstances limitées.
Un code C ou C++ de base tel que for (i = 0; i < LIMIT; i++)
se traduirait par Java par for (i = 0; i < LIMIT; i = i + 1)
; _), car il ne serait pas approprié d’utiliser l’atome i++
Pire encore, les programmeurs venant de C ou d’autres langages similaires à C auraient Java utiliserait de toute façon i++
, Ce qui entraînerait une utilisation inutile des instructions atomiques.
Même au niveau du jeu d'instructions machine, une opération de type incrément n'est pas atomique pour des raisons de performances. En x86, une instruction spéciale "préfixe de verrouillage" doit être utilisée pour rendre l'atome inc
atomique: pour les mêmes raisons que ci-dessus. Si inc
était toujours atomique, il ne serait jamais utilisé lorsqu'un inc non-atomique est requis; les programmeurs et les compilateurs génèrent du code qui charge, ajoute 1 et stocke, car ce serait beaucoup plus rapide.
Dans certaines architectures de jeux d'instructions, il n'y a pas de inc
ou peut-être pas de inc
atomique; pour faire une inc atomique sur MIPS, vous devez écrire une boucle logicielle utilisant le ll
et le sc
: liés à la charge et conditionnés au magasin. Load-linked lit le mot et le magasin conditionnel stocke la nouvelle valeur si le mot n'a pas changé ou sinon il échoue (ce qui est détecté et provoque une nouvelle tentative).
i++
implique deux opérations:
i
i
Quand deux threads effectuent i++
sur la même variable en même temps, ils peuvent tous deux obtenir la même valeur actuelle de i
, puis les incrémenter et les définir à i+1
_, vous obtiendrez donc une seule incrémentation au lieu de deux.
Exemple :
int i = 5;
Thread 1 : i++;
// reads value 5
Thread 2 : i++;
// reads value 5
Thread 1 : // increments i to 6
Thread 2 : // increments i to 6
// i == 6 instead of 7
L'important est la spécification du langage Java (JLS) plutôt que la manière dont diverses implémentations de la machine virtuelle Java peuvent ou non avoir implémenté une certaine fonctionnalité du langage. Le JLS définit l’opérateur ++ postfix dans la clause 15.14.2, qui indique i.a. "la valeur 1 est ajoutée à la valeur de la variable et la somme est stockée dans la variable". Nulle part il ne mentionne ou fait allusion au multithreading ou à l'atomicité. Pour ceux-ci le JLS fournit volatile et synchronisé . De plus, il existe le package Java.util.concurrent.atomic (voir http://docs.Oracle.com/javase/7 /docs/api/Java/util/concurrent/atomic/package-summary.html )
Pourquoi i ++ n'est-il pas atomique en Java?
Divisons l'opération d'incrémentation en plusieurs instructions:
Fil 1 et 2:
S'il n'y a pas de synchronisation, disons que Thread one a lu la valeur 3 et l'a incrémentée à 4, mais ne l'a pas écrite. À ce stade, le changement de contexte se produit. Le thread deux lit la valeur 3, l'incrémente et le changement de contexte se produit. Bien que les deux threads aient incrémenté la valeur totale, il s'agira toujours d'une condition de concurrence de 4.
i++
est une déclaration qui implique simplement 3 opérations:
Ces trois opérations ne sont pas destinées à être exécutées en une étape, autrement dit i++
n'est pas une opération composée . En conséquence, toutes sortes de problèmes peuvent se produire lorsque plusieurs threads sont impliqués dans une opération unique mais non composée.
A titre d'exemple, imaginez ce scénario:
Temps 1 :
Thread A fetches i
Thread B fetches i
Heure 2 :
Thread A overwrites i with a new value say -foo-
Thread B overwrites i with a new value say -bar-
Thread B stores -bar- in i
// At this time thread B seems to be more 'active'. Not only does it overwrite
// its local copy of i but also makes it in time to store -bar- back to
// 'main' memory (i)
Heure 3 :
Thread A attempts to store -foo- in memory effectively overwriting the -bar-
value (in i) which was just stored by thread B in Time 2.
Thread B has nothing to do here. Its work was done by Time 2. However it was
all for nothing as -bar- was eventually overwritten by another thread.
Et voila. Une condition de concurrence.
Voilà pourquoi i++
n'est pas atomique. Si c'était le cas, rien de tout cela ne serait arrivé et chaque fetch-update-store
arriverait atomiquement. C’est exactement ce à quoi AtomicInteger
est destiné et dans votre cas, cela conviendrait probablement.
P.S.
Voici un excellent livre traitant de toutes ces questions et d’autres: Java Concurrency in Practice
Il y a deux étapes:
ce n'est donc pas une opération atomique. Lorsque thread1 exécute i ++ et que thread2 exécute i ++, la valeur finale de i peut être i + 1.
Si l'opération i++
serait atomique, vous n'auriez pas la possibilité de lire la valeur. C’est exactement ce que vous voulez faire avec i++
(à la place d'utiliser ++i
).
Par exemple, regardez le code suivant:
public static void main(final String[] args) {
int i = 0;
System.out.println(i++);
}
Dans ce cas, nous nous attendons à ce que le résultat soit: 0
(parce que nous publions une incrémentation, par exemple, tout d'abord, puis une mise à jour)
C’est l’une des raisons pour lesquelles l’opération ne peut pas être atomique, car vous devez lire la valeur (et en faire quelque chose) et puis mettre à jour le valeur.
L'autre raison importante est que faire quelque chose de manière atomique généralement prend plus de temps à cause du verrouillage. Il serait idiot que toutes les opérations sur les primitives prennent un peu plus de temps dans les rares cas où les gens veulent avoir des opérations atomiques. C'est pourquoi ils ont ajouté AtomicInteger
et other classes atomiques au langage.
Dans la machine virtuelle Java, un incrément implique une lecture et une écriture, il n'est donc pas atomique.