En Java, la gestion des threads permet l'exécution concurrente au sein d'un processus. Un processus est une instance d'un programme en cours d'exécution, avec son propre espace mémoire, tandis qu'un thread est une sous-unité d'exécution partageant les ressources du processus, comme la mémoire et les descripteurs de fichiers.
Les threads et les processus ont des points communs : ils permettent le multitâche, ont un cycle de vie et sont gérés par l'ordonnanceur du système d'exploitation. Cependant, les threads sont plus légers, car ils partagent l'espace mémoire du processus parent, alors que les processus sont isolés.
Implémentation des Threads
Pour créer un thread en Java, on peut étendre la classe Thread ou implémenter l'interface Runnable. Voici un exemple en étendant Thread :
class MonThread extends Thread {
@Override
public void run() {
System.out.println("Exécution du thread");
}
}
public class Demarrage {
public static void main(String[] args) {
MonThread thread = new MonThread();
thread.start();
}
}
Avec l'interface Runnable, on sépare la tâche de la gestion du thread :
class Tache implements Runnable {
@Override
public void run() {
System.out.println("Tâche exécutée");
}
}
public class Demarrage {
public static void main(String[] args) {
Tache tache = new Tache();
Thread thread = new Thread(tache);
thread.start();
}
}
On peut aussi utiliser des expressions lambda pour plus de concision, en passant directement la tâche au constructeur de Thread.
Utilisation Répétée des Threads
Les threads peuvent être réutilisés dans une boucle pour traiter plusieurs tâches. Par exemple, un thread serveur peut attendre des requêtes indéfiniment, sauf si on l'interrompt :
import java.util.concurrent.TimeUnit;
class Serveur implements Runnable {
@Override
public void run() {
try {
while (!Thread.currentThread().isInterrupted()) {
synchronized (this) {
wait();
System.out.println("Traitement d'une requête");
TimeUnit.SECONDS.sleep(2);
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
États des Threads
Un thread Java peut être dans six états, accessibles via Thread.getState() :
- NEW : créé mais non démarré.
- RUNNABLE : démarré, en cours d'exécution ou prêt à être exécuté.
- BLOCKED : en attente d'un verrou pour entrer dans une section synchronisée.
- WAITING : en attente indéfinie, par exemple via
wait()oujoin(). - TIMED_WAITING : en attente temporaire, comme avec
sleep(). - TERMINATED : exécution terminée ou exception non gérée.
Les transitions entre états sont déclenchées par des appels de méthode. Par exemple, start() passe de NEW à RUNNABLE, tandis que wait() passe de RUNNABLE à WAITING en libérant le verrou.
Synchronisation et Communication
Pour garantir la sécurité des threads, Java utilise le mot-clé synchronized. Il protège les sections critiques en assurant l'exclusion mutuelle. Voici les différentes utilisations :
- Sur une méthode d'instance : le verrou est l'objet courant (
this). - Sur une méthode statique : le verrou est l'objet
Classde la classe. - Sur un bloc de code : on peut spécifier un objet verrou arbitraire pour un contrôle plus fin.
Le mot-clé volatile assure la visibilité des modifications des variables partagées entre threads, sans garantir l'atomicité. Il empêche également les réordonnancements d'instructions. Par exemple :
class Moniteur {
private volatile boolean estVide = false;
public void marquerVide() {
estVide = true;
}
public void attendreRemplissage() {
while (!estVide) {
// Attendre
}
}
}
La communication entre threads peut utiliser wait(), notify() et notifyAll() pour coordonner l'exécution, comme dans un modèle producteur-consommateur.
Exemple Pratique
Considérons un scénario avec une guichet bancaire et des clients. Le guichet attend un client, le traite, puis attend à nouveau. Les clients arrivent et notifient le guichet :
import java.util.concurrent.TimeUnit;
class Guichet implements Runnable {
private final int numero;
public Guichet(int numero) {
this.numero = numero;
}
@Override
public void run() {
try {
while (!Thread.currentThread().isInterrupted()) {
synchronized (this) {
wait();
System.out.println("Guichet " + numero + " traite le client");
TimeUnit.SECONDS.sleep(1);
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
class Client implements Runnable {
private final String nom;
private final Guichet guichet;
public Client(String nom, Guichet guichet) {
this.nom = nom;
this.guichet = guichet;
}
@Override
public void run() {
try {
System.out.println(nom + " arrive au guichet");
synchronized (guichet) {
guichet.notify();
System.out.println(nom + " est servi");
TimeUnit.SECONDS.sleep(2);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
public class Application {
public static void main(String[] args) throws InterruptedException {
Guichet guichet = new Guichet(1);
Thread threadGuichet = new Thread(guichet, "Guichet-1");
threadGuichet.start();
TimeUnit.SECONDS.sleep(1);
Client client = new Client("Client-1", guichet);
Thread threadClient = new Thread(client, "Client-1");
threadClient.start();
TimeUnit.SECONDS.sleep(5);
threadGuichet.interrupt();
}
}
Cet exemple illustre l'utilisation des verrous et de la communication pour synchroniser l'accès aux ressources partagées.
Points Clés à Retenir
Les threads partagent la mémoire du processus, d'où la nécessité de synchronisation. synchronized et volatile sont des mécanismes essentiels pour éviter les conditions de course et assurer la cohérence des données. La compréhension des états et des transitions des threads est cruciale pour développer des applications concurrentes robustes.