tl; dr :
Le multithreading comporte 3 problèmes principaux:
1) Conditions de course
2) Mise en cache / mémoire périmée
3) Optimisations Complier et CPU
volatile
peut résoudre 2 & 3, mais ne peut pas résoudre 1. synchronized
/ les verrous explicites peuvent résoudre 1, 2 & 3.
Elaboration :
1) Considérez ce fil non sécurisé:
x++;
Bien que cela puisse ressembler à une opération, il s'agit en fait de 3: lire la valeur actuelle de x dans la mémoire, y ajouter 1 et la sauvegarder en mémoire. Si quelques threads essaient de le faire en même temps, le résultat de l'opération n'est pas défini. Si x
initialement était 1, après 2 threads utilisant le code, il peut être 2 et il peut être 3, selon le thread qui a terminé quelle partie de l'opération avant le transfert du contrôle à l'autre thread. Il s'agit d'une forme de condition de concurrence .
L'utilisation synchronized
sur un bloc de code le rend atomique - ce qui signifie qu'il fait comme si les 3 opérations se produisent en même temps, et il n'y a aucun moyen pour un autre thread de venir au milieu et d'interférer. Donc, si x
était 1, et 2 threads essaient de préformer, x++
nous savons qu'à la fin, il sera égal à 3. Donc, cela résout le problème de la condition de concurrence.
synchronized (this) {
x++; // no problem now
}
Marquer x
comme volatile
ne rend pas x++;
atomique, donc cela ne résout pas ce problème.
2) De plus, les threads ont leur propre contexte - c'est-à-dire qu'ils peuvent mettre en cache les valeurs de la mémoire principale. Cela signifie que quelques threads peuvent avoir des copies d'une variable, mais ils opèrent sur leur copie de travail sans partager le nouvel état de la variable entre d'autres threads.
Considérez que sur un fil, x = 10;
. Et un peu plus tard, dans un autre thread, x = 20;
. La modification de la valeur de x
peut ne pas apparaître dans le premier thread, car l'autre thread a enregistré la nouvelle valeur dans sa mémoire de travail, mais ne l'a pas copiée dans la mémoire principale. Ou qu'il l'a copié dans la mémoire principale, mais le premier thread n'a pas mis à jour sa copie de travail. Donc, si maintenant le premier thread vérifie, if (x == 20)
la réponse sera false
.
Le marquage d'une variable volatile
indique fondamentalement à tous les threads d'effectuer des opérations de lecture et d'écriture sur la mémoire principale uniquement. synchronized
indique à chaque thread d'aller mettre à jour leur valeur depuis la mémoire principale lorsqu'ils entrent dans le bloc, et de vider le résultat dans la mémoire principale lorsqu'ils quittent le bloc.
Notez que contrairement aux races de données, la mémoire obsolète n'est pas si facile à (re) produire, car les vidages de la mémoire principale se produisent de toute façon.
3) Le compliant et le CPU peuvent (sans aucune forme de synchronisation entre les threads) traiter tout le code comme un seul thread. Cela signifie qu'il peut regarder du code, qui est très significatif dans un aspect multithreading, et le traiter comme s'il s'agissait d'un seul thread, alors qu'il n'est pas si significatif. Il peut donc regarder un code et décider, dans un souci d'optimisation, de le réorganiser, voire d'en supprimer complètement certaines parties, s'il ne sait pas que ce code est conçu pour fonctionner sur plusieurs threads.
Considérez le code suivant:
boolean b = false;
int x = 10;
void threadA() {
x = 20;
b = true;
}
void threadB() {
if (b) {
System.out.println(x);
}
}
Vous pourriez penser que threadB ne pourrait imprimer que 20 (ou ne rien imprimer du tout si la vérification if de threadB est exécutée avant de définir b
sur true), car il b
est défini sur true uniquement après x
est défini sur 20, mais le compilateur / CPU peut décider de réorganiser threadA, dans ce cas, threadB peut également imprimer 10. Marquer b
comme volatile
garantit qu'il ne sera pas réorganisé (ou supprimé dans certains cas). Ce qui signifie que threadB ne pouvait imprimer que 20 (ou rien du tout). Marquer les méthodes comme synchronisées permettra d'obtenir le même résultat. Le marquage d'une variable volatile
garantit également qu'elle ne sera pas réorganisée, mais tout ce qui est avant / après peut toujours être réorganisé, de sorte que la synchronisation peut être plus adaptée dans certains scénarios.
Notez qu'avant Java 5 New Memory Model, volatile ne résolvait pas ce problème.