Comprendre les threads
En Java, un thread est un flux d'exécution indépendant au sein d'un processus. Losrqu'un programme s'exécute, plusieurs threads fonctionnent en arrière-plan, comme le thread principal, le thread de garbage collection, etc. Le thread principal, lancé via la méthode main, constitue le point d'entrée du programme. Si plusieurs threads sont créés dans un processus, leur ordre d'exécution est contrôlé par l'ordonnanceur du système et ne peut pas être modifié manuellement. L'accès simultané à une ressource partagée peut entraîner des conflits, nécessitant une synchronisation pour éviter les problèmes de concurrence. L'utilisation de threads engendre une surcharge supplémentaire, notamment en termes de temps de planification CPU et de coûts de synchronisation. De plus, chaque thread interagit avec sa propre mémoire de travail, ce qui peut provoquer des incohérences de données si la gestion mémoire n'est pas appropriée.
Implémentation des threads avec la classe Thread
La première approche consiste à étendre la classe Thread et à redéfinir sa méthode run. Voici un exemple simplifié où deux threads s'exécutent de manière concurrente :
class PremierThread extends Thread {
@Override
public void run() {
for (int index = 0; index < 10; index++) {
System.out.println("Traitement dans le thread: " + index);
}
}
}
public class TestThread {
public static void main(String[] args) {
PremierThread thread1 = new PremierThread();
thread1.start(); // Démarre le thread, l'exécution est planifiée par le CPU
for (int compteur = 0; compteur < 10; compteur++) {
System.out.println("Activité principale: " + compteur);
}
}
}
La méthode start lance le thread et son exécution est gérée par l'ordonnanceur, tandis que run exécute directement le code dans le thread courant.
Téléchargement d'images avec des threads
Pour illustrer l'asynchronisme, voici un exemple où plusieurs threads téléchargent des fichiers en parallèle. Chaque thread est paramétré avec une URL et un nom de fichier :
import java.net.URL;
import java.io.File;
import java.io.IOException;
import org.apache.commons.io.FileUtils; // Suppose l'utilisation d'une bibliothèque IO
class Telechargeur implements Runnable {
private String lien;
private String nomFichier;
public Telechargeur(String lien, String nomFichier) {
this.lien = lien;
this.nomFichier = nomFichier;
}
@Override
public void run() {
try {
FileUtils.copyURLToFile(new URL(lien), new File(nomFichier));
System.out.println("Téléchargement terminé: " + nomFichier);
} catch (IOException e) {
e.printStackTrace();
}
}
}
public class TestTelechargement {
public static void main(String[] args) {
Telechargeur t1 = new Telechargeur("https://exemple.com/image1.jpg", "fichier1.jpg");
Telechargeur t2 = new Telechargeur("https://exemple.com/image2.jpg", "fichier2.jpg");
Telechargeur t3 = new Telechargeur("https://exemple.com/image3.jpg", "fichier3.jpg");
new Thread(t1).start();
new Thread(t2).start();
new Thread(t3).start();
}
}
Cet exemple utilise l'interface Runnable, ce qui permet de séparer la tâche du thread et de favoriser la réutilisabilité.
Implémentation avec l'interface Runnable
Une méthode courante consiste à implémenter l'interface Runnable et à redéfinir run. Cette approche est recommandée car elle permet de découpler le code métier de la classe de thread :
class TacheSimple implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("Exécution de la tâche: " + i);
}
}
}
public class TestRunnable {
public static void main(String[] args) {
TacheSimple tache = new TacheSimple();
new Thread(tache).start();
for (int j = 0; j < 5; j++) {
System.out.println("Thread principal: " + j);
}
}
}
Gestion de la concurrence
L'accès concurrent à des ressources partagées peut provoquer des conditions de course. Voici un exemple de simulation de vente de billets avec synchronisation :
class VenteBillets implements Runnable {
private int billetsRestants = 50;
@Override
public void run() {
while (true) {
if (billetsRestants <= 0) {
break;
}
try {
Thread.sleep(15); // Simulation d'un délai
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (this) {
if (billetsRestants <= 0) {
break;
}
System.out.println(Thread.currentThread().getName() + " a acheté le billet n° " + billetsRestants);
billetsRestants--;
}
}
}
}
public class TestConcurrence {
public static void main(String[] args) {
VenteBillets ventes = new VenteBillets();
new Thread(ventes, "ClientA").start();
new Thread(ventes, "ClientB").start();
new Thread(ventes, "ClientC").start();
}
}
Le bloc synchronized assure que seul un thread à la fois accède à la section critique, évitant ainsi les erreurs de données.
Autres mécanismes de création de threads
L'interface Callable permet de retourner un résultat et de lancer des exceptions, ce qui est utile pour des tâches asynchrones plus complexes. Les classes ExecutorService et Future fournissent des abstractions pour gérer des pools de threads.
États et cycle de vie des threads
Un thread peut se trouver dans différents états : nouveau, runnable, bloqué, en attente, ou terminé. La transition entre ces états est contrôlée par des méthodes comme start, sleep, wait, et join.
Synchronisation avancée avec Lock
Depuis JDK 5, l'interface Lock offre plus de flexibilité que le mot-clé synchronized, avec des fonctionnalités telles que les verrous réentrant, les conditions, et les tentatives de verrouillage. Les classes ReentrantLock et ReadWriteLock sont couramment utilisées pour une gestion fine de la concurrence.
Communication entre threads
La communication entre threads peut être réalisée via des mécanismes comme wait, notify, et notifyAll, qui permettent de synchroniser des actions basées sur des conditions partagées.