Aller au contenu

Sémaphore

Un sémaphore est un objet spécial qui aide à la synchronisation des threads et est un complément au mot clé synchronized. Les sémaphores sont des permis d'exécutions. Seule une quantité de permis sera disponible et le thread devra attendre qu'un permis soit libéré avant de continuer.

On peut voir un parallèle avec les ExecutionService, où N threads pouvaient s'exécuter simultanément. Avec un sémaphore, c'est le même principe, mais avec un bout de code.

Pour utiliser un sémaphore, on doit commencer par le créer.

// Création d'un sémaphore
Semaphore semaphore = new Semaphore(3);

Par la suite, on pourra obtenir un permis et les retourner :

// Obtenir un permis - le code attendra si aucun n'est disponible
semaphore.acquire();

// Exécuter du code
Thread.sleep(5000);

// Retourner le permis
semaphore.release();

Exemple complet utilisant un sémaphore

import java.util.concurrent.Semaphore;

public class App {

    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(2);

        // Créer les philosophes
        Philosophe p1 = new Philosophe("Socrate", semaphore);
        Philosophe p2 = new Philosophe("Platon", semaphore);
        Philosophe p3 = new Philosophe("Aristote", semaphore);
        Philosophe p4 = new Philosophe("Sartre", semaphore);
        Philosophe p5 = new Philosophe("Descartes", semaphore);

        // Démarrer les threads
        (new Thread(p1)).start();
        (new Thread(p2)).start();
        (new Thread(p3)).start();
        (new Thread(p4)).start();
        (new Thread(p5)).start();
    }

}
import java.util.Random;
import java.util.concurrent.Semaphore;

public class Philosophe implements Runnable {

    // Le nom du philosophe
    private String nom;

    // Le sémaphore commun
    private Semaphore semaphore;

    // Créer le philosophe
    public Philosophe(String nom, Semaphore semaphore) {
        this.nom = nom;
        this.semaphore = semaphore;
    }

    // Faire parler le philosophe
    public void parler() throws InterruptedException {
        System.out.printf("%s veut parler.\n", this.nom);
        this.semaphore.acquire();
        System.out.printf("%s parle.\n", this.nom);
        Thread.sleep(new Random().nextInt(1000, 4000));
        System.out.printf("%s a terminé de parler.\n", this.nom);
        this.semaphore.release();
    }

    // Lancer le thread
    @Override
    public void run() {
        System.out.printf("%s entre dans le salon.\n", this.nom);
        try {
            this.parler();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}
Socrate entre dans le salon.
Socrate veut parler.
Platon entre dans le salon.
Platon veut parler.
Platon parle.
Aristote entre dans le salon.
Aristote veut parler.
Sartre entre dans le salon.
Sartre veut parler.
Descartes entre dans le salon.
Descartes veut parler.
Socrate parle.
Platon a terminé de parler.
Aristote parle.
Aristote a terminé de parler.
Sartre parle.
Socrate a terminé de parler.
Descartes parle.
Sartre a terminé de parler.
Descartes a terminé de parler.
  • App.java ligne 6: Le sémaphore de 2 peut représenter deux bâtons de paroles. Seulement deux personnes à la fois pourront parler.
  • Philosophe.java ligne 31: Lorsque le thread est lancé, le philosophe indique qu'il arrive dans le salon.
  • Philosophe.java ligne 20: Le philosophe désire prendre la parole. Pour se faire, il aura besoin d'obtenir un permis, ce qu'il tente d'obtenir à la ligne 21.
  • Philosophe.java lignes 22-24: Le philosophe parle pour une durée de 1 à 4 secondes.
  • Philosophe.java ligne 25: Le philosophe a terminé de parler et retourne son bâton de parole. Un autre philosophe pourra alors le prendre.
  • Console: En regardant de près, on peut voir que tous les philosophes entrent dans le salon et veulent parler. Cependant, seulement 2 peuvent parler en même temps.

Quelques méthodes des sémaphores

Méthode Retour Description
availablePermits() int Retourne la quantité de permis disponibles.
acquire() void Tente d'obtenir un permis. Bloque l'exécution du code.
tryAcquire() boolean Tente d'obtenir un permis et retourne le succès. Ne bloque pas l'exécution du code.
release() void Rend un permis disponible à nouveau.
getQueueLength() int Retourne le nombre de threads en attente d'un permis.

Mises en garde

Quand on joue avec des locks, il faut faire attention à ne pas causer d'attente infinie, c'est à dire qu'un thread attend après une ressource qui ne sera jamais disponible. Le thread ne pourra donc jamais continuer. Imaginons le thread suivant :

@Override
public void run() {
    try {
        System.out.println("Premier acquire");
        this.semaphore.acquire();
        Thread.sleep(1000);
        System.out.println("Second acquire");
        this.semaphore.acquire();
        Thread.sleep(1000);
        System.out.println("Premier release");
        this.semaphore.release();
        System.out.println("Second release");
        this.semaphore.release();
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
}

et le code d'application suivant :

Semaphore semaphore = new Semaphore(1);
Thread thread = new Thread(new ThreadDeadlock(semaphore));
System.out.println("Démarrage du thread");
thread.start();
thread.join();
System.out.println("Fin de l'application");

Le sémaphore ne possède qu'un seul permis, mais on tente d'en obtenir deux dans le thread. C'est un état impossible, le thread devra donc attendre indéfiniment.

Un second exemple, celui-ci à plusieurs threads, peut subvenir chacun des threads attend après l'autre thread pour libérer sa ressource. Par exemple :

@Override
public void run() {
    try {
        // Afficher un message
        this.writer.println("Bienvenue!");

        // Obtenir un permis
        this.semaphore.acquire();

        // Demander un nombre à doubler
        this.writer.print("Veuillez entrer un nombre: ");
        this.writer.flush();
        int nombre = Integer.parseInt(this.reader.readLine());
        int nombreDouble = nombre * 2;

        // Le travail est terminé, retourner le permis
        this.semaphore.release();

        // Afficher le résultat
        this.writer.printf("%d x 2 = %d", nombre, nombreDouble);

        // Fermer la connexion
        this.socket.close();
    } catch (InterruptedException | IOException e) {
        throw new RuntimeException(e);
    }
}
  • Un cilent se connecte via un socket
  • Sur le serveur, un permis de sémaphore est utilisé pour traiter le client
  • Le travail est effectué
  • Le client se déconnecte et le serveur libère le permis

Tout semble bien aller. Cependant, si une erreur arrive (ex. du texte est entré au lieu d'un nombre), le permis ne sera jamais retourné. Une fois tous les permis du sémaphore épuisé, aucune connexion ne sera plus possible.

Les deadlocks

Un deadlock est un autre exemple d'attente infinie. Celui-ci est causé quand plusieurs threads différents peuvent verrouiller plusieurs permis (locks). Une situation pourrait arriver où un thread attend après un lock pour continuer, mais que la libération de ce lock dépend de la fin du thread. Le premier attend après l'autre et l'autre attend après le premier.