1. Lock objects
1.1. L'interface Lock
La synchronisation telle qu'on l'a vue jusqu' à maintenant repose sur le concept d'exclusion mutuelle; cela veut dire qu'une portion de code précédée du mot "synchronized" est traitée par le thread qui détient le verrou et l'entrée/sortie de ce bloc de code est géré dans une seule méthode; cela peut poser d'énormes problèmes. Pour remédier à ces problèmes l'API java.util.concurrent propose des solutions, notamment l'interface Lock et ses implémentations.
Une instance de l'interface 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, à savoir celui qui possède le verrou. Si une variable est modifiée par le thread qui détient le verrou, cette modification est automatiquement, immédiatement vue par l'ensemble des autres threads qui ont une vue sur cette variable lorsque le thread responsable de la modification rend le verrou.
1.2. Les méthodes de l'interface Lock
L'interface Lock expose les méthodes suivantes:
- lock(): Cette méthode verrouille l'instance de Lock si c'est possible; c'est à dire qu'elle tente d'acquérir le verrou. Si cette instance de l'interface de Lock est déjà verrouillée, le thread appelant la méthode lock() est bloquée tant que le verrou n'est pas relâché.
- lockInterruptibility(): cette méthode verrouille l'instance de l'interface Lock jusqu'à ce que le thread courant soit mis dans l'état INTERRUPTED.
- tryLock() et tryLock(long time, TimeUnit unit): ces deux méthodes tentent d'acquérir le verrou; S'il est immédiatement disponible, alors ces deux méthodes retournent "true". Si le verrou n'est pas disponible, la méthode tryLock() retourne immédiatement "false", et la méthode tryLock(long time, TimeUnit unit) attend le temps indiqué en paramètre avant de retourner le résultat.
- unlock(): la méthode unlock() libère le 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 "finaly".
....
1.3. Exemple d'utilisation de l'interface lock
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.Random;
public class Safelock {
static class Friend {
private final String name;
private final Lock lock = new ReentrantLock();
public Friend(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
public boolean impendingBow(Friend bower) {
Boolean myLock = false;
Boolean yourLock = false;
try {
myLock = lock.tryLock();
yourLock = bower.lock.tryLock();
} finally {
if (! (myLock && yourLock)) {
if (myLock) {
lock.unlock();
}
if (yourLock) {
bower.lock.unlock();
}
}
}
return myLock && yourLock;
}
public void bow(Friend bower) {
if (impendingBow(bower)) {
try {
System.out.format("%s: %s has"
+ " bowed to me!%n",
this.name, bower.getName());
bower.bowBack(this);
} finally {
lock.unlock();
bower.lock.unlock();
}
} else {
System.out.format("%s: %s started"
+ " to bow to me, but saw that"
+ " I was already bowing to"
+ " him.%n",
this.name, bower.getName());
}
}
public void bowBack(Friend bower) {
System.out.format("%s: %s has" +
" bowed back to me!%n",
this.name, bower.getName());
}
}
static class BowLoop implements Runnable {
private Friend bower;
private Friend bowee;
public BowLoop(Friend bower, Friend bowee) {
this.bower = bower;
this.bowee = bowee;
}
public void run() {
Random random = new Random();
for (;;) {
try {
Thread.sleep(random.nextInt(10));
} catch (InterruptedException e) {}
bowee.bow(bower);
}
}
}
public static void main(String[] args) {
final Friend alphonse =
new Friend("Alphonse");
final Friend gaston =
new Friend("Gaston");
new Thread(new BowLoop(alphonse, gaston)).start();
new Thread(new BowLoop(gaston, alphonse)).start();
}
}
1.4 Résultat de l'exécution du programme:
...
2. Executor Framework
Executor Framework est un Framework java qui permet de gérer les threads au lancement, à leur exécution et leur terminaison. Ce Framework dispose de méthodes qui contrôlent les pools de threads, le nombre de threads et la taille de chaque thread. Au sein de ce Framework on a l'interface Executor, l'interface ExecutorService et la classe Executors; tous sont inclus dans l' API java.util.concurrent.
La classe Executors crée l'instance de la classe ExecutorService qui exécute les tâches à traiter. On peut surcharger (override) la classe Executors pour implémenter la méthode execute() .
La classe ExecuteService retourne une instance de la classe Future qui teste l'état de la tâche qui est en train d'être traitée.
2.1. L'interface Executor
L'interface Executor contient la méthode execute() qui permet de traiter les commandes:
Dans un nouveau thread, ou
Dans un pool de thread, ou
dans l'appel de thread
et ceci est relatif à la façon dont est implémentée l'interface Executor.
2.2. L'interface ExecutorService
L'interface ExecutorService dérive de l'interface Executor; ExecutoreService possède des fonctionnalités qui traitent les tâches en arrière plan; elle implémente aussi des fonctionnalités de gestion de pool de Threads, des méthodes qui traitent la fin des threads et une méthode qui peut produire une instance de la classe Future pour traquer la progression des différentes tâches au sein de d'un groupe de threads.
2.3 Les méthodes 'interface ExecutorService
- boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException
- Future submit(Callable task) .....
- Future submit(Runnable task, T result) .....
- Future<?> submit(Runnable task)
- void shutdown() ......
- List shutdownNow() ......
- boolean isTerminated() ......
- List<Future> invokeAll(Collection<? extends Callable> tasks) throws InterruptedException ......
2.4. Exemples d'utilisation de Executor et de ExecutorService
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; class MyRunnable implements Runnable { int taskNumber; MyRunnable(int taskNumber) { this.taskNumber = taskNumber; } @Override public void run() { System.out.println(Thread.currentThread().getName() + " executing task no " + taskNumber); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } public class ExecutorServiceTest { //nThreads number of threads will be created and started in executor. //here we will create 2 threads. private static int nThreads = 2; //nTasks number of tasks will be executed. //here we will execute 10 tasks. private static int nTasks = 10; public static void main(String[] args) { ExecutorService executor = Executors.newFixedThreadPool(nThreads); System.out.println("executor created with 2 threads."); System.out.println("2 threads in executor will be used for executing 10 tasks. " + "So, at a time only 2 tasks will be executed"); for (int i = 1; i <= nTasks; i++) { Runnable task = new MyRunnable(i); executor.execute(task); } /* * Initiates shutdown of executor, previously submitted tasks are * executed, but no new tasks will be accepted. */ executor.shutdown(); System.out.println("executor has been shutDown."); } }
Résultat de l'exécution du programme:
executor est créé avec 2 threads; ces deux threads seront utilisés pour réaliser dix tâches; à chaque fois uniquement deux seront exécutées.
pool-1-thread-1 executing task no 1
pool-1-thread-2 executing task no 2
executor has been shutDown.
pool-1-thread-1 executing task no 3
pool-1-thread-2 executing task no 4
pool-1-thread-1 executing task no 5
pool-1-thread-2 executing task no 6
pool-1-thread-1 executing task no 7
pool-1-thread-2 executing task no 8
pool-1-thread-2 executing task no 9
pool-1-thread-1 executing task no 10 */
...
Concurrent Collections
Rappel sur Collections:
Par définition une Collection en java est un groupe d'objets. Certaines familles de Collections peuvent gérer des doublons ou un ordre de tri par construction et d'autres non. La JDK ne fournit pas d'implémentations directes des Collections: par contre elle dispose d'implémentations par famille d'interfaces dérivant de l'interface Collections à savoir:
Set :c'est une collection d'éléments non ordonnés par défaut qui n'accepte pas les doublons
List : collection d'éléments ordonnés qui accepte les doublons
Map : collection sous la forme d'une association de paires clé/valeur
Queue, Dequeue : collection qui stocke des éléments dans un certain ordre
Collections synchronisées, concurrentes:
Le framework Collections dispose déjà de quelques solutions pour construire des collections thread-safe. Les classes Vector et Hashtable sont synchronisées; et la classe Collections fournit plusieurs méthodes pour créer des collections synchronisées, par enveloppement de ces collections et synchronisation des accès. Malheureusement ces classes ne sont pas suffisantes pour traiter de façon atomique des problèmes simples. Le package java.util.concurrent rajoute au framework Collections un certain nombre d'interfaces et de classes pour gérer efficacement la synchronisation et l'accès concurrent à des données au sein des objets d'une collection. Les fonctionnalités rajoutées au framework Collections sont définies dans les interfaces suivants:
BlockingQueue : cet interface définit une structure de données telle que la donnée entrée en premier dans la queue et celle à sortir en premier (first-in-first-out) de la queue ; cette structure arrête ou génère un time-out pour toute tentative d'ajout de données si la queue est pleine ou toute tentative de retrait de données si la queue est vide.
ConcurrentMap : c'est un sous interface de java.util.Map qui définit de très importante opérations atomiques. Ces opérations retire ou remplace une paire de clé/valeur seulement si si la clé est présente, ou ajoutent une paire de clé/valeur seulement si la clé est absente. rendre ces opérations atomiques permet d'éviter la synchronisation; l'implémentation standard de cet interface ConcurrentHashMap qui présente des analogies avec l'interface HashMap.
ConcurrentNavigableMap: c'est une interface qui dérive de ConcurrentMap. Une implémentation de cet interface est celle de ConcurrentSkipListMap qui est analogue à TreeMap.
Atomic variables:
Le package java.util.concurrent.atomic dispose de classes pour traiter des opérations atomiques; chaque classe dans ce package dispose des méthodes get et set qui se comportent comme des méthodes reads / writes sur des variables volatiles. Autrement dit, une méthode set() si elle est appelé à s’exécuter sur une variable donnée , elle doit tenir compte de la méthode get() précédemment réalisée sur ce même variable ( c'est ce qu'on appelle " happens-before relationship ")
...
ThreadLocalRandom
A partir de JDK7, pour des applications multi-threadées qui utilisent des nombres aléatoires, java.util.concurrent de nouvelles classes ThreadLocalRandom; l'utilisation de cette classes à la place de Math.random() améliore la performance. Tout ce qu'il y a faire est de faire appel à ThreadLocalRandom.current() et d'utiliser une de ses méthodes comme suit:
int r = ThreadLocalRandom.current().nextInt(4, 77);
......