Pourquoi n'est i++
pas atomique en Java?
Pour approfondir un peu Java, j'ai essayé de compter la fréquence à laquelle 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.
- Sujet 1: Impressions
System.out.println("Hello from Thread 1!");
- Sujet 2: Impressions
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();
}
}
java
multithreading
concurrency
Andie2302
la source
la source
AtomicInteger
?iinc
opération pour incrémenter les entiers, mais cela ne fonctionne que pour les variables locales, où la concurrence n'est pas un problème. Pour les champs, le compilateur génère des commandes de lecture-modification-écriture séparément.iinc
instruction pour les champs, avoir une seule instruction ne garantit pas l'atomicité, par exemple l' accès nonvolatile
long
etdouble
champ n'est pas garanti atomique indépendamment du fait qu'il est effectué par une seule instruction bytecode.Réponses:
i++
n'est probablement pas atomique en Java car l'atomicité est une exigence particulière qui n'est pas présente dans la majorité des utilisations dei++
. Cette exigence a une surcharge importante: il y a un coût important pour rendre une opération d'incrémentation atomique; cela implique une synchronisation aux niveaux logiciel et matériel qui n'ont pas besoin d'être présents dans un incrément ordinaire.Vous pouvez faire de l'argument qui
i++
aurait dû être conçu et documenté comme exécutant spécifiquement un incrément atomique, de sorte qu'un incrément non atomique soit effectué en utilisanti = i + 1
. Cependant, cela briserait la «compatibilité culturelle» entre Java et C et C ++. De plus, cela enlèverait une notation pratique que les programmeurs familiers avec les langages de type C tiennent pour acquise, lui donnant une signification spéciale qui ne s'applique que dans des circonstances limitées.Un code C ou C ++ de base comme
for (i = 0; i < LIMIT; i++)
serait traduit en Java commefor (i = 0; i < LIMIT; i = i + 1)
; car il serait inapproprié d'utiliser l'atomei++
. Ce qui est pire, les programmeurs venant de C ou d'autres langages de type C vers Java utiliseraient dei++
toute façon, ce qui entraînerait une utilisation inutile d'instructions atomiques.Même au niveau du jeu d'instructions machine, une opération de type incrément n'est généralement pas atomique pour des raisons de performances. En x86, une instruction spéciale "lock prefix" doit être utilisée pour rendre l'
inc
instruction atomique: pour les mêmes raisons que ci-dessus. S'ilinc
était toujours atomique, il ne serait jamais utilisé lorsqu'un inc non atomique est requis; les programmeurs et les compilateurs généreraient du code qui se charge, ajoute 1 et stocke, car ce serait beaucoup plus rapide.Dans certaines architectures de jeu d'instructions, il n'y a pas d'atome
inc
ou peut-être pasinc
du tout; pour faire un inc atomique sur MIPS, vous devez écrire une boucle logicielle qui utilise lell
andsc
: load-linked et store-conditionitional. Load-linked lit le mot et store-conditionitional stocke la nouvelle valeur si le mot n'a pas changé, ou bien il échoue (ce qui est détecté et provoque une nouvelle tentative).la source
i = i + 1
serait une traduction de++i
, pasi++
volatile
champs. Donc, à moins que vous ne traitiez chaque champ comme implicitementvolatile
une fois qu'un thread a utilisé l'++
opérateur dessus, une telle garantie d'atomicité ne résoudrait pas les problèmes de mise à jour simultanée. Alors pourquoi potentiellement gaspiller des performances pour quelque chose si cela ne résout pas le problème.++
? ;)i++
implique deux opérations:i
i
Lorsque deux threads fonctionnent
i++
sur la même variable en même temps, ils peuvent tous les deux obtenir la même valeur actuelle dei
, puis l'incrémenter et la définir suri+1
, vous obtiendrez donc une seule incrémentation au lieu de deux.Exemple :
la source
i++
c'était atomique, ce ne serait pas un comportement bien défini / thread-safe.)L'important est le JLS (spécification du langage Java) plutôt que la manière dont diverses implémentations de la JVM peuvent avoir implémenté ou non une certaine fonctionnalité du langage. Le JLS définit l'opérateur ++ postfix dans la clause 15.14.2 qui dit ia "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 n'indique le multithreading ou l'atomicité. Pour ces derniers, 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 )
la source
Décomposons l'opération d'incrémentation en plusieurs instructions:
Filetage 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 réé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 augmenté la valeur totale, ce sera toujours 4 - condition de concurrence.
la source
i++
est une instruction qui implique simplement 3 opérations:Ces trois opérations ne sont pas destinées à être exécutées en une seule étape ou, en d'autres termes,
i++
ne sont pas opération composée . En conséquence, toutes sortes de choses peuvent mal tourner lorsque plusieurs threads sont impliqués dans une opération unique mais non composée.Considérez le scénario suivant:
Temps 1 :
Temps 2 :
Temps 3 :
Et voila. Une condition de course.
C'est pourquoi ce
i++
n'est pas atomique. Si c'était le cas, rien de tout cela ne serait arrivé et chacunfetch-update-store
se produirait de manière atomique. C'est exactement ce à quoi celaAtomicInteger
sert et dans votre cas, cela s'intégrerait probablement parfaitement.PS
Voici un excellent livre couvrant tous ces problèmes et certains d'entre eux: Java Concurrency in Practice
la source
i++
n'est pas atomique.Dans la JVM, un incrément implique une lecture et une écriture, donc ce n'est pas atomique.
la source
Si l'opération
i++
était atomique, vous n'auriez pas la possibilité d'en lire la valeur. C'est exactement ce que vous voulez faire en utilisanti++
(au lieu d'utiliser++i
).Par exemple, regardez le code suivant:
Dans ce cas, nous nous attendons à ce que la sortie soit:
0
(car nous publions un incrément, par exemple, première lecture, puis 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 faire quelque chose avec), puis mettre à jour la valeur.
L'autre raison importante est que faire quelque chose de manière atomique prend généralement plus de temps à cause du verrouillage. Il serait ridicule 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. Voilà pourquoi ils ont ajouté
AtomicInteger
et d' autres cours atomiques à la langue.la source
i++
pour être étendui.getAndIncrement()
. Une telle expansion n'est pas nouvelle. Par exemple, les lambdas en C ++ sont développés en définitions de classes anonymes en C ++.i++
on peut créer trivialement un atomique++i
ou vice-versa. L'un équivaut à l'autre plus un.Il y a deux étapes:
donc ce n'est pas une opération atomique. Lorsque thread1 exécute i ++ et thread2 exécute i ++, la valeur finale de i peut être i + 1.
la source
La concurrence (la
Thread
classe et autres) est une fonctionnalité ajoutée dans la v1.0 de Java .i++
a été ajouté dans la version bêta avant cela, et en tant que tel, il est encore plus que probable dans sa mise en œuvre originale (plus ou moins).Il appartient au programmeur de synchroniser les variables. Consultez le tutoriel d'Oracle à ce sujet .
Edit: Pour clarifier, i ++ est une procédure bien définie qui précède Java, et en tant que telle, les concepteurs de Java ont décidé de conserver la fonctionnalité d'origine de cette procédure.
L'opérateur ++ a été défini dans B (1969) qui est antérieur à java et au threading d'un peu.
la source
i++
ne pas être atomique est une décision de conception, pas un oubli dans un système en croissance.