On appelle bloc synchronisé, tout bloc portant une déclaration synchronized. On peut poser le mot-clé synchronized de trois façons. Pour chacune de ces façons, le bloc synchronisé fonctionne avec un paramètre de synchronisation.

  • On peut ajouter le mot-clé synchronized en tant que modificateur d'une méthode statique. Dans ce cas le paramètre de synchronisation est la classe qui possède ce bloc.

  • On peut l'ajouter comme modificateur d'une méthode non statique. Dans ce cas, le paramètre de synchronisation est l'instance de cette classe dans laquelle on se trouve. On peut accéder à cette instance par le mot-clé this.

  • On peut enfin créer un bloc commençant par le mot-clé synchronized. Dans ce cas, le paramètre de synchronisation doit être passé explicitement à l'ouverture de ce bloc. Ce peut être tout objet Java.


La paramètre du bloc synchronisé joue le rôle de garde pour ce bloc synchronisé. Cet objet possède une unique clé. Cette clé est nécessaire pour pouvoir entrer dans le bloc synchronisé, et exécuter les instructions qu'il contient.

Quand un thread obtient cette clé, il entre dans le bloc, et exécute le code qui s'y trouve. À la sortie du bloc, il rend la clé à l'objet qui garde le bloc.

Aucun thread ne peut entrer dans ce bloc s'il ne possède pas la clé qui permet d'y entrer.

Supposons qu'un thread se présente à l'entrée du bloc, alors qu'un premier est dedans, en train de l'exécuter. La clé n'étant pas disponible, cet autre thread se verra refuser l'accès au bloc. Il devra attendre que le premier thread rende la clé afin de pouvoir y entrer à son tour.

Tout objet Java possède une clé, il s'agit d'un mécanisme bas niveau, présent sur la classe Object. Lorsque le langage Java a été conçu, cette idée était considérée comme excellente, aujourd'hui elle est plutôt battue en brêche... Il n'empêche, cela explique que tout objet Java peut être utilisé pour garder un bloc d'instructions.

Ce mécanisme de synchronization permet de résoudre le problème de concurrence d'accès que nous avons exposé. Cela dit, il pose un problème de performance. Effectivement, si un grand nombre de threads sont en concurrence d'exécution, tout bloc synchronisé devient un goulet d'étranglement dans lequel les thread ne peuvent passer qu'un par un.

L'accès à l'état d'un objet, et la modification de cet état, est un des quelques problèmes cruciaux de la programmation concurrente. Jusqu'à la version 4, Java n'exposait pas beaucoup d'éléments pour traiter ces questions. Depuis la version 5, l'API s'est considérablement enrichie, comme nous allons le voir.

Une mauvaise gestion de la synchronisation entre blocs peut mener à une situation de blocage total d'une application, appelée deadlock .

Une deadlock intervient lorsqu'un premier thread T1 se trouve dans un bloc synchronisé B1, et est en attente à l'entrée d'un autre bloc synchronisé B2. Malheureusement, dans ce bloc B2, se trouve déjà un thread T2, en attente de pouvoir entrer dans B1.

Ces deux threads s'attendent mutuellement, et ne peuvent exécuter le moindre code. La situation est bloquée, la seule façon de la débloquer est d'interrompre un des deux threads .

Construisons un exemple de deadlock , afin d'illustrer la situation. Cet exemple fonctionnera avec deux classes : une classe Verre et une classe Carafe. Lorsque le verre se remplit, la carafe se vide, et réciproquement.


La classe Carafe ressemble beaucoup à la classe Verre.


Il ne reste plus qu'à faire fonctionner ce modèle pour observer le deadlock .


Lancer cette application peut fonctionner, les deadlocks ne sont pas des phénomènes qui se produisent systématiquement. Cela dit, dans ce cas, on doit l'observer au bout de quelques tentatives : l'application reste bloquée.

En mode débug, Eclipse nous permet de mettre en pause les threads d'une application un par un, afin de voir ce qu'ils font. Dans une situation de deadlock , il peut être très intéressant de le faire, et de regarder quels verrous chaque thread possède, et s'il est en attente, qui possède le verrou qu'il doit acquérir pour avancer.


Une fois Eclipse configuré, la mise en pause des bons threads va nous montrer une situation qui ressemblera à celle présentée sur cette figure.


Sur cette figure, on a ouvert les deux threads Thread-4 et Thread-7.

Le Thread-4 est en train d'exécuter la méthode Carafe.vide(), qui appelle Verre.isPlein(). Ce thread possède le verrou de l'objet Carafe dont l'ID est 19, et il attend celui de l'objet Verre dont l'ID est 28. Il ne peut pas entrer dans le corps de la méthode Verre.isPlein() tant qu'il n'a pas ce verrou.

Le Thread-7 est en train d'exécuter la méthode Verre.remplis() qui appelle Carafe.isPlein(). Il possède le verrou de l'objet Verre dont l'ID est 28. C'est ce verrou que le Thread-4 attend. Et il attend le verrou de l'objet Carafe dont l'ID est 29. Ce verrou est possédé par le Thread-4.

On remarquera qu'Eclipse a mis ces deux threads en rouge, car il a détecté la situation de deadlock .

Dans ce contexte, la seule façon de progresser est d'interrompre l'un des deux threads .

Éviter les situations de deadlocks n'est pas toujours une chose facile. Néanmoins, on peut tenter de respecter quelques principes, qui permettent de limiter les risques.

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