« Bonjour, Amigo ! Tu te souviens de quand Ellie t'a parlé des problèmes qui se posent lorsque plusieurs threads tentent d'accéder simultanément à une ressource partagée, pas vrai ? »

« Oui. »

« Mais la vérité est que ce n'est pas tout. Il y a un autre petit problème. »

Comme tu le sais, un ordinateur dispose d'une mémoire où les données et les commandes (le code) sont stockées, ainsi qu'un processeur qui exécute ces commandes et travaille avec les données. Le processeur lit les données de la mémoire, les modifie, et les réécrit en mémoire. Pour accélérer les calculs, le processeur a sa propre mémoire « rapide » intégrée : le cache.

Le processeur fonctionne plus rapidement en copiant les variables et zones de mémoire les plus fréquemment utilisées dans son cache. Ensuite, il apporte tous les changements dans cette mémoire rapide. Puis il copie ces données dans la mémoire « lente ». Pendant tout ce temps, la mémoire lente contient les anciennes variables (sans changement !).

Voilà où réside le problème. Un thread change une variable, comme isCancel ou isInterrupted dans l'exemple ci-dessus, mais un second thread « ne voit pas » ce changement, car il est survenu dans la mémoire rapide. C'est une conséquence du fait que les threads n'ont pas accès à la mémoire cache des autres. (Un processeur contient souvent plusieurs cœurs indépendants et les threads peuvent s'exécuter sur des cœurs physiquement différents).

Rappelons-nous de l'exemple d'hier :

Code Description
class Clock implements Runnable
{
private boolean isCancel = false;

public void cancel()
{
this.isCancel = true;
}

public void run()
{
while (!this.isCancel)
{
Thread.sleep(1000);
System.out.println("Tick");
}
}
}
Le thread « ne sait pas » que les autres threads existent.

Dans la méthode run, la variable isCancel est mise dans le cache du thread enfant quand il est utilisé pour la première fois. Cette opération revient à écrire le code suivant :

public void run()
{
boolean isCancelCached = this.isCancel;
while (!isCancelCached)
{
Thread.sleep(1000);
System.out.println("Tick");
}
}

L'appel à la méthode cancel depuis l'autre thread modifiera la valeur de isCancel dans la mémoire normale (lente), mais pas dans les caches des autres threads.

public static void main(String[] args)
{
Clock clock = new Clock();
Thread clockThread = new Thread(clock);
clockThread.start();

Thread.sleep(10000);
clock.cancel();
}

« Ouah ! Et ils ont trouvé une belle solution pour cela aussi, comme avec synchronized ? »

« Tu ne vas pas le croire ! »

La première idée a été de désactiver le cache, mais les programmes s'exécutaient alors bien plus lentement. Une autre solution a alors émergé.

Le mot-clé volatile est né. Nous mettons ce mot-clé avant une déclaration de variable pour indiquer que sa valeur ne doit pas être mise en cache. Plus précisément, ce n'est pas qu'elle ne peut pas être mise en cache, c'est tout simplement qu'elle doit toujours être lue et écrite dans la mémoire normale (lente).

Voici comment corriger notre solution pour que tout fonctionne bien :

Code Description
class Clock implements Runnable
{
private volatile boolean isCancel = false;

public void cancel()
{
this.isCancel = true;
}

public void run()
{
while (!this.isCancel)
{
Thread.sleep(1000);
System.out.println("Tick");
}
}
}
Le modificateur volatile fait qu'une variable doit toujours être lue et écrite dans la mémoire normale partagée par tous les threads.
public static void main(String[] args)
{
Clock clock = new Clock();
Thread clockThread = new Thread(clock);
clockThread.start();

Thread.sleep(10000);
clock.cancel();
}

« C'est tout ? »

« C'est tout. Simple et élégant. »