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.
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:
long
ou double
non volatile ne peuvent pas être atomiques, et ++
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!
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,synchronized
vous 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,volatile
est 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.
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:
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.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.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.
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.