Proxy de synchronisation en Java 8


Contenu du snippet

Ci-dessous le code Java 8 d'une interface permettant de faire un proxy de synchronisation, c'est à dire d'empêcher l'exécution simultanée (par des threads en parallèles) de plusieurs méthodes utilisant la même ressource.

public interface Synchronizer {

    default Object getSynchronizer() {
        return this;
    }

    @FunctionalInterface
    interface WithResult<R> {
        R sync() throws RuntimeException;
    }

    default <R> R synchronize(WithResult<R> fun) throws RuntimeException {
        synchronized (getSynchronizer()) {
            return fun.sync();
        }
    }

    @FunctionalInterface
    interface WithoutResult {
        void sync() throws RuntimeException;
    }

    default void synchronize(WithoutResult fun) throws RuntimeException {
        synchronized (getSynchronizer()) {
            fun.sync();
        }
    }
}

La méthode getSynchronizer() permet de déterminer quel objet va servir de verrou, c'est à dire qu'un seul Thread en même temps pourra utiliser ce verrou (cela peut être un flux de fichier par exemple). Par défaut le verrou c'est this, c'est à dire qu'un appel à une méthode de la classe verrouillera toutes les autres méthodes pour les autres thread (une seule méthode peut être exécuté à la fois).

Les méthodes synchronize() servent de proxy, on leur passe en paramètre une méthode - avec ou sans résultat - qui sera exécuté dès que le verrou sera disponible tout en le verrouillant à son tour pour empêcher d'autres appels de méthodes.

Un exemple : soit l'interface suivante avec deux méthodes withResult et withoutResult.

public interface Example {
    public void withoutResult(int n);
    public String withResult(int n);
}

Le proxy de synchronisation pourra alors s'implémenter comme la classe suivante, avec une indirection sur une instance non synchronisée de l'interface Example.

public class SynchronizedExample implements Synchronizer, Example {
    private final Example example;

    public SynchronizedExample(Example example) {
        this.example = example;
    }

    @Override
    public void withoutResult(int n) {
        synchronize(() -> example.withoutResult(n));
    }

    @Override
    public String withResult(int n) {
        return synchronize(() -> example.withResult(n));
    }
}

Remarque : selon les besoins on pourra redéfinir le verrou comme le code suivant.

@Override
public Object getSynchronizer() {
    return example;
}

Attention : dans ce cas il faut être sûr que l'objet de synchronisation ne va pas être utilisé comme verrou par une autre classe, cela pourrait conduire à une famine.
Exemple : une méthode a verrouillé l'objet et appelle une autre méthode qui ne peut pas s'exécuter tant que l'objet n'est pas déverrouillé, ce qui bloque tout indéfiniment.

Fin de l'exemple pour tester, voici une implémentation non synchronisée de l'interface Example que l'on pourra utiliser pour faire l'indirection. Cela affiche juste un message au début de chaque méthode puis on attend 1 seconde avant d'afficher un message à la fin de la méthode.

public class BasicExample implements Example {

    private void sleep(String label) {
        try {
            System.out.println("Begin " + label);
            Thread.sleep(1_000);
        } catch (InterruptedException e) {
            System.err.println(e);
        } finally {
            System.out.println("Finish " + label);
        }
    }

    @Override
    public void withoutResult(int n) {
        sleep("withoutResult : " + n);
    }

    @Override
    public String withResult(int n) {
        sleep("withResult : " + n);
        return null;
    }
}

Test 1 : on appelle 4 threads en parallèles avec l'implémentation basique, non synchronisée.

public class Test1 {

    public static void main(String[] args) {
        Example example = new BasicExample();

        new Thread(() -> example.withoutResult(1)).start();
        new Thread(() -> example.withResult(2)).start();
        new Thread(() -> example.withoutResult(3)).start();
        new Thread(() -> example.withResult(4)).start();
    }
}

Résultat, on a les 4 "Begin" qui s'affichent plus ou moins en même temps, on attends 1 seconde, et on a les 4 "Finish" qui s'affichent plus ou moins en même temps.

Begin withoutResult : 1
Begin withResult : 2
Begin withResult : 4
Begin withoutResult : 3
Finish withoutResult : 1
Finish withResult : 2
Finish withResult : 4
Finish withoutResult : 3

Test 2 : c'est exactement le même code, sauf en ligne 4 où l'on définit une implémentation synchronisée.

public class Test2 {

    public static void main(String[] args) {
        Example example = new SynchronizedExample(new BasicExample());

        new Thread(() -> example.withoutResult(1)).start();
        new Thread(() -> example.withResult(2)).start();
        new Thread(() -> example.withoutResult(3)).start();
        new Thread(() -> example.withResult(4)).start();
    }
}

Résultat, on a une première méthode qui affiche son Begin, on attends 1 seconde, puis elle affiche son Finish, ce qui libère le verrou pour permettre à une deuxième méthode de faire pareil, puis la troisième et la quatrième.

Begin withResult : 2
Finish withResult : 2
Begin withResult : 4
Finish withResult : 4
Begin withoutResult : 1
Finish withoutResult : 1
Begin withoutResult : 3
Finish withoutResult : 3

Compatibilité : 1

A voir également

Vous n'êtes pas encore membre ?

inscrivez-vous, c'est gratuit et ça prend moins d'une minute !

Les membres obtiennent plus de réponses que les utilisateurs anonymes.

Le fait d'être membre vous permet d'avoir un suivi détaillé de vos demandes et codes sources.

Le fait d'être membre vous permet d'avoir des options supplémentaires.