Pourquoi `synchronized (new Object ()) {}` un no-op?

Dans le code suivant:

class A { private int number; public void a() { number = 5; } public void b() { while(number == 0) { // ... } } } 

Si la méthode b est appelée et qu’un nouveau thread est lancé, ce qui déclenche la méthode a, la méthode b n’est pas garantie de voir le changement de number et donc b peut ne jamais se terminer.

Bien sûr, nous pourrions rendre le number volatile pour résoudre ce problème. Cependant, pour des raisons académiques, supposons que volatile n’est pas une option:

La FAQ JSR-133 nous dit:

Après avoir quitté un bloc synchronisé, nous libérons le moniteur, ce qui a pour effet de vider le cache dans la mémoire principale , afin que les écritures effectuées par ce thread puissent être visibles pour les autres threads. Avant de pouvoir entrer dans un bloc synchronisé, nous acquérons le moniteur, ce qui a pour effet d’invalider le cache du processeur local afin que les variables soient rechargées à partir de la mémoire principale.

Cela ressemble à ce que j’ai juste besoin que les deux a et b entrent et sortent de n’importe quel locking synchronized , quel que soit le moniteur qu’ils utilisent. Plus précisément, cela ressemble à ceci:

 class A { private int number; public void a() { number = 5; synchronized(new Object()) {} } public void b() { while(number == 0) { // ... synchronized(new Object()) {} } } } 

… éliminerait le problème et garantirait que b verra le changement à a et donc finira aussi éventuellement.

Cependant, la FAQ indique clairement:

Une autre implication est que le modèle suivant, que certaines personnes utilisent pour forcer une barrière de mémoire, ne fonctionne pas:

 synchronized (new Object()) {} 

Ceci est en fait un no-op, et votre compilateur peut le supprimer complètement, car le compilateur sait qu’aucun autre thread ne se synchronisera sur le même moniteur. Vous devez définir une relation “passe-avant” pour qu’un thread puisse voir les résultats d’un autre.

Maintenant, c’est déroutant. Je pensais que la déclaration synchronisée entraînerait le vidage des caches. Il ne peut sûrement pas vider un cache dans la mémoire principale de manière à ce que les modifications de la mémoire principale ne puissent être vues que par les threads qui se synchronisent sur le même moniteur, surtout que pour volatile qui fait la même chose surveiller, ou je me trompe là? Alors, pourquoi est-ce un non-op et ne provoque pas la résiliation de b par garantie?

La FAQ n’est pas l’autorité en la matière; le JLS est. La section 17.4.4 spécifie les synchronisations avec les relations, qui alimentent les relations entre événements antérieurs (17.4.5). Le point pertinent est le suivant:

  • Une action de délocking sur le moniteur m se synchronise avec toutes les actions de locking ultérieures sur m (où “la suite” est définie en fonction de l’ordre de synchronisation).

Puisque m est la référence au new Object() , et qu’il n’est jamais stocké ou publié sur un autre thread, nous pouvons être sûrs qu’aucun autre thread n’acquerra un verrou sur m après la libération du verrou dans ce bloc. De plus, puisque m est un nouvel object, nous pouvons être sûrs qu’il n’y a aucune action précédemment déverrouillée. Par conséquent, nous pouvons être sûrs qu’aucune action ne se synchronise formellement avec cette action.

Techniquement, vous n’avez même pas besoin de faire un vidage complet du cache pour être à la hauteur de la spécification JLS; c’est plus que la JLS exige. Une implémentation typique le fait, car c’est la chose la plus simple que le matériel vous permet de faire, mais cela se passe “au-delà” pour ainsi dire. Dans les cas où l’ parsing d’échappement indique à un compilateur optimisé que nous avons encore moins besoin, le compilateur peut effectuer moins de choses. Dans votre exemple, l’parsing d’échappement peut indiquer au compilateur que l’action n’a aucun effet (en raison du raisonnement ci-dessus) et peut être entièrement optimisée.

le schéma suivant, que certaines personnes utilisent pour forcer une barrière de mémoire, ne fonctionne pas:

Ce n’est pas garanti d’être un no-op, mais la spécification permet d’être un no-op. La spécification nécessite uniquement une synchronisation pour établir une relation avant-terme entre deux threads lorsque les deux threads se synchronisent sur le même object, mais il serait en fait plus facile d’implémenter une JVM où l’identité de l’object importait peu.

Je pensais que la déclaration synchronisée entraînerait le vidage des caches

Il n’y a pas de “cache” dans la spécification de langage Java. C’est un concept qui n’existe que dans les détails de certaines plates-formes matérielles et de leurs implémentations JVM.