web-dev-qa-db-fra.com

Quand utiliser volatile et synchronisé

Je sais qu'il y a beaucoup de questions à ce sujet, mais je ne comprends toujours pas bien. Je sais ce que font ces deux mots clés, mais je ne peux pas déterminer lequel utiliser dans certains scénarios. Voici quelques exemples que j'essaie de déterminer lequel est le mieux à utiliser.

Exemple 1:

import Java.net.ServerSocket;

public class Something extends Thread {

    private ServerSocket serverSocket;

    public void run() {
        while (true) {
            if (serverSocket.isClosed()) {
                ...
            } else { //Should this block use synchronized (serverSocket)?
                //Do stuff with serverSocket
            }
        }
    }

    public ServerSocket getServerSocket() {
        return serverSocket;
    }

}

public class SomethingElse {

    Something something = new Something();

    public void doSomething() {
        something.getServerSocket().close();
    }

}

Exemple 2:

public class Server {

    private int port;//Should it be volatile or the threads accessing it use synchronized (server)?

    //getPort() and setPort(int) are accessed from multiple threads
    public int getPort() {
        return port;
    }

    public void setPort(int port) {
        this.port = port;
    }

}

Toute aide est grandement appréciée.

30
Stripies

Une réponse simple est la suivante:

  • synchronized peut toujours être utilisé pour vous donner une solution thread-safe/correcte, 

  • volatile sera probablement plus rapide, mais ne pourra être utilisé que pour vous donner un thread-safe/correct dans des situations limitées.

En cas de doute, utilisez synchronized. La correction est plus importante que la performance.

La caractérisation des situations dans lesquelles volatile peut être utilisé en toute sécurité implique de déterminer si chaque opération de mise à jour peut être effectuée comme une mise à jour atomique unique vers une seule variable volatile. Si l'opération implique d'accéder à un autre état (non final) ou de mettre à jour plusieurs variables partagées, l'opération ne peut pas être effectuée en toute sécurité avec une valeur volatile. Vous devez également vous rappeler que:

  • les mises à jour de long ou double non volatile ne peuvent pas être atomiques, et 
  • Les opérateurs Java tels que ++ et += ne sont pas atomiques.

Terminologie: une opération est "atomique" si l'opération se produit entièrement ou ne se produit pas du tout. Le terme "indivisible" est un synonyme.

Lorsque nous parlons d'atomicité, nous habituellement entendons l'atomicité du point de vue d'un observateur extérieur; par exemple. un thread différent de celui qui effectue l'opération. Par exemple, ++ n'est pas atomique du point de vue d'un autre thread, car ce thread peut peut-être observer l'état du champ incrémenté au milieu de l'opération. En effet, si le champ est une long ou une double, il peut même être possible d'observer un état qui n'est ni l'état initial ni l'état final!

38
Stephen C

Le mot clé synchronized

synchronized indique qu'une variable sera partagée entre plusieurs threads. Il est utilisé pour assurer la cohérence en "verrouillant" l'accès à la variable, de sorte qu'un thread ne puisse pas la modifier pendant qu'un autre l'utilise.

Exemple classique: mise à jour d'une variable globale indiquant l'heure actuelle
La fonction incrementSeconds() doit pouvoir se terminer sans interruption car, lors de son exécution, elle crée des incohérences temporaires dans la valeur de la variable globale time. Sans synchronisation, une autre fonction pourrait afficher une time de "12:60:00" ou, au commentaire marqué avec >>>, "11:00:00" si l'heure est vraiment "12:00:00" car le les heures n'ont pas encore augmenté.

void incrementSeconds() {
  if (++time.seconds > 59) {      // time might be 1:00:60
    time.seconds = 0;             // time is invalid here: minutes are wrong
    if (++time.minutes > 59) {    // time might be 1:60:00
      time.minutes = 0;           // >>> time is invalid here: hours are wrong
      if (++time.hours > 23) {    // time might be 24:00:00
        time.hours = 0;
      }
    }
  }

Le mot clé volatile

volatile indique simplement au compilateur de ne pas émettre d'hypothèses sur la constance d'une variable, car cela peut changer lorsque le compilateur ne s'y attendait pas normalement. Par exemple, le logiciel d’un thermostat numérique peut avoir une variable indiquant la température et dont la valeur est mise à jour directement par le matériel. Cela peut changer à des endroits qu'une variable normale ne changerait pas.

Si degreesCelsius n'est pas déclaré comme étant volatile, le compilateur est libre de l'optimiser:

void controlHeater() {
  while ((degreesCelsius * 9.0/5.0 + 32) < COMFY_TEMP_IN_Fahrenheit) {
    setHeater(ON);
    sleep(10);
  }
}

dans ceci:

void controlHeater() {
  float tempInFahrenheit = degreesCelsius * 9.0/5.0 + 32;

  while (tempInFahrenheit < COMFY_TEMP_IN_Fahrenheit) {
    setHeater(ON);
    sleep(10);
  }
}

En déclarant que degreesCelsius est volatile, vous indiquez au compilateur qu'il doit vérifier sa valeur chaque fois qu'il parcourt la boucle.

Résumé

En bref,synchronizedvous permet de contrôler l’accès à une variable, de sorte que vous puissiez garantir que les mises à jour sont atomiques (c’est-à-dire qu'un ensemble de modifications sera appliqué en tant qu'unité; aucun autre thread ne pourra accéder à la variable à moitié mis à jour). Vous pouvez l'utiliser pour assurer la cohérence de vos données. D'autre part,volatileest un aveu que le contenu d'une variable est indépendant de votre volonté. Le code doit donc supposer qu'il peut changer à tout moment.

18
Adam Liss

Les informations que vous publiez dans votre message sont insuffisantes pour déterminer ce qui se passe. C'est pourquoi tous les conseils que vous obtenez sont des informations générales sur volatile et synchronized.

Alors, voici mon conseil général:

Au cours du cycle d'écriture-compilation-exécution d'un programme, il existe deux points d'optimisation:

  • au moment de la compilation, le compilateur peut essayer de réorganiser les instructions ou d’optimiser la mise en cache des données.
  • au moment de l'exécution, lorsque le processeur dispose de ses propres optimisations, telles que la mise en cache et l'exécution dans le désordre.

Tout cela signifie que les instructions ne seront probablement pas exécutées dans l'ordre dans lequel vous les avez écrites, que cet ordre soit ou non maintenu afin de garantir l'exactitude du programme dans un environnement multithread. Voici un exemple classique que vous trouverez souvent dans la littérature:

class ThreadTask implements Runnable {
    private boolean stop = false;
    private boolean work;

    public void run() {
        while(!stop) {
           work = !work; // simulate some work
        } 
    }

    public void stopWork() {
        stop = true; // signal thread to stop
    }

    public static void main(String[] args) {
        ThreadTask task = new ThreadTask();
        Thread t = new Thread(task);
        t.start();
        Thread.sleep(1000);
        task.stopWork();
        t.join();
    }
}

Selon les optimisations du compilateur et l'architecture de la CPU, le code ci-dessus peut ne jamais se terminer sur un système multiprocesseur. En effet, la valeur de stop sera mise en cache dans un registre du thread exécutant la CPU t, de sorte que le thread ne lira plus jamais la valeur dans la mémoire principale, même si le thread principal l'a mis à jour entre-temps.

Pour lutter contre ce genre de situation, des barrières mémoire ont été introduites. Ce sont des instructions spéciales qui ne permettent pas de réorganiser des instructions régulières avant la clôture avec des instructions après la clôture. L'un de ces mécanismes est le mot clé volatile. Les variables marquées volatile ne sont pas optimisées par le compilateur/CPU et seront toujours écrites/lues directement dans/depuis la mémoire principale. En bref, volatile assure la visibilité de la valeur d'une variable dans les cœurs de processeur .

La visibilité est importante, mais ne doit pas être confondue avec atomicity . Deux threads incrémentant la même variable partagée peuvent produire des résultats incohérents même si la variable est déclarée volatile. Cela est dû au fait que sur certains systèmes, l'incrément est traduit en une séquence d'instructions d'assembleur pouvant être interrompues à tout moment. Dans de tels cas, des sections critiques telles que le mot clé synchronized doivent être utilisées. Cela signifie qu'un seul thread peut accéder au code contenu dans le bloc synchronized. Les autres utilisations courantes des sections critiques sont les mises à jour atomiques d'une collection partagée. Lorsqu'une itération sur une collection pendant qu'un autre thread ajoute/supprime des éléments, une exception est alors générée.

Enfin deux points intéressants: 

  • synchronized et quelques autres constructions telles que Thread.join introduiront implicitement les clôtures de mémoire. Par conséquent, incrémenter une variable à l'intérieur d'un bloc synchronized ne nécessite pas que la variable soit également volatile, en supposant que c'est le seul endroit où elle est en cours de lecture/écriture.
  • Pour les mises à jour simples telles que l'échange, l'incrément, la décrémentation, vous pouvez utiliser des méthodes atomiques non bloquantes comme celles trouvées dans AtomicInteger, AtomicLong, etc. Elles sont beaucoup plus rapides que synchronized car elles ne déclenchent pas de changement de contexte dans le cas où le verrou est déjà pris par un autre fil. Ils introduisent également des barrières de mémoire lorsqu’ils sont utilisés.
9
Tudor

Remarque: Dans votre premier exemple, le champ serverSocket n'est en réalité jamais initialisé dans le code que vous affichez.

En ce qui concerne la synchronisation, cela dépend si la classe ServerSocket est thread-safe ou non. (Je suppose que c'est le cas, mais je ne l'ai jamais utilisé.) Si c'est le cas, vous n'avez pas besoin de vous synchroniser avec cela.

Dans le deuxième exemple, les variables int peuvent être mises à jour de manière atomique afin que volatile puisse suffire.

1
Péter Török

volatile résout le problème de «visibilité» entre les cœurs de la CPU. Par conséquent, la valeur des registres locaux est purgée et synchronisée avec la RAM. Cependant, si nous avons besoin d'une valeur cohérente et d'opération atomique, nous avons besoin d'un mécanisme pour défendre les données critiques. Cela peut être réalisé par un bloc synchronized ou un verrou explicite.

1
Roy