Les variables statiques sont-elles partagées entre les threads?

95

Mon professeur dans une classe Java de niveau supérieur sur le threading a dit quelque chose dont je n'étais pas sûr.

Il a déclaré que le code suivant ne mettrait pas nécessairement à jour la readyvariable. Selon lui, les deux threads ne partagent pas nécessairement la variable statique, en particulier dans le cas où chaque thread (thread principal versus ReaderThread) tourne sur son propre processeur et ne partage donc pas les mêmes registres / cache / etc et un seul processeur ne mettra pas à jour l'autre.

Essentiellement, il a dit qu'il est possible que ce readysoit mis à jour dans le thread principal, mais PAS dans le ReaderThread, de sorte que ReaderThreadcela boucle indéfiniment.

Il a également affirmé qu'il était possible pour le programme d'imprimer 0ou 42. Je comprends comment 42pourrait être imprimé, mais pas 0. Il a mentionné que ce serait le cas lorsque la numbervariable est définie sur la valeur par défaut.

J'ai pensé qu'il n'était peut-être pas garanti que la variable statique soit mise à jour entre les threads, mais cela me semble très étrange pour Java. Est-ce que faire readyvolatile corrige ce problème?

Il a montré ce code:

public class NoVisibility {  
    private static boolean ready;  
    private static int number;  
    private static class ReaderThread extends Thread {   
        public void run() {  
            while (!ready)   Thread.yield();  
            System.out.println(number);  
        }  
    }  
    public static void main(String[] args) {  
        new ReaderThread().start();  
        number = 42;  
        ready = true;  
    }  
}
dontocsata
la source
La visibilité des variables non locales ne dépend pas du fait qu'il s'agisse de variables statiques, de champs d'objet ou d'éléments de tableau, elles ont toutes les mêmes considérations. (Avec le problème que les éléments de tableau ne peuvent pas être rendus volatils.)
Paŭlo Ebermann
1
demandez à votre professeur quel type d'architecture il juge possible de voir «0». Pourtant, en théorie, il a raison.
bestsss
4
@bestsss Poser ce genre de question révélerait au professeur qu'il avait raté tout ce qu'il disait. Le fait est que les programmeurs compétents comprennent ce qui est garanti et ce qui ne l'est pas et ne se fient pas à des choses qui ne sont pas garanties, du moins pas sans comprendre précisément ce qui n'est pas garanti et pourquoi.
David Schwartz
Ils sont partagés entre tout ce qui est chargé par le même chargeur de classe. Y compris les fils.
Marquis of Lorne
Votre professeur (et la réponse acceptée) ont raison à 100%, mais je mentionnerai que cela arrive rarement - c'est le genre de problème qui se cachera pendant des années et ne se manifestera que lorsqu'il serait le plus dangereux. Même de courts tests essayant d'exposer le problème ont tendance à agir comme si tout allait bien (probablement parce qu'ils n'ont pas le temps pour la JVM de faire beaucoup d'optimisation), c'est donc un très bon problème à prendre en compte.
Bill K

Réponses:

75

Il n'y a rien de spécial à propos des variables statiques en matière de visibilité. S'ils sont accessibles, n'importe quel thread peut les atteindre, vous êtes donc plus susceptible de rencontrer des problèmes de concurrence car ils sont plus exposés.

Il existe un problème de visibilité imposé par le modèle de mémoire de la JVM. Voici un article sur le modèle de mémoire et sur la façon dont les écritures deviennent visibles pour les threads . Vous ne pouvez pas compter sur les changements qu'un thread rend visibles aux autres threads en temps opportun (en fait, la JVM n'a aucune obligation de rendre ces modifications visibles pour vous, à aucun moment), sauf si vous établissez une relation qui se produit avant .

Voici une citation de ce lien (fournie dans le commentaire de Jed Wesley-Smith):

Le chapitre 17 de la spécification du langage Java définit la relation d'avance sur les opérations de mémoire telles que les lectures et écritures de variables partagées. Les résultats d'une écriture par un thread sont garantis d'être visibles pour une lecture par un autre thread uniquement si l'opération d'écriture se produit avant l'opération de lecture. Les constructions synchronisées et volatiles, ainsi que les méthodes Thread.start () et Thread.join (), peuvent former des relations qui se produisent avant. En particulier:

  • Chaque action d'un thread se produit avant chaque action de ce thread qui vient plus tard dans l'ordre du programme.

  • Un déverrouillage (bloc synchronisé ou sortie de méthode) d'un moniteur se produit avant chaque verrouillage suivant (bloc synchronisé ou entrée de méthode) de ce même moniteur. Et comme la relation arrive-avant est transitive, toutes les actions d'un thread avant le déverrouillage se produisent avant toutes les actions consécutives à un verrouillage de thread qui surveille.

  • Une écriture dans un champ volatil se produit avant chaque lecture ultérieure de ce même champ. Les écritures et lectures de champs volatils ont des effets de cohérence de mémoire similaires à ceux des moniteurs d'entrée et de sortie, mais n'impliquent pas de verrouillage d'exclusion mutuelle.

  • Un appel pour démarrer sur un thread se produit avant toute action dans le thread démarré.

  • Toutes les actions d'un thread se produisent avant que tout autre thread ne retourne avec succès d'une jointure sur ce thread.

Nathan Hughes
la source
3
Dans la pratique, «en temps opportun» et «jamais» sont synonymes. Il serait très possible que le code ci-dessus ne se termine jamais.
TREE
4
En outre, cela démontre un autre anti-modèle. N'utilisez pas volatile pour protéger plus d'un élément d'état partagé. Ici, nombre et prêt sont deux éléments d'état, et pour les mettre à jour / les lire tous les deux de manière cohérente, vous avez besoin d'une synchronisation réelle.
TREE
5
Le fait de devenir finalement visible est faux. Sans aucune relation explicite qui se produit avant, il n'y a aucune garantie que toute écriture sera jamais vue par un autre thread, car le JIT est tout à fait dans son droit d'imposer la lecture dans un registre et vous ne verrez jamais aucune mise à jour. Toute charge éventuelle est de la chance et ne doit pas être invoquée.
Jed Wesley-Smith
2
"sauf si vous utilisez le mot clé volatile ou synchronisez." devrait se lire "à moins qu'il n'y ait une relation pertinente entre l'auteur et le lecteur" et ce lien: download.oracle.com/javase/6/docs/api/java/util/concurrent/…
Jed Wesley-Smith
2
@bestsss bien repéré. Malheureusement, ThreadGroup est cassé à bien des égards.
Jed Wesley-Smith
37

Il parlait de visibilité et de ne pas être pris au pied de la lettre.

Les variables statiques sont en effet partagées entre les threads, mais les modifications apportées dans un thread peuvent ne pas être immédiatement visibles par un autre thread, ce qui donne l'impression qu'il y a deux copies de la variable.

Cet article présente une vue cohérente avec la façon dont il a présenté les informations:

Tout d'abord, vous devez comprendre un petit quelque chose sur le modèle de mémoire Java. J'ai eu un peu de mal au fil des ans à l'expliquer brièvement et bien. À partir d'aujourd'hui, la meilleure façon dont je puisse penser pour le décrire est de l'imaginer de cette façon:

  • Chaque thread en Java a lieu dans un espace mémoire séparé (c'est clairement faux, alors soyez indulgents avec moi sur celui-ci).

  • Vous devez utiliser des mécanismes spéciaux pour garantir que la communication se produit entre ces threads, comme vous le feriez sur un système de transmission de messages.

  • Les écritures en mémoire qui se produisent dans un thread peuvent «fuir» et être vues par un autre thread, mais ce n'est en aucun cas garanti. Sans communication explicite, vous ne pouvez pas garantir quelles écritures sont vues par d'autres threads, ni même l'ordre dans lequel elles sont vues.

...

modèle de fil

Mais encore une fois, il s'agit simplement d'un modèle mental pour penser au threading et à la volatilité, pas littéralement au fonctionnement de la JVM.

Bert F
la source
12

Fondamentalement, c'est vrai, mais en fait, le problème est plus complexe. La visibilité des données partagées peut être affectée non seulement par les caches CPU, mais aussi par l'exécution dans le désordre des instructions.

Par conséquent, Java définit un modèle de mémoire , qui indique dans quelles circonstances les threads peuvent voir l'état cohérent des données partagées.

Dans votre cas particulier, l'ajout volatilegarantit la visibilité.

axtavt
la source
8

Ils sont bien sûr «partagés» dans le sens où ils se réfèrent tous les deux à la même variable, mais ils ne voient pas nécessairement les mises à jour de l'autre. Cela est vrai pour n'importe quelle variable, pas seulement statique.

Et en théorie, les écritures effectuées par un autre thread peuvent sembler être dans un ordre différent, à moins que les variables ne soient déclarées volatileou que les écritures ne soient explicitement synchronisées.

biziclop
la source
4

Dans un seul chargeur de classe, les champs statiques sont toujours partagés. Pour étendre explicitement les données aux threads, vous souhaitez utiliser une fonction telle que ThreadLocal.

Kirk Woll
la source
2

Lorsque vous initialisez la variable de type primitif statique, java default attribue une valeur aux variables statiques

public static int i ;

lorsque vous définissez la variable comme ceci, la valeur par défaut de i = 0; c'est pourquoi il y a une possibilité de vous obtenir 0. alors le thread principal met à jour la valeur de boolean prêt à true. puisque ready est une variable statique, le thread principal et l'autre thread font référence à la même adresse mémoire, donc la variable ready change. ainsi le fil secondaire sort de la boucle while et imprime la valeur. lors de l'impression de la valeur initialisée, la valeur du nombre est 0. si le processus de thread a réussi la boucle while avant la variable numéro de mise à jour du thread principal. alors il est possible d'imprimer 0

NuOne
la source
-2

@dontocsata vous pouvez retourner voir votre professeur et le scolariser un peu :)

quelques notes du monde réel et peu importe ce que vous voyez ou ce qu'on vous dit. Veuillez noter que les mots ci-dessous concernent ce cas particulier dans l'ordre exact indiqué.

Les 2 variables suivantes résideront sur la même ligne de cache sous pratiquement n'importe quelle architecture connue.

private static boolean ready;  
private static int number;  

Thread.exit(thread principal) est garanti pour quitter et exitest garanti pour provoquer une barrière de mémoire, en raison de la suppression du thread de groupe de threads (et de nombreux autres problèmes). (c'est un appel synchronisé, et je ne vois pas de moyen unique d'être implémenté sans la partie de synchronisation puisque le ThreadGroup doit également se terminer s'il ne reste aucun thread démon, etc.).

Le thread démarré ReaderThreadva maintenir le processus en vie puisqu'il ne s'agit pas d'un démon! Ainsi readyet numberseront vidés ensemble (ou le nombre avant si un changement de contexte se produit) et il n'y a aucune vraie raison de réorganiser dans ce cas au moins je ne peux même pas penser à un. Vous aurez besoin de quelque chose de vraiment bizarre pour voir autre chose 42. Encore une fois, je suppose que les deux variables statiques seront dans la même ligne de cache. Je ne peux pas imaginer une ligne de cache de 4 octets de long OU une JVM qui ne les attribuera pas dans une zone continue (ligne de cache).

bestsss
la source
3
@bestsss bien que tout cela soit vrai aujourd'hui, il s'appuie sur l'implémentation JVM actuelle et l'architecture matérielle pour sa vérité, pas sur la sémantique du programme. Cela signifie que le programme est toujours en panne, même s'il peut fonctionner. Il est facile de trouver une variante triviale de cet exemple qui échoue réellement de la manière spécifiée.
Jed Wesley-Smith
1
J'ai dit que cela ne suivait pas les spécifications, mais en tant qu'enseignant, trouvez au moins un exemple approprié qui peut réellement échouer sur une architecture de base, donc l'exemple est en quelque sorte réel.
bestsss
6
Peut-être le pire conseil sur l'écriture de code thread-safe que j'ai vu.
Lawrence Dol
4
@Bestsss: La réponse simple à votre question est simplement la suivante: "Codez selon la spécification et le document, et non selon les effets secondaires de votre système ou de votre implémentation". Ceci est particulièrement important dans une plate-forme de machine virtuelle conçue pour être indépendante du matériel sous-jacent.
Lawrence Dol
1
@Bestsss: Le point de vue des enseignants est que (a) le code pourrait bien fonctionner lorsque vous le testez, et (b) le code est cassé car il dépend du fonctionnement du matériel et non des garanties de la spécification. Le fait est que cela semble correct, mais ce n'est pas correct.
Lawrence Dol