Le modificateur synchronized permet de synchroniser des méthodes ou des blocs de codes.

Lorsqu'une classe déclare une ou plusieurs zones synchronisées, un verrou mutex (mutual exclusion) est créé par la machine virtuelle Java pour la classe ou pour son instance. C'est ce verrou qui doit être acquis par les threads pour accèder au code synchronisée. Le verrou mutex ressemble à une porte qui se ferme dès qu'une personne est dans un espace déterminé et s'ouvre dès qu'elle en est sortie pour laisser entrer une autre personne. La maison représente l'objet auquel est associé le verrou.

Si plusieurs instances d'une classe possèdant des zones synchronisées, sont utilisées par différents threads, alors il existera un verrou pour chacune des instances. Dans ce cas, les threads peuvent exécuter des sections critiques simultanément sur les objets auprès desquels, ils détiennent le verrou. Ainsi, plusieurs maisons peuvent ouvrir leur porte à plusieurs personnes dans le même laps de temps. Evidemmment, les personnes sont invitées à entrer un par un.

Un verrou possède une portée bien délimitée. Ces limites sont le début et la fin d'une section critique, c'est-à-dire, la première et la dernière instruction d'une méthode ou d'un bloc de code synchronisé. Réduire la portée d'un verrou permet d'optimiser les performances d'une application multi-threads. En effet, il est souhaitable d'englober essentiellement le code vraiment sensible dans un bloc synchronisé et de laisser les instructions non-critiques dans un espace qui pourra être exécuter parallèlement par plusieurs threads. Pour des opérations lourdes ne mettant pas en jeu des variables sensibles, telles que le chargement de fichier ou de ressources distantes, cela s'avère décisif afin de profiter pleinement des avantages qu'offrent les threads.

En outre, synchroniser des méthodes peut conduire à des interblocages dans des cas précis.

public class Interblocage {
  public int variableSensible = -1;
  public synchronized void ecrire(){
    while(true){
      try{
        if(variableSensible == -1){
          variableSensible = (int)(Math.random() * 1000);
          break;
        }
        Thread.sleep(100);
      }
      catch(InterruptedException e){}
    }
  }
  public synchronized int lire(){
    int valeur = variableSensible;
    variableSensible = -1;
    return valeur;
  }
}
  • Un thread A accède à la méthode ecrire() (variableSensible = -1).
  • Un thread B tente d'accèder à la méthode ecrire() (variableSensible = -1).
  • Le thread B se met en attente du verrou (variableSensible = -1).
  • Le thread A sort de la méthode et libère le verrou (variableSensible = 458).
  • Le thread B entre dans la méthode ecrire() (variableSensible = 458).
  • Le thread A tente d'accèder à la méthode lire() (variableSensible = 458).
  • Le thread A se met en attente du verrou (variableSensible = 458).
  • Le thread B entre dans une boucle infinie (variableSensible = 458).
  • Le programme est bloqué.

Cette situation est un cas particulier d'interblocage mettant en jeu une condition variableSensible == -1 et un verrou mutex. La solution à ce problème consiste à synchroniser le bloc de code contenant la variable sensible. De cette manière, la boucle infinie n'empêche plus d'autres threads d'obtenir le verrou pour accèder à l'une et à l'autre des méthodes.

public void ecrire(){
  while(true){
    try{
      synchronized(this){
        if(variableSensible == -1)
          variableSensible = (int)(Math.random() * 1000);
      }
        Thread.sleep(100);
    }
    catch(InterruptedException e){}
  }
}

Les performances d'un objet, qui déclare des méthodes ou/et des blocs synchronisés, sont indubitablement affectées par l'acquisition du verrou d'exclusion mutuelle. Cela est inhérent à la synchronisation. Par exmple, la collection Vector sécurisée pour des accès concurrents, demeure assurément moins rapide qu'un objet ArrayList. Sécuriser des sections critiques améliore le fonctionnement d'une application multi-threads, mais se ressent aux niveaux de ses performances. Il faut donc un habile dosage entre la synchronisation indispensable et l'exigence d'optimisation des accès concurrents.

L'invocation de méthodes synchronisées au sein d'une méthode elle même synchronisée ne pose pas de problèmes. En effet, le thread peut accéder aux méthodes synchronisées imbriquées puisqu'il détient déjà le verrou depuis qu'il est entré dans la méthode parente. A partir de l'instant ou un verrou est acquis, toutes les méthodes synchronisées peuvent être appeleées par un thread exécutant une méthode synchronisée.

public synchronized int ecrire(){
  if(autoriser())
    variableSensible = (int)(Math.random() * 1000);
  return lire();
}
public synchronized boolean autoriser(){
 return variableSensible == -1;
}
public synchronized int lire(){
  int valeur = variableSensible;
  variableSensible = -1;
  return valeur;
}

Les blocs synchronisés peuvent être imbriqués. Les objets sur lesquels s'applique la synchronisation sont choisis en fonction des traitements qu'ils doivent subir dans la méthode. Effectivement, tant q'un objet sensible est concerné par des opérations, il doit être sécurisé. Dans le cas contraire, il risquerait de se retrouver dans un état instable préjudiciable pour le bon fonctionnement d'une application.

public void ajouterFragment(DocumentXML document, FragmentXML fragment){
  if(fragment == null || document == null) return;
  synchronized(fragment){
    if(fragment.hasChildNodes()){
      NodeList enfants = frgament.getChildNodes();
      synchronized(document){
        NodeList liste = document.getElementsByTagName(nom);
        String nom = fragment.getFirstChild().getNodeName();
        for(int i = 0; i < enfants.getLength(); i++){
          booelan trouve = false;
          for(int j = 0; j < liste.getLength(); j++){
            if(liste.item(j).isEqualNode(enfants.item(i)){
              trouve = true;
              break;
            }
          }
          if(!trouve)
            document.getDocumentElement().appendChild(fragment);
        }
      }
    }
  }
}
public void extraireFragment(DocumentXML document, FragmentXML fragment){
  XpathFactory fabrique = XPathFactory.newInstance();
  XPath xpath = fabrique.newXPath();
  XPathExpression expression = xpath.compile("/racine/element");
  synchronized(document){
    NodeList liste = (NodeList)expression.evaluate(
                                    document, 
                                    XPathConstants.NODESET);
    if(liste != null){
      synchronized(fragment){
        for(int i = 0; i < liste.getLength(); i++){
          fragment.appendChild(liste.item(i));
        }
      }
  }
}

Pour ce scénario, on imagine que les deux objets document et fragment sont communs à deux threads A et B et qu'ils accèdent simultanément et respectivement aux méthodes ajouterFragment() et extraireFragment.

  • Un thread A entre dans la méthode ajouterFragment().
  • Un thread B entre dans la méthode extraireFragment().
  • Le thread A acquiert le verrou sur l'objet fragment.
  • Le thread B acquiert le verrou sur l'objet document.
  • Le thread A tente d'acquérir le verrou sur l'objet document.
  • Le thread B tente d'acquérir le verrou sur l'objet fragment.
  • Le thread A se met en attente.
  • Le thread B se met en attente.
  • Le programme est bloqué.

Les interblocages représentent un risque inéluctable si lors de la conception d'une application, les accès concurrents n'ont pas été suffisamment étudiés. Outre le cas d'interblocage précité du à un problème entre un verrou et une condition, d'autres cas mettent en adéquation plusieurs threads, plusieurs verrous mutex et des circonstances délétères.