106 votes

Pourquoi i++ n'est pas atomique ?

Pourquoi est-ce que i++ non atomique en Java ?

Pour aller un peu plus loin dans Java, j'ai essayé de compter combien de fois les boucles dans les threads sont exécutées.

J'ai donc utilisé un

private static int total = 0;

dans la classe principale.

J'ai deux fils.

  • Fil conducteur 1 : Impressions System.out.println("Hello from Thread 1!");
  • Fil conducteur 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();
    }
}

14 votes

Pourquoi n'essayez-vous pas avec AtomicInteger ?

7 votes

0 votes

@user2864740, pourquoi dites-vous que AtomicInteger n'est pas atomique ? Vous pouvez utiliser le [getAndIncrement](http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/atomic/AtomicInteger.html#getAndIncrement()) pour ce faire. Elle est atomique.

133voto

Kaz Points 18072

i++ n'est probablement pas atomique en 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 un surcoût significatif : rendre une opération d'incrémentation atomique a un coût important ; cela implique une synchronisation à la fois au niveau logiciel et matériel qui n'a pas besoin d'être présente dans une incrémentation ordinaire.

On pourrait dire 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é en utilisant la fonction i = i + 1 . Toutefois, cela romprait la "compatibilité culturelle" entre Java, d'une part, et C et C++, d'autre part. De plus, cela supprimerait une notation pratique que les programmeurs familiers des langages de type C considèrent comme acquise, en lui donnant une signification spéciale qui ne s'applique que dans des circonstances limitées.

Code de base C ou C++ comme for (i = 0; i < LIMIT; i++) se traduirait en Java par for (i = 0; i < LIMIT; i = i + 1) parce qu'il serait inapproprié d'utiliser la méthode atomique. i++ . Pire encore, les programmeurs passant de C ou d'autres langages similaires à Java utiliseraient i++ de toute façon, ce qui entraîne une utilisation inutile des instructions atomiques.

Même au niveau du jeu d'instructions de la 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'opération de type inc instruction atomique : pour les mêmes raisons que ci-dessus. Si inc serait toujours atomique, il ne serait jamais utilisé lorsqu'un inc non atomique est nécessaire ; les programmeurs et les compilateurs généreraient du code qui charge, ajoute 1 et stocke, car cela serait bien plus rapide.

Dans certaines architectures de jeux d'instructions, il n'y a pas d'atomicité. inc ou peut-être pas inc du tout ; pour faire une incrémentation atomique sur MIPS, vous devez écrire une boucle logicielle qui utilise la fonction ll y sc Il s'agit de : load-linked, et store-conditional. Load-linked lit le mot, et store-conditional 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).

2 votes

Comme java n'a pas de pointeurs, l'incrémentation des variables locales est intrinsèquement sauvegardée par les threads, donc avec des boucles, le problème ne serait pas si grave. votre remarque sur la moindre surprise reste bien sûr valable. aussi, tel quel, i = i + 1 serait une traduction de ++i pas i++

23 votes

Le premier mot de la question est "pourquoi". Pour l'instant, c'est la seule réponse qui aborde la question du "pourquoi". Les autres réponses ne font que reformuler la question. Donc +1.

3 votes

Il peut être intéressant de noter qu'une garantie d'atomicité ne résoudrait pas le problème de la visibilité pour les mises à jour de données non-SGP. volatile champs. Donc, à moins que vous ne traitiez chaque champ comme implicitement volatile une fois qu'un thread a utilisé le ++ sur elle, une telle garantie d'atomicité ne résoudrait pas les problèmes de mise à jour simultanée. Alors pourquoi gaspiller potentiellement les performances pour quelque chose si cela ne résout pas le problème.

39voto

Eran Points 35360

i++ implique deux opérations :

  1. lire la valeur actuelle de i
  2. incrémenter la valeur et l'affecter à i

Lorsque deux threads exécutent i++ sur la même variable au même moment, ils peuvent tous deux obtenir la même valeur actuelle de i puis l'incrémenter et le fixer à i+1 Vous n'aurez donc qu'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

0 votes

(Même si i++ était atomique, ce ne serait pas un comportement bien défini/sûr pour les threads).

15 votes

+1, mais "1. A, 2. B et C" sonne comme trois opérations, pas deux. :)

3 votes

Notez que même si l'opération était implémentée avec une seule instruction machine qui incrémente un emplacement de stockage en place, il n'y a aucune garantie qu'elle soit thread-safe. La machine doit toujours aller chercher la valeur, l'incrémenter et la stocker à nouveau, plus il peut y avoir plusieurs copies de cache de cet emplacement de stockage.

14voto

Jonathan Rosenne Points 407

Spécification Java

L'important, c'est le JLS (Spécification du langage Java) plutôt que la façon dont diverses implémentations de la JVM peuvent ou non avoir implémenté une certaine fonctionnalité du langage.

Le JLS définit l'opérateur postfixe ++ dans la clause 15.14.2 qui dit entre autres : "la valeur 1 est ajoutée à la valeur de la variable et la somme est réenregistrée dans la variable". Il ne fait nulle part mention ou allusion au multithreading ou à l'atomicité.

Pour le multithreading ou l'atomicité, le JLS fournit volatile y synchronisé . En outre, il y a les Atomic… classes.

6voto

Aniket Thakur Points 10135

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

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

Fils 1 & 2 :

  1. Récupérer la valeur du total en mémoire
  2. Ajouter 1 à la valeur
  3. Réécriture 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 deuxième thread 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, elle sera toujours de 4 - condition de course.

3 votes

Je ne comprends pas en quoi cela devrait être une réponse à la question. Un langage peut définir n'importe quelle fonctionnalité comme atomique, que ce soit des incréments ou des licornes. Vous ne faites qu'illustrer une conséquence de ne pas être atomique.

0 votes

Oui, un langage peut définir n'importe quelle fonctionnalité comme atomique mais pour autant que java soit considéré comme un opérateur d'incrémentation (qui est la question posée par le PO) n'est pas atomique et ma réponse en donne les raisons.

2 votes

(désolé pour mon ton dur dans le premier commentaire) Mais ensuite, la raison semble être "parce que si c'était atomique, alors il n'y aurait pas de conditions de course". C'est-à-dire qu'il semble qu'une condition de course soit souhaitable.

5voto

Konos5 Points 490

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

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

Ces trois opérations ne sont pas destinées à être exécutées en une seule fois ou en d'autres termes i++ n'est pas un composé opération. Par conséquent, toutes sortes de choses peuvent mal tourner lorsque plusieurs threads sont impliqués dans une opération unique mais non composée.

Considérons 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 voilà, vous l'avez. Une condition de course.


C'est pourquoi i++ n'est pas atomique. S'il l'était, rien de tout cela ne serait arrivé et chaque fetch-update-store se produirait de manière atomique. C'est exactement ce que AtomicInteger et dans votre cas, il s'adapterait probablement parfaitement.

P.S.

Voici un excellent livre qui couvre toutes ces questions et bien d'autres encore : Java Concurrency en pratique

2 votes

Hmm. Un langage peut définir n'importe quelle fonctionnalité comme atomique, que ce soit des incréments ou des licornes. Vous illustrez juste une conséquence de ne pas être atomique.

0 votes

@phresnel Exactement. Mais je souligne également qu'il ne s'agit pas d'une opération unique, ce qui, par extension, implique que le coût de calcul pour transformer de multiples opérations de ce type en opérations atomiques est beaucoup plus élevé, ce qui justifie -partiellement- la raison pour laquelle l'application de la loi sur l'accès à l'information et la protection de la vie privée a été mise en place. i++ n'est pas atomique.

2 votes

Bien que je comprenne votre point de vue, votre réponse est un peu confuse pour l'apprentissage. Je vois un exemple, et une conclusion qui dit "à cause de la situation dans l'exemple" ; à mon avis, c'est un raisonnement incomplet :(

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X