10. Classes de contrôle d'accès

10.1. Introduction

L'API Concurrent introduit enfin des verrous explicites, qui viennent enrichir les possibilités de ce que l'on pouvait faire jusqu'en Java 4. En fait, jusqu'à cette version de Java, du moins avec les éléments standard qui nous étaient fournis dans le JDK, on n'avait guère que le mot-clé synchronized pour synchroniser des blocs d'instruction. Les classes et interfaces que nous allons présenter dans cette partie ne sont pas toutes des verrous à proprement parler. Cela dit, elles permettent toutes de contrôler la concurrence d'accès à des blocs, c'est la raison pour laquelle nous les avons regroupées ici.

10.2. Interfaces Lock et ReadWriteLock

L'un des premiers reproches qui a été fait au modèle de synchronisation de Java, est que l'entrée et la sortie d'un bloc synchronisé ne peut se faire que dans la même méthode. L'interface Lock et ses implémentations sont là pour régler ce problème. Une instance de Lock est là pour contrôler l'accès à une variable partagée par plusieurs threads à la fois. Une variable contrôlée par une instance de Lock ne peut être modifiée que par un seul thread à la fois, celui qui possède ce verrou. Notons immédiatement que la gestion de la mémoire obéit aux même règles lorsque l'on entre dans un bloc gardé par une instance de Lock et que l'on en sort, que lorsque l'on entre et l'on sort d'un bloc synchronisé normal. Toutes les variables modifiées par le thread qui rend le verrou sont automatiquement et immédiatement vues par l'ensemble des autres threads qui ont une vue sur ces variables.

10.2.1. Interface Lock

L'interface Lock expose les méthodes suivantes.
  • lock() : tente d'acquérir ce verrou. Cette méthode rend la main quand le verrou a été acquis. Donc si un autre thread possède ce verrou, le thread courant attendra que cet autre thread le rende.
  • lockInterruptibility() : tente d'acquérir ce verrou, sauf si le thread courant est dans l'état INTERRUPTED.
  • tryLock() et tryLock(long time, TimeUnit unit) : tente d'acquérir ce verrou. S'il est immédiatement disponible, alors ces deux méthodes retournent true. S'il ne l'est pas, la première retourne immédiatement false, la seconde attend le temps indiqué en paramètre qu'il se libère.
  • unlock() : libère ce verrou. Il est indispensable que cette méthode soit appelée dans tous les cas. Il faut donc appeler cette méthode dans une clause finally.

10.2.2. Interface ReadWriteLock

Cette interface n'est pas une extension de Lock. Elle n'expose que deux méthodes :
  • readLock() : retourne un verrou en lecture.
  • writeLock() : retourne un verrou en écriture.
Ces deux verrous en lecture et en écriture sont des instances de Lock. Leur sémantique est un peu particulière. Une instance de ReadWriteLock peut fournir deux types de verrous : un verrou de lecture et un verrou d'écriture. Cette instance peut fournir autant de verrous de lecture que l'on veut, tant qu'aucun verrou d'écriture n'a été demandé. En revanche, un verrou d'écriture est exclusif : il ne peut y avoir qu'un seul thread qui puisse en demander un. Ce type de verrou est efficace dans les cas, nombreux, dans lesquels les écritures sont rares par rapport aux lectures. Les lectures peuvent se faire en concurrence d'accès sans problème, seules les écritures doivent être synchronisées.

10.3. Notion de verrou réentrant

Nous avons vu qu'un bloc synchronisé ne pouvait être exécuté que par un seul thread à la fois. Nous avons passé sous silence le cas particulier où, un thread possède un verrou, et tente d'entrer dans un bloc synchronisé dont le paramètre est le même objet que celui dont il a le verrou. Ce cas peut se présenter souvent, comme dans l'exemple suivant.

Exemple 85. Cas de deux blocs synchronisés sur le même objet

public  class Marin {

    public  synchronized vogue() {
       // je vogue !
   }
}

 public  class Capitaine  extends Marin {

    public  synchronized vogue() {
       // je vogue aussi !
       super.vogue() ;
   }
}

Dans cet exemple, appeler la méthode vogue() sur une instance de Capitaine appellera également la méthode vogue() de la classe Marin. Ces deux méthodes sont synchronisées sur le même objet, this. Que se passe-t-il alors ? Un thread possède un verrou, tente d'entrer dans un bloc synchronisé dont le verrou n'est pas disponible, mais il se trouve que c'est ce thread qui le possède. La synchronisation en Java est dite réentrante , cela signifie que dans ce cas, le thread qui possède le bon verrou, peut entrer dans le nouveau bloc synchronisé. Dans le cas des instances de Lock, ce point n'est pas spécifié, il est donc de la responsabilité de la classe d'implémentation de décider si elle est réentrante ou pas. Les implémentations fournies avec l'API standard le sont.

10.4. Classe RentrantLock

La classe RentrantLock est une implémentation de Lock, de caractère réentrant, comme son nom peut le laisser supposer. Elle expose quelques méthodes supplémentaires par rapport à l'interface Lock, très utile.
  • getHoldCount() : retourne le nombre de fois que la méthode lock() de ce verrou a été appelée, sans que sa méthode unlock() ne l'a été. Rappelons que pour tout appel à lock(), il doit y avoir un appel à unlock(), et que ce verrou ne sera ouvert que s'il y a eu autant d'appels pour ces deux méthodes.
  • getQueueLength() : retourne le nombre de threads en attente sur ce verrou.
  • hasQueuedThreads()et hasQueueThread(Thread t) : retourne true si des threads sont en attente sur ce verrou, ou si le thread passé en paramètre est dans cette file d'attente.
  • isHeldByCurrentThread() : retourne true si le thread courant est en attente sur ce verrou.
  • isLocked() : retourne true si ce verrou a été pris par un thread .
Voyons un exemple d'utilisation de cette classe.

Exemple 86. Utilisation d'un RentrantLock

public  class Marin {

    // création d'une instance de ReentrantLock
    private Lock lock =  new ReentrantLock() ;
   
    public  void vogue() {
   
       // acquisition du verrou
       // cette méthode ne renvoie la main que lorsque 
       // le verrou est disponible
      lock.lock() ;
       try {
          // traitement
      }  catch (...) {
          // le bloc catch est facultatif
      }  finally {
          // en revanche, la clause finally est obligatoire !
         lock.unlock() ;
      }
   }
}

10.5. Classe ReadWriteRentrantLock

La classe ReadWriteRentrantLock implémente la sémantique de l'interface ReadWriteLock. Elle expose quelques méthodes supplémentaires, analogues à celle de ReentrantLock, qui permettent d'examiner les threads en attente de verrous. Dans la mesure où les verrous de lecture sont différents du verrou d'écriture, le mécanisme de réentrance est légèrement différent. Les verrous de lecture sont réentrant sans problème, ce qui est naturel. Un thread qui possède un verrou de lecture ne peut pas obtenir de verrou d'écriture sur le même objet instance de ReadWriteRentrantLock. Il doit tout d'abord libérer son verrou de lecture. En revanche, un thread qui possède un verrou d'écriture peut acquérir un verrou de lecture sur le même objet, puis libérer son verrou d'écriture.
Java API avancées
Retour au blog Java le soir
Cours & Tutoriaux
Table des matières
API Collection
1. Introduction
2. Interface Collection
2.1. Notion de Collection
2.2. Détail des méthodes disponibles
2.3. Interface Iterator
2.4. Implémentation, exemples d'utilisation
3. Interface List
3.1. Notion de List
3.2. Détail des méthodes disponibles
3.3. Interface ListIterator
3.4. Implémentations, exemples d'utilisation
4. Interface Set
4.1. Notion de Set
4.2. Implémentations HashSet et LinkedHashSet
4.3. Exemples d'utilisation
5. Interface SortedSet
5.1. Notion de SortedSet
5.2. Détails des méthodes disponibles
5.3. Exemples d'utilisation
6. Interface NavigableSet
6.1. Notion de NavigableSet
6.2. Détails des méthodes disponibles
6.3. Exemple d'utilisation
7. Interfaces Queue et Deque
7.1. Notion de file d'attente
7.2. Détail des méthodes disponibles
7.3. Utilisation des interfaces Queue et Deque
8. Tables de hachage
8.1. Notion de table de hachage
8.2. Interface Map
8.3. Interface Map.Entry
8.4. Interface SortedMap
8.5. Interface NavigableMap
8.6. Implémentations
8.7. Exemples d'utilisation
9. Classes utilitaires Collections et Arrays
9.1. Introduction
9.2. Classe Arrays
9.3. Classe Collections
Génériques
1. Introduction
2. Un premier exemple
2.1. Une première classe générique
2.2. Une première méthode générique
3. Contraindre un type générique
3.1. Problème posé
3.2. Contraindre un type générique
4. Implémentation des génériques
4.1. Type erasure
4.2. Types génériques et casts
4.3. Type générique et exception
4.4. Construction d'une instance générique
4.5. Génériques et membres statiques
4.6. Collisions de méthodes génériques
4.7. Implémentation de plusieurs types identiques
5. Type <?>
5.1. Introduction
5.2. Type ? extension d'un type
5.3. Type ? super-type d'un type
Expressions régulières
1. Introduction
2. Mise en œuvre des expressions régulières
2.1. Fonctionnement d'une regexp
2.2. Fonctionnement de l'API en Java
2.3. Un premier exemple
2.4. Classe Pattern
2.5. Classe Matcher
2.6. Utilisation des méthode find() et group()
2.7. Méthodes de remplacement
2.8. Sélection de régions
3. Syntaxe des expressions régulières
3.1. Notion de classe
3.2. Étude d'un cas réel
3.3. Recherche d'un mot précis
3.4. Recherche de deux mots précis
3.5. Recherche d'un mot commençant par une lettre donnée
3.6. Cas de mots comportant des caractères accentués
3.7. Recherche sur les lignes
Introspection
1. Introduction
2. La classe Class
2.1. Utilisation de Class
2.2. Méthodes disponibles
2.3. Remarque sur la propriété Accessible
2.4. Type d'une classe
2.5. Création d'une instance à partir d'un objet Class
2.6. Cas des énumérations
3. Les classes Method et Constructor
3.1. Utilisation de Method
3.2. Utilisation de Constructor
3.3. Méthodes disponibles
3.4. Invocation d'une méthode par introspection
4. La classe Field
4.1. Utilisation de Field
4.2. Méthodes disponibles
4.3. Accès à un champ par introspection
5. La classe Modifier
Programmation concurrente
1. Introduction
2. Lançons nos premiers threads
2.1. Introduction
2.2. Un premier thread, extension de Thread
2.3. Un deuxième thread, implémentation de Runnable
2.4. Remarque sur la méthode Thread.sleep(long)
2.5. Arrêter un thread
3. Concurrence d'accès
3.1. Notion d'état
3.2. Exemple de concurrence d'accès sur un état
3.3. Analyse de la concurrence d'accès
3.4. Solution au problème
3.5. Champs volatile
4. Synchronisation
4.1. Définition d'un bloc synchronisé
4.2. Fonctionnement d'un bloc synchronisé
4.3. Notion de deadlock
4.4. Bonnes pratiques pour la synchronisation de threads
5. Opérations atomiques
5.1. Atomicité d'une opération
5.2. Solutions disponibles
5.3. Variables atomiques
6. Collections synchronisées et concurrentes
6.1. Introduction
6.2. Position du problème
6.3. Solutions proposées
7. Files d'attente
7.1. Introduction, pattern producteur / consommateur
7.2. Interface BlockingQueue<E>
7.3. Implémentations de BlockingQueue
7.4. Exemple de producteur / consommateur
7.5. Arrêter un producteur / consommateur : pilule empoisonnée
8. Classes utilitaires de l'API Concurrent
8.1. Introduction
8.2. Énumération TimeUnit
8.3. Interface Callable<V>
8.4. Interfaces Future<V> et RunnableFuture<V>
8.5. Interface ScheduledFuture<V> et RunnableScheduledFuture<V>
9. Pattern executor
9.1. Notion de réserve de threads
9.2. Interface Executor
9.3. Interface ExecutorService
9.4. Interface ScheduledExecutorService
9.5. Classe Executors
9.6. Pattern de lancement de tâches
10. Classes de contrôle d'accès
10.1. Introduction
10.2. Interfaces Lock et ReadWriteLock
10.3. Notion de verrou réentrant
10.4. Classe RentrantLock
10.5. Classe ReadWriteRentrantLock
11. Sémaphores, barrières et latches
11.1. Introduction
11.2. Notion de sémaphore, classe Semaphore
11.3. Notion de latch, classe CountDownLatch
11.4. Notion de barrière, classe CyclicBarrier