Pourquoi i ++ n'est-il pas atomique?

97

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();
    }
}
Andie2302
la source
14
Pourquoi tu n'essaies pas avec AtomicInteger?
Braj
3
la JVM a une iincopé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.
Silly Freak
14
Pourquoi vous attendriez-vous même à ce que ce soit atomique?
Hot Licks
2
@Silly Freak: même s'il y avait une iincinstruction pour les champs, avoir une seule instruction ne garantit pas l'atomicité, par exemple l' accès non volatile longet doublechamp n'est pas garanti atomique indépendamment du fait qu'il est effectué par une seule instruction bytecode.
Holger

Réponses:

125

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 de i++. 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 utilisant i = 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 comme for (i = 0; i < LIMIT; i = i + 1); car il serait inapproprié d'utiliser l'atome i++. Ce qui est pire, les programmeurs venant de C ou d'autres langages de type C vers Java utiliseraient de i++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' incinstruction atomique: pour les mêmes raisons que ci-dessus. S'il incé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 incou peut-être pas incdu tout; pour faire un inc atomique sur MIPS, vous devez écrire une boucle logicielle qui utilise le lland sc: 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).

Kaz
la source
2
comme java n'a pas de pointeurs, l'incrémentation des variables locales est intrinsèquement thread save, donc avec les boucles, le problème ne serait généralement pas si grave. votre point sur la moindre surprise tient, bien sûr. aussi, tel quel, i = i + 1serait une traduction de ++i, pasi++
Silly Freak
22
Le premier mot de la question est "pourquoi". Pour l'instant, c'est la seule réponse pour aborder la question du «pourquoi». Les autres réponses ne font que reformuler la question. Donc +1.
Dawood ibn Kareem
3
Il pourrait être intéressant de noter qu'une garantie d'atomicité ne résoudrait pas le problème de visibilité pour les mises à jour des non- volatilechamps. Donc, à moins que vous ne traitiez chaque champ comme implicitement volatileune 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.
Holger
1
@DavidWallace n'est-ce pas ++? ;)
Dan Hlavenka
36

i++ implique deux opérations:

  1. lire la valeur actuelle de i
  2. incrémenter la valeur et l'affecter à 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 de i, puis l'incrémenter et la définir sur 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
Eran
la source
(Même si i++ c'était atomique, ce ne serait pas un comportement bien défini / thread-safe.)
user2864740
15
+1, mais "1. A, 2. B et C" sonne comme trois opérations, pas deux. :)
yshavit
3
Notez que même si l'opération a été implémentée avec une seule instruction machine qui a incrémenté un emplacement de stockage en place, il n'y a aucune garantie qu'elle serait thread-safe. La machine a encore besoin de récupérer la valeur, de l'incrémenter et de la stocker à nouveau, et il peut y avoir plusieurs copies de cache de cet emplacement de stockage.
Hot Licks
3
@Aquarelle - Si deux processeurs exécutent la même opération sur le même emplacement de stockage simultanément, et qu'il n'y a pas de diffusion de "réserve" sur l'emplacement, alors ils interféreront presque certainement et produiront des résultats faux. Oui, il est possible que cette opération soit "sûre", mais cela demande un effort particulier, même au niveau matériel.
Hot Licks
6
Mais je pense que la question était "Pourquoi" et non "Que se passe-t-il".
Sebastian Mach
11

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 )

Jonathan Rosenne
la source
5

Pourquoi i ++ n'est-il pas atomique en Java?

Décomposons l'opération d'incrémentation en plusieurs instructions:

Filetage 1 et 2:

  1. Récupérer la valeur totale de la mémoire
  2. Ajouter 1 à la valeur
  3. Réécrire dans la mémoire

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.

Aniket Thakur
la source
2
Je ne comprends pas comment cela devrait être une réponse à la question. Un langage peut définir n'importe quelle fonctionnalité comme atomique, qu'il s'agisse d'incréments ou de licornes. Vous illustrez simplement une conséquence de ne pas être atomique.
Sebastian Mach
Oui, un langage peut définir n'importe quelle fonctionnalité comme atomique, mais dans la mesure où java est considéré comme un opérateur d'incrémentation (qui est la question posée par OP) n'est pas atomique et ma réponse en indique les raisons.
Aniket Thakur
1
(désolé pour mon ton dur dans le premier commentaire) Mais alors, la raison semble être "parce que si ce serait atomique, alors il n'y aurait pas de conditions de course". Ie, il semble qu'une condition de concurrence soit souhaitable.
Sebastian Mach
@phresnel la surcharge introduite pour conserver un incrément atomique est énorme et rarement souhaitée, maintenir l'opération bon marché et par conséquent non atomique est la plupart du temps souhaitable.
josefx
4
@josefx: Notez que je ne remets pas en question les faits, mais le raisonnement de cette réponse. Il dit essentiellement "i ++ n'est pas atomique en Java à cause des conditions de course qu'il a" , ce qui revient à dire "une voiture n'a pas d'airbag à cause des accidents qui peuvent survenir" ou "vous n'avez pas de couteau avec votre commande de currywurst parce que le wurst devra peut-être être coupé " . Ainsi, je ne pense pas que ce soit une réponse. La question n'était pas "Que fait i ++?" ou "Quelle est la conséquence du fait que i ++ n'est pas synchronisé?" .
Sebastian Mach
5

i++ est une instruction qui implique simplement 3 opérations:

  1. Lire la valeur actuelle
  2. Ecrire une nouvelle valeur
  3. Stocker une nouvelle valeur

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 :

Thread A fetches i
Thread B fetches i

Temps 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)

Temps 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 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 cela AtomicIntegersert 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

kstratis
la source
1
Hmm. Un langage peut définir n'importe quelle fonctionnalité comme atomique, qu'il s'agisse d'incréments ou de licornes. Vous illustrez simplement une conséquence de ne pas être atomique.
Sebastian Mach
@phresnel Exactement. Mais je souligne également que ce n'est pas une opération unique qui, par extension, implique que le coût de calcul pour transformer plusieurs opérations de ce type en opérations atomiques est beaucoup plus cher, ce qui justifie en partie pourquoi il i++n'est pas atomique.
kstratis
1
Bien que je comprends votre point de vue, votre réponse est un peu déroutante pour l'apprentissage. Je vois un exemple et une conclusion qui dit "à cause de la situation dans l'exemple"; à mon humble avis c'est un raisonnement incomplet :(
Sebastian Mach
1
@phresnel Ce n'est peut-être pas la réponse la plus pédagogique mais c'est la meilleure que je puisse offrir actuellement. Espérons que cela aidera les gens et ne les confondra pas. Merci pour la critique cependant. J'essaierai d'être plus précis dans mes prochains articles.
kstratis
2

Dans la JVM, un incrément implique une lecture et une écriture, donc ce n'est pas atomique.

céleritas
la source
2

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 utilisant i++(au lieu 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 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é AtomicIntegeret d' autres cours atomiques à la langue.

Roy van Rijn
la source
2
C'est trompeur. Vous devez séparer l'exécution et l'obtention du résultat, sinon vous ne pourriez pas obtenir les valeurs d' une opération atomique.
Sebastian Mach
Non, c'est pourquoi AtomicInteger de Java a un get (), getAndIncrement (), getAndDecrement (), incrementAndGet (), decrementAndGet () etc.
Roy van Rijn
1
Et le langage Java aurait pu être défini i++pour être étendu i.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 ++.
Sebastian Mach
Étant donné un atomique, i++on peut créer trivialement un atomique ++iou vice-versa. L'un équivaut à l'autre plus un.
David Schwartz
2

Il y a deux étapes:

  1. chercher i de la mémoire
  2. définir i + 1 sur i

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.

Yanghaogn
la source
-1

La concurrence (la Threadclasse 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 chauve-souris
la source
-1 "Thread de classe publique ... Depuis: JDK1.0" Source: docs.oracle.com/javase/7/docs/api/index.html?java/lang
Silly Freak
La version n'a pas tant d'importance que le fait qu'elle était encore implémentée avant la classe Thread et qu'elle n'a pas été modifiée à cause de cela, mais j'ai modifié ma réponse pour vous plaire.
TheBat
5
Ce qui compte, c'est que votre affirmation "qu'elle était encore implémentée avant la classe Thread" ne soit pas étayée par les sources. i++ne pas être atomique est une décision de conception, pas un oubli dans un système en croissance.
Silly Freak
Lol c'est mignon. i ++ a été défini bien avant Threads, simplement parce qu'il y avait des langages qui existaient avant Java. Les créateurs de Java ont utilisé ces autres langages comme base au lieu de redéfinir une procédure bien acceptée. Où ai-je jamais dit que c'était un oubli?
TheBat
@SillyFreak Voici quelques sources qui montrent l'âge de ++: en.wikipedia.org/wiki/Increment_and_decrement_operators en.wikipedia.org/wiki/B_(programming_language)
TheBat