Des cas typiques de programmation mettent souvent en concurrence un producteur créant un flux de données et un consommateur utilisant le flux de données précité. Par exemple, un thread producteur peut écrire un texte dans un fichier, tandis qu'un consommateur lit ce-dernier, ou respectivement qu'un crédite un compte bancaire et que l'autre débite ce même compte, etc..
Il faut synchroniser les méthodes ou les blocs d'instructions critiques, c'est à dire ceux qui agissent directement sur les données communes.
public class Fichier { public synchronized String lire() { //Instructions... } public synchronized void ecrire(String txt) { //Instructions... } } //ou... public class Compte { public synchronized debiter(double somme) { //Instructions... } public synchronized crediter(double somme) { //Instructions... } }
De cette manière, lorsqu'un thread accède à une donnée partagée d'un objet particulier, par l'intermédiaire d'une des méthodes synchronisées, l'autre devient inaccessible aux autres threads concurrents au même moment. A la fin du traitement opéré par la méthode synchronisée accédée, les autres méthodes synchronisées deviennent à nouveau accessibles par les threads concurrents.
Evidemment, s'il existe plusieurs instances d'une classe particulière, alors une méthode d'instance synchronisée pourra être exécutée par objet simultanément, par d'autres threads.
//... compte1 = new Compte(); compte2 = new Compte(); Thread debit1 = new Thread(new Debit(compte1, 100.00)); Thread debit2 = new Thread(new Debit(compte2, 2050.00)); Thread debit3 = new Thread(new Debit(compte1, 820.00)); Thread credit = new Thread(new Credit(compte1, 2500.00)); debit1.start(); debit2.start(); debit3.start(); credit.start(); //... public class Debit implements Runnable { private Compte compte; private double somme; public Debit(Compte compte, double somme){ this.compte = compte; this.somme = somme; } public void run(){ compte.debiter(somme); } } public class Credit implements Runnable { private Compte compte; private double somme; public Credit(Compte compte, double somme){ this.compte = compte; this.somme = somme; } public void run(){ compte.crediter(somme); } }
Dans cet exemple, il existe deux comptes, à partir desquels, il est possible d'accéder en même temps à une opération de débit par deux threads différents, en l'occurrence debit1
et debit2
. Par contre, les threads debit3
et credit
devront attendre que debit1
ait terminé avec l'opération débitrice.
Les méthodes non-synchronisées peuvent être sollicitées à n'importe quel moment et par n'importe quel thread, indépendamment des méthodes synchronisées.
La synchronisation permet d'éviter des problèmes de sureté (safety), mais engendre d'autres risques durant l'activité des threads (liveness).
Le principal écueil réside dans l'interblocage du programme, c'est à dire que tous les threads s'attendent mutuellement et, donc, aucun ne peut prendre le moniteur. Autrement dit, le programme est bloqué, car chaque thread attend la libération du verrou en vain.
public class Compte { public synchronized void retirer(double somme) { // Effectue un retrait... } public synchronized void verser(double somme) { // Effectue un versement } public synchronized void virer(Compte dest, double somme){ this.retirer(somme); dest.verser(somme); } } public class Virement extends Thread { private Compte dest; private Compte ori; private double somme; public void Virement( Compte destinataire, Compte origine, double somme){ this.dest = destinataire; this.ori = origine; this.somme = somme; } public void run(){ ori.virer(dest, somme); } } public static void main(String[] args) { Compte cptDest = new Compte(...); Compte cptOri = new Compte(...); Thread t1 = new Virement(cptDest, cptOri, 2000); Thread t2 = new Virement(cptOri, cptDest, 1000); t1.start(); t2.start(); }
Dans ce cas, le thread t1 appelle la méthode verser() de la classe Compte et obtient le verrou sur l'instance de cette classe (cptOri). De même, le thread t2 obtient le verrou sur l'objet cptDest en invoquant la méthode verser(). L'invocation de la méthode retirer() par les threads t1 et t2 se déroule sans difficulté, mais lorsque les threads tentent respectivement d'appeler simultanément la méthode verser() sur les objets cptOri et cptDest(), un conflit se produit puisqu'un verrou est déjà posé sur les deux objets. La solution dans ce cas, consisterait à écrire le processus de virement directement dans le thread.
public void run(){ ori.retirer(somme); dest.verser(somme); }
De cette manière, le thread t1 obtient le verrou sur l'objet cptOri en invoquant la méthode retirer(), puis le libère à la fin de l'exécution de cette méthode. Le thread t2 fait la même chose pour l'objet cptDest. L'appel de la méthode verser() par les deux threads, entraîne le verrouillage des objets précédemment libérés. Au terme de l'exécution de la méthode verser(), les verrous se libèrent.
Il peut également se produire des problèmes de conflit (contention) :
Pour résoudre ce problème, les threads avec une priorité elevée doivent périodiquement invoquer les méthodes sleep() ou yield() pour donner aux threads ayant une priorité moins élevée, les mêmes occasions de s'exécuter.
La mise en sommeil prolongée des threads (dormancy) se produit lorsqu'un thread qui est en attente ne peut jamais redevenir exécutable. Le thread a invoqué la méthode wait(), puis aucun des autres threads n'invoque la méthode notify() ou notifyAll(). L'endormissement des threads peut être évité en prenant des précautions quant à l'utilisation de la méthode wait() sur un thread, sans s'être assuré d'invoquer la méthode notify() ou notifyAll() sur d'autres threads.
Exemple [voir]// Fichier : Credit.java public class Credit implements Runnable { private Compte compte; private double somme; public Credit(Compte compte, double somme) { this.compte = compte; this.somme = somme; } public void run() { double solde = compte.getSolde(); compte.crediter(somme); System.out.println("################## CREDIT ###################" + "\n# Référence Client : " + compte.getReference() + "\n# Solde AvO : " + solde + "\n# Somme créditée : " + somme + "\n# Solde ApO : " + compte.getSolde()); } } // Fichier : Debit.java public class Debit implements Runnable { private Compte compte; private double somme; public Debit(Compte compte, double somme) { this.compte = compte; this.somme = somme; } public void run() { double solde = compte.getSolde(); compte.debiter(somme); System.out.println("################### DEBIT ###################" + "\n# Référence Client : " + compte.getReference() + "\n# Solde AvO : " + solde + "\n# Somme débitée : " + somme + "\n# Solde ApO : " + compte.getSolde()); } } // Fichier : Compte.java public class Compte { private int refClient; private double solde; private int nbOperations; public Compte(int ref, double solde) { this.refClient = ref; this.solde = solde; nbOperations = 0; } public synchronized void debiter(double somme) { solde -= somme; nbOperations++; } public synchronized void crediter(double somme) { solde += somme; nbOperations++; } public synchronized double getSolde() { return solde; } public synchronized int getNbOperations() { return nbOperations; } public int getReference() { return refClient; } } // Fichier : Banque.java import java.util.Random; public class Banque { private static int nbComptes = 3; private static int nbOperations = 10; private static int sommeMax = 100000; private static Random generateur = new Random(); public static void main(String[] args) { Compte[] comptes = creerComptes(); afficherEtatComptes(comptes); Thread[] operations = creerOperations(comptes); lancerOperations(operations); patienter(operations); afficherEtatComptes(comptes); } public static Compte[] creerComptes() { Compte[] comptes = new Compte[nbComptes]; for (int i = 0; i < comptes.length; i++) { comptes[i] = new Compte(i, generateur.nextInt(sommeMax)); } return comptes; } public static Thread[] creerOperations(Compte[] comptes) { Thread[] operations = new Thread[nbOperations]; for (int i = 0; i < operations.length; i++) { if (i % 3 == 0) operations[i] = new Thread(new Credit(comptes[generateur .nextInt(nbComptes)], generateur.nextInt(sommeMax))); else operations[i] = new Thread(new Debit(comptes[generateur .nextInt(nbComptes)], generateur.nextInt(sommeMax))); } return operations; } public static void lancerOperations(Thread[] operations) { for (int i = 0; i < operations.length; i++) { operations[i].start(); } } public static void afficherEtatComptes(Compte[] comptes) { System.out.println("############# ETATS DES COMPTES #############"); for (int i = 0; i < comptes.length; i++) { System.out.println("############## ETAT DU COMPTE ###############" + "\n# Référence Client : " + comptes[i].getReference() + "\n# Solde : " + comptes[i].getSolde() + "\n# Nombre d'opérations : " + comptes[i].getNbOperations()); } System.out.println("#############################################"); } public static void patienter(Thread[] operations) { for (int i = 0; i < operations.length; i++) { if (operations[i].isAlive()) { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } i = 0; } } } } |