US

Synchronisation

On vient de voir, à la section précédente, que lorsque plusieurs threads accèdent en même temps à un même objet partagé, des problèmes peuvent survenir. Le problème était dû au fait que plusieurs threads pouvaient être en même temps, de manière concurrente, dans une même méthode qui modifie l'état de l'objet. Une solution pour pallier au problème consisterait à rendre l'exécution de la méthode en question atomique. C'est-à-dire que l'exécution de la méthode se passe en un seul bloc, indivisible. En effet, le scheduler peut arrêter l'exécution d'un thread après chaque instruction élémentaire exécutée. Dès lors, si l'exécution de la méthode apparait comme un bloc atomique, le problème qu'on a eu ne pourrait plus se produire. Ceci n'est pas réalisable en Java, mais on va quand même pouvoir résoudre le problème avec un autre mécanisme : la synchronisation.

Le mot réservé synchronized

On ne peut pas rendre l'exécution d'une méthode atomique en Java. Par contre, ce qu'on va pouvoir faire, c'est lorsqu'un thread est en train d'exécuter une méthode, empêcher l'accès à cette méthode à tous les autres threads. Et pour ce faire, c'est simple, il suffit de marquer la méthode avec le mot réservé synchronized. On va donc modifier la méthode withdraw de la classe BankAccount ainsi :

1 
2 
3 
4 
5 
6 
7 
8 
public synchronized void withdraw (int amount)
{
    if (amount <= balance)
    {
        balance -= amount;
        System.out.println ("Compte débité de " + amount + " euros");
    }
}
Listing 2.4 Méthode withdraw de la classe BankAccount.

Maintenant, supposons que Alice a commencé à exécuter la méthode withdraw, elle vient d'évaluer la condition du if. À ce moment, le scheduler décide d'arrêter Alice et de laisser Bob s'exécuter. Ce dernier va donc tenter d'appeler la méthode withdraw, mais comme Alice est déjà occupée dedans, Bob va passer dans l'état bloqué, et ce jusqu'à ce que Alice ait fini d'exécuter la méthode.

Ceci nous permet donc de résoudre le problème auquel on était confronté, mais comme nous allons le voir, il faut utiliser ce mécanisme avec grande précaution. Avant cela, il faut bien comprendre comment il fonctionne et c'est ce qu'on va faire tout de suite.

Lock

Tous les objets possèdent un built-in lock. Ce lock est nécessaire à un thread qui souhaite accéder à une méthode marquée synchronized. Un thread d'exécution va pouvoir prendre le lock d'un objet, et une fois cela fait, il va pouvoir entrer librement dans toutes les méthodes marquées synchronized. Une fois que le thread a terminé, il rend le lock de l'objet.

Lorsqu'un thread tente d'exécuter une méthode marquée synchronized, deux cas peuvent se produire. Soit le lock de l'objet sur lequel la méthode est appelée est encore disponible et dans ce cas, le thread s'empare du lock et peut exécuter la méthode. Dans l'autre cas, le lock a déjà été pris et le thread est bloqué, en attente de la libération du lock.

Voyons cela sur un exemple. On va repartir de l'exemple d'Alice et Bob qui veulent tous les deux accéder à la méthode withdraw d'un objet BankAccount partagé. Dans la situation initiale, illustrée sur la figure suivante, le lock (représenté par le disque noir sur l'objet) est toujours disponible sur l'objet.

Built-in lock d'un objet
Figure 2.2 Built-in lock d'un objet.

Maintenant, supposons que Alice souhaite appeler la méthode withdraw de l'objet. Comme elle est marquée synchropnized, il faut que le lock soit disponible. Comme c'est bien le cas, Alice s'en empare et peut exécuter la méthode comme le montre la figure suivante.

Thread est en possession du lock d'un objet
Figure 2.3 Thread est en possession du lock d'un objet.

Supposons maintenant que le scheduler décide d'arrêter d'exécuter le thread Alice (qui passe donc dans l'état Runnable) et fasse passer Bob dans l'état Running. Bob veut également exécuter la méthode withdraw, mais le lock n'est plus disponible. Le thread Bob va se bloquer et passer dans l'état Blocked. Il est en attende du lock et restera bloqué jusqu'à ce qu'il puisse enfin avoir le lock qu'il attend. La figure suivante montre cette situation.

Thread bloqué en l'attente du lock d'un objet
Figure 2.4 Thread bloqué en l'attente du lock d'un objet.

Une fois qu'Alice aura terminé d'exécuter la méthode withdraw, le lock de l'objet sera libéré et Bob va s'en emparer et quitter l'état Blocked pour passer dans l'état Runnable. Dès que le scheduler l'aura choisi, Bob pourra commencer à exécuter la méthode withdraw.

Ce mécanisme permet donc de s'assurer qu'un seul thread à la fois est en train d'exécuter des méthodes d'un objet. Il ne permet pas de faire en sorte qu'une méthode soit exécutée complètement par un thread d'une seule fois sans être interrompu. Un thread peut détenir autant de locks qu'il le souhaite, et lors de l'appel de la méthode sleep, le thread passe dans l'état Waiting et conserve tous les locks qu'il possède.

Méthodes de classe

On peut également marquer les méthodes de classe synchronized. On vient de voir que chaque objet possède un lock, mais chaque classe en possède également un. Ce dernier est utilisé pour gérer les méthodes de classe synchronized.

En fait, la machine virtuelle Java crée automatiquement une instance de la classe Class pour chaque classe différente qui est utilisée dans le programme en cours d'exécution. Les locks des classes sont en fait les locks des instances de la classe Class utilisées pour représenter ces classes.

Bloc synchronized

Il faut bien se rendre compte que ce mécanisme est contraire à la concurrence, puisqu'on n'autorise plus plusieurs choses à se passer en même temps, c'est chacun son tour ! Il faut donc utiliser le moins possible synchronized.

Il n'est parfois pas nécessaire que tout le corps d'une méthode soit synchronized. Java permet de définir un bloc synchronized, c'est-à-dire un bloc de code dans lequel un thread pourra entrer que s'il possède le lock d'un certain objet, qu'il va falloir spécifier.

Le listing suivant montre une classe avec deux méthodes contenant un bloc synchronized. Dans le cas de cet exemple, c'est exactement comme si on avait marqué les méthodes synchronized. On peut voir que pour la première méthode, on synchronise sur this, c'est-à-dire qu'un thread voulant exécuter ce bloc aura besoin du lock de l'instance de l'objet sur laquelle la méthode est appelée. Pour la seconde méthode, on synchronise sur SynchBlock.class. Écrire X.class permet d'obtenir l'instance de la classe Class associée à la classe X. Un thread voulant exécuter cette méthode devra donc obtenir le lock associé à la classe SynchBlock.

1 
2 
3 
4 
5 
6 
7 
8 
9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
public class SynchBlock
{
    public void instanceMethod()
    {
        synchronized (this)
        {
            // ...
        }
    }
 
    public static void classMethod()
    {
        synchronized (SynchBlock.class)
        {
            // ...
        }
    }
}
Listing 2.5 Méthodes synchronized.

Que faut-il synchroniser ?

Comment faut-il choisir quelles sont les méthodes qu'il faut marquer synchronized ? Reprenons la classe BankAccount qu'on a utilisée jusqu'à présent. Le listing suivant reprend cette classe, avec ses méthodes marquées synchronized afin de pouvoir partager les instances de la classe entre plusieurs threads.

1 
2 
3 
4 
5 
6 
7 
8 
9 
10 
11 
12 
13 
14 
15 
16 
17 
public class BankAccount
{
    private int balance = 200;
 
    public synchronized int getBalance()
    {
        return balance;
    }
 
    public synchronized void withdraw (int amount)
    {
        if (amount <= balance)
        {
            balance -= amount;
        }
    }
}
Listing 2.6 La classe BankAccount, utilisable entre plusieurs threads.

Comme vous l'avez sans doute remarqué, les deux méthode de la classe ont été marquées synchronized. Pourquoi ? Supposons que seule la méthode withdraw soit marquée synchronized et qu'il y a 200 euros sur le compte. Un thread est actuellement en train d'exécuter la méthode pour retirer 200 euros, le test du if est passé et l'argent va donc pouvoir être débitée du compte. À ce moment, un autre thread est en train d'appeler la méthode getBalance pour savoir s'il reste assez d'argent sur le compte. La méthode n'étant pas synchronized, cela signifie qu'autant de threads qu'on le souhaite peuvent exécuter cette méthode, sans contrainte. Ce thread voit qu'il reste assez d'argent, alors qu'en réalité, il ne reste plus d'argent sur le compte, mais comme la méthode withdraw n'est pas exécutée de manière atomique, la mise à jour du solde du compte ne se fait pas en une seule opération.

En toute généralité, toute méthode qui modifie l'état d'un objet doit être marquée synchronized pour pouvoir être utilisée par plusieurs threads. De même, toute méthode qui permet d'accéder à l'état d'un objet doit être marquée synchronized, si l'état n'est pas toujours garanti changer de manière atomique. On va revenir sur cela à la dernière section de ce chapitre.

  • Espace membre
  • Learning Center
  • Les forums
  • Livre d'or
  • Imprimer
  • Boutique
  • Info
  • Règlement
  • Erreur
  • Newsletter

MyPagerank.Net

Firefox 3.6

Browse Happy logo

Open Clip Art Library

Join our Facebook Group

Twitter

Copyright © 2000-2012 UKO. Toute reproduction strictement interdite sans autorisation du webmaster

Valid XHTML 1.1 !
Valid CSS2 !
Level Triple-A conformance icon, W3C-WAI Web Content Accessibility Guidelines 1.0
ICRA Internet Content Rating Association
Creative Commons License
Site optimisé pour Firefox avec une résolution 1024x768 --- Page chargée en 0.0465040 secondes --- This site uses Thumbshots previews