2010-11-09 26 views
8

Donc, je viens d'apprendre sur le mot-clé volatile tout en écrivant quelques exemples pour une section que je suis TA demain. J'ai écrit un programme rapide pour démontrer que les opérations ++ et - ne sont pas atomiques.Pourquoi le marquage d'une variable Java volatile rend les choses moins synchronisées?

public class Q3 { 

    private static int count = 0; 

    private static class Worker1 implements Runnable{ 

     public void run(){ 
      for(int i = 0; i < 10000; i++) 
       count++; //Inner class maintains an implicit reference to its parent 
     } 
    } 

    private static class Worker2 implements Runnable{ 

     public void run(){ 
      for(int i = 0; i < 10000; i++) 
       count--; //Inner class maintains an implicit reference to its parent 
     } 
    } 


    public static void main(String[] args) throws InterruptedException { 
     while(true){ 
      Thread T1 = new Thread(new Worker1()); 
      Thread T2 = new Thread(new Worker2()); 
      T1.start(); 
      T2.start(); 

      T1.join(); 
      T2.join(); 
      System.out.println(count); 
      count = 0; 
      Thread.sleep(500); 

     } 
    } 
} 

Comme prévu la sortie de ce programme est généralement le long des lignes de:

-1521 
    -39 
    0 
    0 
    0 
    0 
    0 
    0 

Cependant, quand je change:

private static int count = 0; 

à

private static volatile int count = 0; 

ma la sortie passe à:

0 
3077 
    1 
-3365 
    -1 
    -2 
2144 
    3 
    0 
    -1 
    1 
    -2 
    6 
    1 
    1 

J'ai lu When exactly do you use the volatile keyword in Java? donc je sens que j'ai une compréhension de base de ce que le mot-clé ne (maintenir la synchronisation à travers des copies mises en cache d'une variable dans différents threads, mais est en lecture update-écriture pas en sécurité) . Je comprends que ce code n'est, bien sûr, pas sécurisé. Il est spécifiquement non thread-safe d'agir comme un exemple pour mes étudiants. Cependant, je suis curieux de savoir pourquoi l'ajout du mot-clé volatile rend la sortie moins «stable» que lorsque le mot-clé n'est pas présent.

Répondre

1

La raison de tous ces zéros est et non que les ++ et s s'équilibrent les uns les autres. La raison en est qu'il n'y a rien ici pour provoquer count dans les threads en boucle pour affecter count dans le thread principal. Vous avez besoin de blocs de synchronisation ou d'un count volatile (une «barrière de mémoire») pour forcer la JVM à faire en sorte que tout soit identique: avec votre JVM/matériel, il est très probable que la valeur soit conservée dans un registre à tout moment. sans jamais obtenir de cache - et encore moins la mémoire principale - tout

Dans le second cas, vous faites ce que vous aviez l'intention:. incréments non atomiques et décrémente sur le même course et obtenir des résultats quelque chose comme ce que vous attendiez

.

C'est une question ancienne, mais quelque chose qui devait être dit sur chaque fil en gardant son propre , copie indépendante des données.

+0

Merci pour cette réponse. En dépit de demander il y a trois ans c'est la meilleure explication que j'ai entendue. – JohnS

+0

Ceci est complètement faux Thread.start et Thread.join créent des "barrières de mémoire". http://docs.oracle.com/javase/tutorial/essential/concurrency/memconsist.html voir ma réponse pour le bon. – Oleg

+0

@Oleg: Les barrières de mémoire Thread start & finish n'auront aucun effet pendant une course parallèle. Alors que la JVM peut montrer à chaque thread la même valeur dans le 'count' non-volatile, elle n'a pas besoin de le faire et ne le fera généralement pas. Je peux bien croire qu'une JVM pourrait exécuter les deux threads en séquence plutôt qu'en parallèle, bien que vous ayez toujours deux valeurs différentes parmi les trois copies de 'count'. Une fois que vous vous êtes éloigné de ce que le langage Java demande réellement, les choses peuvent varier beaucoup en fonction de la machine, de ce qui est en cours d'exécution, de la date et de l'heure, et de la JVM. – RalphChapin

5

La question «pourquoi le code est-il pire?» Avec le mot clé volatile n'est pas une question valide. Il se comporte différemment en raison du modèle de mémoire différent qui est utilisé pour les champs volatiles. Le fait que la sortie de votre programme tend vers 0 sans le mot-clé ne peut pas être invoqué et si vous êtes passé à une architecture différente avec un thread CPU différent ou un nombre de processeurs, des résultats très différents ne seraient pas rares.

Aussi, il est important de se rappeler que bien que x++ semble atomique, il s'agit en fait d'une opération de lecture/modification/écriture. Si vous exécutez votre programme de test sur un certain nombre d'architectures différentes, vous trouverez des résultats différents car la manière dont la JVM implémente volatile dépend fortement du matériel. L'accès aux champs volatile peut également être sensiblement plus lent que l'accès aux champs mis en cache - parfois par 1 ou 2 ordres de grandeur, ce qui va changer le timing de votre programme.

L'utilisation du mot-clé volatile fait ériger une barrière de mémoire pour le champ spécifique et (à partir de Java 5) cette barrière de mémoire est étendue à toutes les autres variables partagées. Cela signifie que la valeur des variables sera copiée dans/hors du stockage central lors de l'accès. Cependant, il existe des différences subtiles entre volatile et le mot-clé synchronized en Java. Par exemple, il n'y a pas de verrouillage avec volatile donc si plusieurs threads mettent à jour une variable volatile, les conditions de course existeront autour des opérations non-atomiques.

Voici une bonne lecture sur le sujet:

Hope this helps.

+1

Le mot clé volatile met en œuvre la synchronisation, mais sur chaque opération atomique: http://www.javamex.com/tutorials/synchronization_volatile.shtml. Les conditions de course ne vont pas s'améliorer si vous essayez de faire des opérations non atomiques sans autre synchronisation. –

+1

Il n'y a pas de "synchronisation" réelle (dans le contexte java habituel au moins). Comme le dit le lien, cela ne fait que "agir comme s'il était enfermé dans un bloc synchronisé". Personnellement, je souhaite que l'auteur se soit abstenu d'utiliser le mot tout à fait. – Mania

+2

Je n'aime vraiment pas la définition de volatile qui inclut le s-mot Zack. Je comprends que vous n'obtiendrez pas d'état partiel lorsque vous lisez un volatile - vous obtiendrez la valeur la plus à jour du stockage global - mais ceci est très différent du comportement du mot-clé synchronisé que l'utilisation de ce mot est très trompeur. Je pense que le lien d'IBM que j'ai posté est meilleur que la description de javamex. – Gray

4

Une estimation éclairée de ce que vous voyez: lorsqu'elle n'est pas marquée comme volatile, le compilateur JIT utilise les opérations x86 inc/dec qui peuvent mettre à jour la variable de manière atomique. Une fois marquées comme volatiles, ces opérations ne sont plus utilisées et la variable est à la place lue, incrémentée/décrémentée, puis finalement écrite entraînant plus d '"erreurs".

La configuration non volatile ne garantit pas le bon fonctionnement - sur une architecture différente, elle peut être pire que lorsqu'elle est marquée comme volatile. Marquer le terrain volatile ne commence pas à résoudre les problèmes de course présents ici.

Une solution consisterait à utiliser la classe AtomicInteger, qui autorise les incréments/décréments atomiques.

+0

"_qui peut mettre à jour la variable atomiquement_" Non WRT atomiquement d'autres processeurs/cœurs. – curiousguy

2

Les variables volatiles agissent comme si chaque interaction était incluse dans un bloc synchronisé. Comme vous l'avez mentionné, l'incrémentation et la décrémentation ne sont pas atomiques, ce qui signifie que chaque incrément et décrément contient deux régions synchronisées (la lecture et l'écriture). Je soupçonne que l'ajout de ces pseudolocks augmente les chances que les opérations soient en conflit.

En général, les deux threads auraient un décalage aléatoire par rapport à un autre, ce qui signifie que la probabilité que l'un des deux écrase l'autre soit pair. Mais la synchronisation imposée par les volatiles peut les forcer à être inverses, ce qui, s'ils sont enchevêtrés dans le mauvais sens, augmente les chances d'un incrément ou d'un décrément manqué. De plus, une fois qu'ils sont dans cette position, la synchronisation rend moins probable qu'ils s'en détachent, ce qui augmente l'écart.

0

Cela ne «rend pas les choses moins synchronisées». Cela les rend plus synchronisés, en ce que les threads verront toujours une valeur à jour pour la variable. Cela nécessite l'érection de barrières de mémoire, qui ont un coût en temps.

1

Si vous voyez une valeur de count qui est et non un multiple de 10000, cela indique simplement que vous avez un mauvais optimiseur.

2

Je suis tombé sur cette question et après avoir joué avec le code pour un peu trouvé une réponse très simple.

Après chaud initiale et optimisations (les 2 premiers chiffres avant les zéros) lorsque la machine virtuelle Java fonctionne à pleine vitesse T1 simplement commence et se termine avantT2 commence même, si count va tout le chemin jusqu'à 10000 et puis à 0. Lorsque j'ai changé le nombre d'itérations dans les threads de travail de 10000 à 100000000, la sortie est très instable et différente à chaque fois.

La raison de la sortie instable lors de l'ajout volatile est qu'il rend le code beaucoup plus lent et même avec 10000 itérations T2 a suffisamment de temps pour commencer et interférer avec T1.