vendredi 30 octobre 2009

ThreadLocal

La classe ThreadLocal (java.lang.ThreadLocal) permet d'implanter un singleton par thread.


Il est possible d'utiliser des objets instances d'une classe non synchronisée dans un environnement multi-thread si on s'assure que chaque instance n'est utilisée que par un unique thread.

Par exemple, un objet Connection (java.sql.Connection) n'est pas thread-safe. Un même objet Connection ne doit donc pas être manipulé par différents threads. Mais si chaque thread possède son propre objet Connection, ils peuvent effectuer des requêtes simultanées de manière sûre. La classe ThreadLocal permet d'obtenir un unique objet (un singleton) par thread.

Un excellent article sur le sujet a été rédigé par Brian Goetz : Threading lightly, Part 3: Sometimes it's best not to share

Dans les exemples ci-dessous, on utilise un objet Counter pour numéroter des objets. La classe Counter est très simple et non synchronisée.

class Counter{
    private int next = 1;

    public int next(){
        return next++;
    }
}


Pour les besoins des exemples, on se dote d'une classe qui étend la classe Thread (java.lang.Thread). Elle permet d'afficher sur la sortie standard un certain nombre d'objets qu'elle instancie par introspection. Elle préfixe chaque trace par son nom.


class PrintNewObjectThread extends Thread{
    private Class aClass;
    private int nbPrint;

    public PrintNewObjectThread(String name, Class aClass, int nbPrint) {
        super(name);
        this.aClass = aClass;
        this.nbPrint = nbPrint;
    }

    @Override
    public void run() {
        for(int i = 0 ; i < nbPrint ; i++){
            try {
                System.out.println(getName() + ":" + aClass.newInstance());
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    }
}
Le premier exemple utilise un CountedObject, une classe non synchronisée. Cette classe encapsule un objet Counter de manière statique pour numéroter chaque nouvelle instance.
class CountedObject{
    private static final Counter COUNTER = new Counter();
    private int number = COUNTER.next();

    public String toString() {
        return "Object #" + number;
    }
}
Le second exemple utilise un CountedPerThreadObject, une classe non synchronisée. Cette classe est semblable à la classe CountedObject mais encapsule de manière statique un objet ThreadLocal. La classe interne qui étend ThreadLocal est chargée d'instancier un objet Counter par thread.
class CountedPerThreadObject{
    private static final ThreadLocal COUNTER = new ThreadLocal(){
        protected Counter initialValue() {
            return new Counter();
        }
    };

    private int number = COUNTER.get().next();

    public String toString() {
        return "Object #" + number;
    }
}
Le programme ThreadLocalExemple exécute les deux exemples. L'exemple 1 lance deux threads chargés d'afficher des objets CountedObject, l'exemple 2 lance deux threads chargés d'afficher des objets CountedPerThreadObject.
public class ThreadLocalExemple {
    public static void main(String[] args) throws Exception {
        exemple1();
        exemple2();
    }

    private static void exemple1() throws Exception{
        System.out.println("Exemple 1");
        System.out.println("=========");
        Thread t1 = new PrintNewObjectThread("t1", CountedObject.class, 5);
        Thread t2 = new PrintNewObjectThread("t2", CountedObject.class, 5);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println();
    }

    private static void exemple2() throws Exception{
        System.out.println("Exemple 2");
        System.out.println("=========");
        Thread t1 = new PrintNewObjectThread("t1", CountedPerThreadObject.class, 5);
        Thread t2 = new PrintNewObjectThread("t2", CountedPerThreadObject.class, 5);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println();
    }
}
Le résultat, l'exemple 1 affiche des objets numérotés indépendamment des threads qui les créent, l'exemple 2 affiche des objets numérotés par thread.
Exemple 1
=========
t1:Object #1
t1:Object #2
t1:Object #3
t1:Object #4
t1:Object #5
t2:Object #6
t2:Object #7
t2:Object #8
t2:Object #9
t2:Object #10

Exemple 2
=========
t1:Object #1
t1:Object #2
t1:Object #3
t1:Object #4
t1:Object #5
t2:Object #1
t2:Object #2
t2:Object #3
t2:Object #4
t2:Object #5
OK, on observe bien l'effet singleton par thread lorsque l'on introduit un objet LocalThread. Mais les deux exemples sont-ils pour autant thread-safe ?
Les threads qui sont exécutés affichent des objets (CountedObject dans l'exemple 1 et CountedPerThreadObject dans l'exemple 2) qu'ils ne partagent pas, mais ces objets modifient un membre privé et statique dans leur constructeur. Dans les deux exemples, l'objet modifié est de type Counter.
L'exemple 2 utilise un ThreadLocal qui garanti un objet Counter par thread, chaque objet Counter ne sera jamais accédé en concurrence. L'exemple 2 est donc thread-safe.
Dans l'exemple 1, les threads partagent un unique objet Counter et le modifient en parallèle un créant des objets CountedObject. L'objet Counter voit sont état modifié chaque fois qu'un objet CountedObject invoque sa méthode next() durant sa création. L'instruction next++ exécutée par la méthode next() n'est pas atomique, elle correspond à deux instructions atomiques : l'addition de next avec l'incrément 1 et l'affectation du résultat de l'addition à next. L'exemple 1 n'est donc pas thread-safe.
Pour que l'exemple 1 soit thread-safe, la méthode next() de la classe Counter doit être synchrosisée.
class Counter{
    private int next = 1;

    public synchronized int next(){
        return next++;
    }
}
Attention, "la classe LocalThread permet d'obtenir un objet singleton par thread" ne signifie pas que l'objet est lui-même un singleton. Au contraire, cela ruinerait l'avantage de l'utilisation d'un ThreadLocal. Imaginons la classe Counter rédigée de la manière suivante.
class Counter{
    private static final Counter singleton = new Counter();
    private int next = 1;

    public static Counter getInstance() {
        return singleton;
    }

    private Counter() {
    }

    public synchronized int next(){
        return next++;
    }
}
La classe CountedObject ou la classe interne ThreadLocal de CountedPerThreadObject ne pourraient obtenir que l'unique singleton Counter via la méthode statique getInstance(). Voici alors la trace que nous obtiendrions.
Exemple 1
=========
t1:Object #1
t1:Object #3
t1:Object #4
t1:Object #5
t1:Object #6
t2:Object #2
t2:Object #7
t2:Object #8
t2:Object #9
t2:Object #10

Exemple 2
=========
t1:Object #11
t1:Object #13
t1:Object #14
t1:Object #15
t1:Object #16
t2:Object #12
t2:Object #17
t2:Object #18
t2:Object #19
t2:Object #20
Dans ce cas de figure, tous les objets partagent le même objet Counter, qu'ils soient de type CountedObject ou CountedPerThreadObject. L'objet LocalThread de la classe CountedPerThreadObject gère une référence par thread pointant sur le même objet Counter.
Liens externes :

Aucun commentaire:

Enregistrer un commentaire