À quoi servent les clôtures de mémoire en Java?

18

Tout en essayant de comprendre comment SubmissionPublisher( code source dans Java SE 10, OpenJDK | docs ), une nouvelle classe ajoutée à Java SE dans la version 9, a été implémentée, je suis tombé sur quelques appels d'API dont VarHandleje n'étais pas au courant auparavant:

fullFence, acquireFence, releaseFence, loadLoadFenceEt storeStoreFence.

Après avoir fait quelques recherches, en particulier en ce qui concerne le concept de barrières / clôtures de mémoire (j'en ai entendu parler auparavant, oui; mais je ne les ai jamais utilisées, donc je ne connaissais pas bien leur sémantique), je pense avoir une compréhension de base de ce à quoi elles servent. . Néanmoins, comme mes questions pourraient découler d'une idée fausse, je veux m'assurer d'avoir bien compris en premier lieu:

  1. Les barrières mémoire réorganisent les contraintes concernant les opérations de lecture et d'écriture.

  2. Les barrières mémoire peuvent être classées en deux catégories principales: les barrières mémoire unidirectionnelles et bidirectionnelles, selon qu'elles définissent des contraintes sur les lectures ou les écritures ou sur les deux.

  3. C ++ prend en charge une variété de barrières de mémoire , cependant, celles-ci ne correspondent pas à celles fournies par VarHandle. Cependant, certaines des barrières de mémoire disponibles VarHandlefournir des effets de commande qui sont compatibles à leurs barrières de mémoire C ++ correspondant.

    • #fullFence est compatible avec atomic_thread_fence(memory_order_seq_cst)
    • #acquireFence est compatible avec atomic_thread_fence(memory_order_acquire)
    • #releaseFence est compatible avec atomic_thread_fence(memory_order_release)
    • #loadLoadFenceet #storeStoreFencen'ont pas de contrepartie C ++ compatible

Le mot compatible semble vraiment important ici car la sémantique diffère clairement en ce qui concerne les détails. Par exemple, toutes les barrières C ++ sont bidirectionnelles, contrairement aux barrières Java (nécessairement).

  1. La plupart des barrières mémoire ont également des effets de synchronisation. Celles-ci dépendent en particulier du type de barrière utilisé et des instructions de barrière précédemment exécutées dans d'autres threads. Comme les implications complètes d'une instruction de barrière sont spécifiques au matériel, je m'en tiendrai aux barrières de niveau supérieur (C ++). En C ++, par exemple, les modifications apportées avant une instruction de barrière de libération sont visibles par un thread exécutant une instruction de barrière d' acquisition .

Mes hypothèses sont-elles correctes? Si oui, mes questions résultantes sont:

  1. Les barrières de mémoire disponibles VarHandleprovoquent-elles une sorte de synchronisation de la mémoire?

  2. Qu'elles provoquent ou non une synchronisation de la mémoire, à quoi peuvent servir les contraintes de réorganisation en Java? Le modèle de mémoire Java donne déjà des garanties très solides concernant l'ordre lorsque des champs volatils, des verrous ou des VarHandleopérations similaires #compareAndSetsont impliqués.

Dans le cas où vous cherchez un exemple: ce qui précède BufferedSubscription, une classe interne de SubmissionPublisher(source liée ci-dessus), a établi une clôture complète dans la ligne 1079 (fonction growAndAdd; car le site Web lié ne prend pas en charge les identificateurs de fragment, juste CTRL + F pour cela ). Cependant, je ne sais pas à quoi cela sert.

Souaffle
la source
1
J'ai essayé de répondre, mais pour le dire très simplement, ils existent parce que les gens veulent un mode plus faible que celui de Java. Dans l' ordre croissant, ceux - ci seraient: plain -> opaque -> release/acquire -> volatile (sequential consistency).
Eugene

Réponses:

11

C'est principalement une non-réponse, vraiment (au départ, je voulais en faire un commentaire, mais comme vous pouvez le voir, c'est beaucoup trop long). C'est juste que je me suis beaucoup posé des questions, j'ai fait beaucoup de lecture et de recherche et à ce stade, je peux dire en toute sécurité: c'est compliqué. J'ai même écrit plusieurs tests avec jcstress pour savoir comment ils travaillent vraiment (en regardant le code assembleur généré) et alors que certains d'entre eux d'une certaine manière , le sujet en général pas facile du sens.

La toute première chose que vous devez comprendre:

La spécification du langage Java (JLS) ne mentionne aucun obstacle , nulle part. Cela, pour java, serait un détail d'implémentation: il agit vraiment en termes de se passe avant la sémantique. Pour pouvoir les spécifier correctement selon le JMM (Java Memory Model), le JMM devrait changer beaucoup .

Ce travail est en cours.

Deuxièmement, si vous voulez vraiment gratter la surface ici, c'est la toute première chose à regarder . Le discours est incroyable. Ma partie préférée est quand Herb Sutter lève ses 5 doigts et dit: "C'est le nombre de personnes qui peuvent vraiment et correctement travailler avec ces derniers." Cela devrait vous donner un aperçu de la complexité impliquée. Néanmoins, il existe des exemples triviaux faciles à saisir (comme un compteur mis à jour par plusieurs threads qui ne se soucie pas des autres garanties de mémoire, mais se soucie seulement qu'il est lui-même incrémenté correctement).

Un autre exemple est quand (en java) vous voulez qu'un volatiledrapeau contrôle les threads pour s'arrêter / démarrer. Vous savez, le classique:

volatile boolean stop = false; // on thread writes, one thread reads this    

Si vous travaillez avec java, vous sauriez que sans volatile ce code est cassé (vous pouvez lire pourquoi le verrouillage de double vérification est cassé sans lui par exemple). Mais savez-vous aussi que pour certaines personnes qui écrivent du code haute performance, c'est trop? volatilela lecture / écriture garantit également la cohérence séquentielle - qui a de solides garanties et certaines personnes veulent une version plus faible de cela.

Un drapeau thread-safe, mais pas volatile? Oui, exactement: VarHandle::set/getOpaque.

Et vous vous demandez pourquoi quelqu'un pourrait en avoir besoin, par exemple? Tout le monde n'est pas intéressé par tous les changements soutenus par a volatile.

Voyons comment nous y parviendrons en java. Tout d' abord, ces exotiques choses existaient déjà dans l'API: AtomicInteger::lazySet. Ceci n'est pas spécifié dans le modèle de mémoire Java et n'a pas de définition claire ; les gens l'utilisaient encore (LMAX, afaik ou ceci pour plus de lecture ). À mon humble avis, AtomicInteger::lazySetest VarHandle::releaseFence(ou VarHandle::storeStoreFence).


Essayons de répondre à la raison pour laquelle quelqu'un en a besoin ?

JMM a essentiellement deux façons d'accéder à un champ: simple et volatile (ce qui garantit la cohérence séquentielle ). Toutes ces méthodes que vous mentionnez sont là pour apporter quelque chose entre les deux - libérer / acquérir la sémantique ; il y a des cas, je suppose, où les gens en ont réellement besoin.

Un relâchement encore plus de la libération / acquisition serait opaque , ce que j'essaie toujours de comprendre pleinement .


Donc, résultat (votre compréhension est assez correcte, btw): si vous prévoyez d'utiliser cela en java - ils n'ont pas de spécification pour le moment, faites-le à vos risques et périls. Si vous voulez les comprendre, leurs modes équivalents C ++ sont le point de départ.

Eugène
la source
1
N'essayez pas de comprendre le sens de lazySeten reliant des réponses anciennes, la documentation actuelle dit précisément ce que cela signifie, de nos jours. De plus, il est trompeur de dire que le JMM n'a que deux modes d'accès. Nous avons une lecture et une écriture volatiles , qui ensemble peuvent établir une relation qui se produit avant .
Holger
1
J'étais en train d'écrire quelque chose de plus à ce sujet. Considérez que le cas est à la fois une lecture et une écriture, agissant comme une barrière complète, et vous comprendrez peut-être pourquoi il est souhaitable de le détendre. Par exemple, lors de l'implémentation d'un verrou, la première action est cas (0, 1) sur le nombre de verrous, mais vous avez seulement besoin d'acquérir la sémantique (comme la lecture volatile), tandis que l'écriture finale de 0 pour déverrouiller devrait avoir libéré la sémantique (comme l'écriture volatile ), il y a donc un événement entre le déverrouillage et le verrouillage ultérieur. Acquérir / libérer est encore plus faible que la lecture / écriture volatile en ce qui concerne les threads utilisant différents verrous.
Holger
1
@Peter Cordes: La première version C ayant un volatilemot - clé était C99, cinq ans après Java, mais elle manquait toujours de sémantique utile, même C ++ 03 n'a pas de modèle de mémoire. Les choses que C ++ appelle "atomiques" sont également beaucoup plus jeunes que Java. Et le volatilemot-clé n'implique même pas de mises à jour atomiques. Alors pourquoi devrait-il être nommé ainsi.
Holger
1
@PeterCordes peut-être, je le confond avec restrict, cependant, je me souviens des moments où j'ai dû écrire __volatilepour utiliser une extension de compilateur sans mot-clé. Alors peut-être, il n'a pas complètement implémenté C89? Ne me dis pas que je suis si vieux. Avant Java 5, volatileétait beaucoup plus proche de C. Mais Java n'avait pas de MMIO, donc son objectif était toujours le multi-threading, mais la sémantique pré-Java 5 n'était pas très utile pour cela. Donc, la version / acquisition comme la sémantique a été ajoutée, mais ce n'est pas atomique (les mises à jour atomiques sont une fonctionnalité supplémentaire construite au-dessus).
Holger
2
@Eugene à ce sujet , mon exemple était spécifique pour l'utilisation de cas pour le verrouillage qui serait acquis. Un verrou de compte à rebours porterait des décrets atomiques avec la sémantique de libération, suivi par le thread atteignant zéro, insérant une clôture d'acquisition et exécutant l'action finale. Bien sûr, il existe d'autres cas de mises à jour atomiques où la clôture complète reste requise.
Holger