Synchronisation du champ non final


91

Un avertissement s'affiche chaque fois que je synchronise sur un champ de classe non final. Voici le code:

public class X  
{  
   private Object o;  

   public void setO(Object o)  
   {  
     this.o = o;  
   }  

   public void x()  
   {  
     synchronized (o) // synchronization on a non-final field  
     {  
     }  
   }  
 } 

j'ai donc changé le codage de la manière suivante:

 public class X  
 {  

   private final Object o;       
   public X()
   {  
     o = new Object();  
   }  

   public void x()  
   {  
     synchronized (o)
     {  
     }  
   }  
 }  

Je ne suis pas sûr que le code ci-dessus soit le bon moyen de synchroniser sur un champ de classe non final. Comment synchroniser un champ non final?

Réponses:


127

Tout d'abord, je vous encourage à vraiment essayer de résoudre les problèmes de concurrence à un niveau d'abstraction plus élevé, c'est-à-dire en les résolvant en utilisant des classes de java.util.concurrent telles que ExecutorServices, Callables, Futures, etc.

Cela étant dit, il n'y a rien de mal à synchroniser sur un champ non final en soi. Vous devez simplement garder à l'esprit que si la référence de l'objet change, la même section de code peut être exécutée en parallèle . Par exemple, si un thread exécute le code dans le bloc synchronisé et que quelqu'un appelle setO(...), un autre thread peut exécuter le même bloc synchronisé sur la même instance simultanément.

Synchronisez sur l'objet dont vous avez besoin d'un accès exclusif (ou, mieux encore, un objet dédié à sa protection).


1
Je dis que, si vous synchronisez sur un champ non final, vous devez être conscient du fait que l'extrait de code s'exécute avec un accès exclusif à l'objet oauquel il est fait référence au moment où le bloc synchronisé a été atteint. Si l'objet qui ofait référence aux changements, un autre thread peut venir et exécuter le bloc de code synchronisé.
aioobe

42
Je ne suis pas d'accord avec votre règle de base - je préfère synchroniser sur un objet dont le seul but est de protéger un autre état. Si vous ne faites jamais rien avec un objet autre que le verrouillage, vous savez avec certitude qu'aucun autre code ne peut le verrouiller. Si vous vous verrouillez sur un objet "réel" dont vous appelez ensuite les méthodes, cet objet peut également se synchroniser sur lui-même, ce qui rend plus difficile de raisonner sur le verrouillage.
Jon Skeet

9
Comme je l' ai dit dans ma réponse, je pense que je besoin de l' avoir moi très soigneusement justifiée, pourquoi vous voulez faire une telle chose. Et je ne recommanderais pas la synchronisation thisnon plus - je recommanderais de créer une variable finale dans la classe uniquement à des fins de verrouillage , ce qui empêche quiconque de se verrouiller sur le même objet.
Jon Skeet

1
C'est un autre bon point, et je suis d'accord; le verrouillage sur une variable non finale nécessite certainement une justification minutieuse.
aioobe

Je ne suis pas sûr des problèmes de visibilité de la mémoire liés à la modification d'un objet utilisé pour la synchronisation. Je pense que vous auriez de gros problèmes en changeant un objet et en vous appuyant ensuite sur le code pour voir ce changement correctement afin que "la même section de code puisse être exécutée en parallèle". Je ne sais pas quelles garanties, le cas échéant, sont étendues par le modèle de mémoire à la visibilité mémoire des champs utilisés pour verrouiller, par opposition aux variables accessibles dans le bloc de synchronisation. Ma règle d'or serait, si vous synchronisez sur quelque chose, ce devrait être définitif.
Mike Q

47

Ce n'est vraiment pas une bonne idée - car vos blocs synchronisés ne sont plus vraiment synchronisés de manière cohérente.

En supposant que les blocs synchronisés sont destinés à garantir qu'un seul thread accède à certaines données partagées à la fois, considérez:

  • Le fil 1 entre dans le bloc synchronisé. Yay - il a un accès exclusif aux données partagées ...
  • Thread 2 appelle setO ()
  • Le fil 3 (ou encore 2 ...) entre dans le bloc synchronisé. Eek! Il pense qu'il a un accès exclusif aux données partagées, mais le thread 1 est toujours en train de se débattre avec lui ...

Pourquoi voudriez-vous que cela se produise? Peut - être il y a des situations très spécialisés où il est logique ... mais vous auriez à me présenter un cas d'utilisation spécifique (ainsi que des moyens d'atténuer le genre de scénario que j'ai donné ci - dessus) avant que je serais heureux avec il.


2
@aioobe: Mais alors le thread 1 pourrait toujours exécuter du code qui mute la liste (et auquel il fait souvent référence o) - et à mi-chemin de son exécution, commencez à muter une liste différente. En quoi serait-ce une bonne idée? Je pense que nous sommes fondamentalement en désaccord sur la question de savoir si c'est une bonne idée de verrouiller les objets que vous touchez d'une autre manière. Je préférerais pouvoir raisonner sur mon code sans aucune connaissance de ce que fait un autre code en termes de verrouillage.
Jon Skeet

2
@Felype: Il semble que vous devriez poser une question plus détaillée en tant que question distincte - mais oui, je créerais souvent des objets séparés comme des verrous.
Jon Skeet

3
@VitBernatik: Non. Si le thread X commence à modifier la configuration, le thread Y change la valeur de la variable en cours de synchronisation, puis le thread Z commence à modifier la configuration, alors X et Z modifieront la configuration en même temps, ce qui est mauvais .
Jon Skeet

1
En bref, il est plus sûr de toujours déclarer ces objets de verrouillage définitifs, n'est-ce pas?
Saint

2
@LinkTheProgrammer: "Une méthode synchronisée synchronise chaque objet de l'instance" - non, ce n'est pas le cas. Ce n'est tout simplement pas vrai et vous devriez revoir votre compréhension de la synchronisation.
Jon Skeet

12

Je suis d'accord avec l'un des commentaires de John: Vous devez toujours utiliser un mannequin de verrouillage final lors de l'accès à une variable non finale pour éviter les incohérences en cas de changement de référence de la variable. Donc dans tous les cas et en règle générale:

Règle n ° 1: Si un champ n'est pas final, utilisez toujours un mannequin de verrouillage final (privé).

Raison n ° 1: vous maintenez le verrou et changez vous-même la référence de la variable. Un autre thread en attente en dehors du verrou synchronisé pourra entrer dans le bloc protégé.

Raison n ° 2: vous maintenez le verrou et un autre thread modifie la référence de la variable. Le résultat est le même: un autre thread peut entrer dans le bloc protégé.

Mais lors de l'utilisation d'un factice de verrouillage final, il y a un autre problème : vous pouvez obtenir des données erronées, car votre objet non final ne sera synchronisé avec la RAM que lors de l'appel de synchronize (object). Donc, comme deuxième règle de base:

Règle n ° 2: Lors du verrouillage d'un objet non final, vous devez toujours faire les deux: Utiliser un factice de verrouillage final et le verrou de l'objet non final pour des raisons de synchronisation RAM. (La seule alternative sera de déclarer tous les champs de l'objet comme volatils!)

Ces verrous sont également appelés «verrous imbriqués». Notez que vous devez toujours les appeler dans le même ordre, sinon vous obtiendrez un dead lock :

public class X {
    private final LOCK;
    private Object o;

    public void setO(Object o){
        this.o = o;  
    }  

    public void x() {
        synchronized (LOCK) {
        synchronized(o){
            //do something with o...
        }
        }  
    }  
} 

Comme vous pouvez le voir, j'écris les deux verrous directement sur la même ligne, car ils vont toujours ensemble. Comme ça, vous pouvez même faire 10 verrous d'imbrication:

synchronized (LOCK1) {
synchronized (LOCK2) {
synchronized (LOCK3) {
synchronized (LOCK4) {
    //entering the locked space
}
}
}
}

Notez que ce code ne cassera pas si vous acquérez simplement un verrou interne comme synchronized (LOCK3)par un autre thread. Mais cela se cassera si vous appelez dans un autre thread quelque chose comme ceci:

synchronized (LOCK4) {
synchronized (LOCK1) {  //dead lock!
synchronized (LOCK3) {
synchronized (LOCK2) {
    //will never enter here...
}
}
}
}

Il n'existe qu'une seule solution de contournement autour de ces verrous imbriqués lors de la gestion des champs non finaux:

Règle n ° 2 - Alternative: Déclarez tous les champs de l'objet comme volatils. (Je ne parlerai pas ici des inconvénients de faire cela, par exemple empêcher tout stockage dans les caches de niveau x même pour les lectures, etc.)

Donc, aioobe a tout à fait raison: utilisez simplement java.util.concurrent. Ou commencez à tout comprendre sur la synchronisation et faites-le vous-même avec des verrous imbriqués. ;)

Pour plus de détails sur les raisons pour lesquelles la synchronisation sur les champs non finaux est interrompue, jetez un œil à mon cas de test: https://stackoverflow.com/a/21460055/2012947

Et pour plus de détails sur les raisons pour lesquelles vous avez besoin d'être synchronisé en raison de la RAM et des caches, regardez ici: https://stackoverflow.com/a/21409975/2012947


1
Je pense que vous devez envelopper le setter oavec un synchronisé (LOCK) pour établir une relation «arrive avant» entre le réglage et l'objet de lecture o. Je discute de cela dans une question similaire à la mienne: stackoverflow.com/questions/32852464/…
Petrakeas

J'utilise dataObject pour synchroniser l'accès aux membres dataObject. Comment est-ce mal? Si le dataObject commence à pointer quelque part différemment, je veux qu'il soit synchronisé sur les nouvelles données pour empêcher les threads simultanés de le modifier. Des problèmes avec ça?
Harmen

2

Je ne vois pas vraiment la bonne réponse ici, c'est-à-dire que c'est parfaitement normal de le faire.

Je ne sais même pas pourquoi c'est un avertissement, il n'y a rien de mal à cela. La JVM s'assure que vous récupérez un objet valide (ou nul) lorsque vous lisez une valeur, et vous pouvez synchroniser sur n'importe quel objet.

Si vous prévoyez de changer réellement le verrou pendant son utilisation (par opposition, par exemple, à le changer à partir d'une méthode init, avant de commencer à l'utiliser), vous devez créer la variable que vous prévoyez de changer volatile. Ensuite, tout ce que vous avez à faire est de synchroniser à la fois l'ancien et le nouvel objet, et vous pouvez modifier la valeur en toute sécurité

public volatile Object lock;

...

synchronized (lock) {
    synchronized (newObject) {
        lock = newObject;
    }
}

Là. Ce n'est pas compliqué, écrire du code avec des verrous (mutex) est en fait assez facile. Ecrire du code sans eux (code sans verrouillage) est ce qui est difficile.


Cela peut ne pas fonctionner. Dites o a commencé comme référence à O1, puis le fil T1 verrouille o (= O1) et O2 et définit o sur O2. En même temps, le thread T2 verrouille O1 et attend que T1 le déverrouille. Lorsqu'il reçoit le verrou O1, il mettra o à O3. Dans ce scénario, entre T1 libérant O1 et T2 verrouillage O1, O1 est devenu invalide pour le verrouillage via o. A ce moment, un autre thread peut utiliser o (= O2) pour le verrouillage et continuer sans interruption en course avec T2.
GPS du

2

EDIT: Donc, cette solution (comme suggéré par Jon Skeet) pourrait avoir un problème avec l'atomicité de l'implémentation de "synchronized (object) {}" pendant que la référence de l'objet change. J'ai demandé séparément et selon M. erickson, ce n'est pas thread-safe - voir: L'entrée de bloc synchronisé est-elle atomique? . Alors prenez-le comme exemple comment NE PAS le faire - avec des liens pourquoi;)

Voir le code comment cela fonctionnerait si synchronized () était atomique:

public class Main {
    static class Config{
        char a='0';
        char b='0';
        public void log(){
            synchronized(this){
                System.out.println(""+a+","+b);
            }
        }
    }

    static Config cfg = new Config();

    static class Doer extends Thread {
        char id;

        Doer(char id) {
            this.id = id;
        }

        public void mySleep(long ms){
            try{Thread.sleep(ms);}catch(Exception ex){ex.printStackTrace();}
        }

        public void run() {
            System.out.println("Doer "+id+" beg");
            if(id == 'X'){
                synchronized (cfg){
                    cfg.a=id;
                    mySleep(1000);
                    // do not forget to put synchronize(cfg) over setting new cfg - otherwise following will happend
                    // here it would be modifying different cfg (cos Y will change it).
                    // Another problem would be that new cfg would be in parallel modified by Z cos synchronized is applied on new object
                    cfg.b=id;
                }
            }
            if(id == 'Y'){
                mySleep(333);
                synchronized(cfg) // comment this and you will see inconsistency in log - if you keep it I think all is ok
                {
                    cfg = new Config();  // introduce new configuration
                    // be aware - don't expect here to be synchronized on new cfg!
                    // Z might already get a lock
                }
            }
            if(id == 'Z'){
                mySleep(666);
                synchronized (cfg){
                    cfg.a=id;
                    mySleep(100);
                    cfg.b=id;
                }
            }
            System.out.println("Doer "+id+" end");
            cfg.log();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Doer X = new Doer('X');
        Doer Y = new Doer('Y');
        Doer Z = new Doer('Z');
        X.start();
        Y.start();
        Z.start();
    }

}

1
Cela pourrait être correct - mais je ne sais pas s'il y a une garantie dans le modèle de mémoire que la valeur sur laquelle vous synchronisez est la plus récemment écrite - je ne pense pas qu'il y ait une garantie de "lecture et synchronisation" atomique. Personnellement j'essaye d'éviter de synchroniser sur des moniteurs qui ont de toute façon d'autres usages, pour plus de simplicité. (En ayant un champ séparé, le code devient clairement correct au lieu d'avoir à le raisonner soigneusement.)
Jon Skeet

@Jon. Thx pour la réponse! J'entends ton inquiétude. Je suis d'accord pour ce cas le verrou externe éviterait la question de "l'atomicité synchronisée". Ce serait donc préférable. Bien qu'il puisse y avoir des cas où vous souhaitez introduire plus de configuration dans le runtime et partager une configuration différente pour différents groupes de threads (bien que ce ne soit pas mon cas). Et puis cette solution pourrait devenir intéressante. J'ai posté la question stackoverflow.com/questions/29217266/… sur l'atomicité synchronized () - nous verrons donc si elle peut être utilisée (et quelqu'un répond-elle)
Vit Bernatik

2

AtomicReference s'adapte à vos besoins.

De la documentation java sur le paquet atomique :

Une petite boîte à outils de classes prenant en charge la programmation thread-safe sans verrouillage sur des variables uniques. En substance, les classes de ce package étendent la notion de valeurs volatiles, de champs et d'éléments de tableau à ceux qui fournissent également une opération de mise à jour conditionnelle atomique du formulaire:

boolean compareAndSet(expectedValue, updateValue);

Exemple de code:

String initialReference = "value 1";

AtomicReference<String> someRef =
    new AtomicReference<String>(initialReference);

String newReference = "value 2";
boolean exchanged = someRef.compareAndSet(initialReference, newReference);
System.out.println("exchanged: " + exchanged);

Dans l'exemple ci-dessus, vous remplacez Stringpar le vôtreObject

Question SE connexe:

Quand utiliser AtomicReference en Java?


1

Si ojamais ne change pendant la durée de vie d'une instance de X, la deuxième version est de meilleur style, que la synchronisation soit impliquée ou non.

Maintenant, il est impossible de répondre s'il y a quelque chose qui ne va pas avec la première version sans savoir ce qui se passe d'autre dans cette classe. J'aurais tendance à être d'accord avec le compilateur pour dire qu'il semble sujet aux erreurs (je ne répéterai pas ce que les autres ont dit).


1

Juste en ajoutant mes deux cents: j'ai eu cet avertissement lorsque j'ai utilisé un composant qui est instancié via le concepteur, donc son champ ne peut pas vraiment être final, car le constructeur ne peut pas prendre de paramètres. En d'autres termes, j'avais un champ quasi-final sans le mot-clé final.

Je pense que c'est pour cela que c'est juste un avertissement: vous faites probablement quelque chose de mal, mais cela peut aussi être juste.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.