Sécurité des threads
Les problèmes de sécurité des threads surviennent pour plusieurs raisons fondamentales.
Cause racine : ordonnancement aléatoire des threads
L'ordonnancement des threads par le système d'exploitation est imprévisible, ce qui peut condurie à des comportements inattendus.
Modification de données partagées
La modification simultanée d'une même variable par plusieurs threads pose des risques. En revanche, la lecture par plusieurs threads ou la modification de variables distinctes est généralement sans danger. Voici un exemple illustrant le problème :
// Exemple de compteur non synchronisé
class CompteurNonProtege {
private int valeur = 0;
public void incrémenter() { valeur++; } // Opération non atomique
}
Opérations non atomiques
Une opération atomique est indivisible : elle réussit complètement ou échoue sans état intermédiaire. Les opérations simples comme l'affectation (=) en Java sont atomiques. Cependant, des instructions apparentes comme valeur++ impliquent des étapes multiples (lecture, modification, écriture), ce qui peut entraîner des pertes de mises à jour en environnement multithreadé.
Problèmes de visibilité
En raison des caches CPU et des optimisations compilateur, les modifications d'un thread peuvent ne pas être immédiatement visibles par d'autres sans mécanismes de synchronisation. L'exemple suivant montre une boucle infinie possible :
class ProblèmeVisibilité {
private boolean indicateur = false; // Manque de volatile
public void basculer() { indicateur = true; }
public void exécuter() { while(!indicateur); } // Peut ne jamais s'arrêter
}
Réordonnancement des instructions
Les compilateurs et processeurs peuvent réordonner les instructions pour optimiser les performances, ce qui peut briser des hypothèses temporelles implicites. Cela pose des risques en multithreadé, contrairement au single-thread.
Le mot-clé synchronized
Exclusion mutuelle
Pour résoudre les problèmes de sécurité, on transforme les opérations non atomiques en opérations atomiques en utilisant des verrous. Le mot-clé synchronized encapsule une section de code pour garantir l'atomicité. L'acquisition du verrou se fait à l'entrée, et la libération à la sortie. Lorsque plusieurs threads s'affrontent pour un même verrou, le premier à l'acquérir bloque les autres, qui restent en attente jusqu'à sa libération.
Réentrance
Les blocs synchronized sont réentrants pour un même thread, évitant ainsi les interblocages. Par exemple, imbriquer deux verrous sur le même objet fonctionne correctement :
for (int i = 0; i < 50000; i++) {
synchronized (verrou) {
synchronized (verrou) {
compteur++;
}
}
}
En Java, synchronized est un verrou réentrant, ce qui empêche les situations d'interblocage dans ce cas.
Utilisation de synchronized
Méthode de base :
synchronized (objetVerrou) {
// Code à protéger
}
Application à une méthode non statique : Cette forme est équivalente à synchroniser sur this.
Application à une méthode statique : Ici, le verrou est appliqué sur l'objet de classe, comme ClasseCompteur.class.
Concepts essentiels
Pour manipuler des objets de classe comme ClasseCompteur.class, on utilise la réflexion. En Java, à l'exécution, on peut accéder aux métadonnées d'une classe ou d'un objet, telles que ses membres, méthodes, hiérarchie d'héritage, etc. Les fichiers .java compilent en .class, qui sont chargés en mémoire par la JVM pour obtenir les objets de classe. Un point crucial est que pour éviter les conflits de verrouillage, il faut que les threads synchronisent sur le même objet, peu importe lequel, tant que c'est identique.