web-dev-qa-db-fra.com

Synchronisation du champ non final

Un avertissement s'affiche chaque fois que je me 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 la bonne façon de synchroniser sur un champ de classe non final. Comment synchroniser un champ non final?

77
divz

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

Cela étant dit, il n'y a rien mal avec la synchronisation sur un champ non final en soi. Vous devez juste garder à l'esprit que si la référence d'objet change, la même section de code peut être exécutée en parallèle . Autrement dit, 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é à le garder).

114
aioobe

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

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

  • Le thread 1 entre dans le bloc synchronisé. Oui - il a un accès exclusif aux données partagées ...
  • Thread 2 appelle setO ()
  • Le thread 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 s'en nourrit toujours ...

Pourquoi voudriez-vous vouloir que cela se produise? Peut-être qu'il y a certains situations très spécialisées où cela a du sens ... mais vous devrez me présenter un cas d'utilisation spécifique (ainsi que des moyens d'atténuer le type de scénario que j'ai donné ci-dessus) avant d'en être satisfait.

41
Jon Skeet

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 tenez le verrou et modifiez 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 gardé.

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 gardé.

Mais lorsque vous utilisez un mannequin de verrouillage final, il y a un autre problème: Vous pouvez obtenir des données erronées, car votre non l'objet final ne sera synchronisé qu'avec RAM lors de l'appel de synchronize (object). Donc, en règle générale:

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

Ces verrous sont également appelés "verrous imbriqués". Notez que vous devez les appeler toujours dans le même ordre, sinon vous obtiendrez un verrou mort:

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 appartiennent 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 se cassera pas si vous venez d'acquérir un verrou interne comme synchronized (LOCK3) par un autre thread. Mais il se cassera si vous appelez un autre fil comme ceci:

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

Il n'y a 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 en empêchant tout stockage dans des 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, consultez mon scénario de test: https://stackoverflow.com/a/21460055/2012947

Et pour plus de détails, pourquoi vous avez besoin de synchroniser du tout en raison de RAM et les caches ont un regard ici: https://stackoverflow.com/a/21409975/2012947

12
Marcus

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

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

Si vous prévoyez de réellement changer le verrou pendant qu'il est en cours d'utilisation (par opposition à le changer depuis une méthode init, avant de commencer à l'utiliser), vous devez faire la variable que vous prévoyez de changer volatile. Ensuite, tout ce que vous devez faire est de synchroniser sur les deux 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. Écrire du code sans eux (code sans verrouillage) est ce qui est difficile.

2
Evan Dark

AtomicReference convient à vos besoins.

De Java documentation sur atomic package:

Une petite boîte à outils de classes qui prennent 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 à celles 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 String par votre propre Object

Question SE associée:

Quand utiliser AtomicReference en Java?

2
Ravindra babu

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 d'objet change. J'ai demandé séparément et selon M. erickson, ce n'est pas sûr pour les threads - voir: L'entrée du bloc synchronisé est-elle atomique? . Prenez donc comme exemple comment NE PAS le faire - avec des liens pourquoi;)

Voir le code comment cela fonctionnerait si synchronized () serait 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();
    }

}
2
Vit Bernatik

Juste ajouter 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 pourquoi c'est juste un avertissement: vous faites probablement quelque chose de mal, mais cela pourrait aussi être bien.

1
peenut

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

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

1
NPE