Image Principale

Comprendre les threads en Java


Dans cet article, nous découvrirons rapidement comment fonctionnent les threads en Java. Ceux-ci vous permettent d'exécuter du code asynchrone, non bloquant et donc de gagner en performance dans votre application.

Contact Person Mathieu Marteau
il y a 1 semaine

Prenons en exemple simple de code Java:

public class MyThread {

    protected String name;

    public MyThread (String name) {
        this.name = name;
    }

    public void run () {
        for (int i = 0; i < 10; i++) {
            System.out.println(this.getName());
        }
    }

    public String getName () {
        return this.name;
    }
}

Nous avons ici une classe Java nommée MyThread. Son constructeur prend en paramètre une String qui sera placé dans un paramètre name de la classe. La classe possède également une méthode run() qui va afficher 10 fois le name entré en paramètre lors de la création de la classe.

Si maintenant on décide de créer un main() dans lequel on créé deux instances de cette classe, voici ce que cela va donner:

public class Main {

    public static void main(String[] args) {

        MyThread myThreadA = new MyThread("A");
        myThreadA.run();

        MyThread myThreadB = new MyThread("B");
        myThreadB.run();
    }
}

Ce code affichera donc le résultat suivant:

A
A
A
A
A
A
A
A
A
B
B
B
B
B
B
B
B
B
B

Comme vous le voyez, la console affiche 10 fois la lettre A et 10 fois la lettre B à la suite. L'exécution des deux méthodes run() se fait donc de façon séquentielle.

Imaginons maintenant que l'on souhaite que ces deux fonctions soient lancées en même temps, on aurait donc un entrelacement aléatoire de nos lettres, puisque l'on lance en même temps les deux fonctions.

Pour cela, il nous faut utiliser des threads. Et pour utiliser ces threads, il faut faire hériter notre classe de la classe Thread.

En héritant notre classe, on va d'ailleurs pouvoir supprimer notre paramètre String et notre méthode getName() qui sont déjà présents dans la classe de base:

public class MyThread extends Thread {

    public MyThread (String name) {
        super(name);
    }

    public void run () {
        for (int i = 0; i < 10; i++) {
            System.out.println(this.getName());
        }
    }
}

Lorsque vous allez relancer votre code, vous ne devriez pas encore voir de changement. En effet, en appelant directement la méthode run(), rien d'extraordinaire ne va se passer et la méthode va s'exécuter classiquement.

En revanche, en utilisant la méthode start() de Thread, l'exécution des codes va effectivement se dérouler en parallèle:

public class Main {

    public static void main(String[] args) {

        MyThread myThreadA = new MyThread("A");
        myThreadA.start();

        MyThread myThreadB = new MyThread("B");
        myThreadB.start();
    }
}

Vous ne verrez pas forcément l'effet que peut amener ce changement. Pour être sûr de mieux le voir, on va rajouter un peu de délai aléatoire dans notre classe:

import java.util.Random;

public class MyThread extends Thread {

    public MyThread (String name) {
        super(name);
    }

    public void run () {
        for (int i = 0; i < 10; i++) {
            try {
                Thread.sleep((new Random()).nextInt(1000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(this.getName());
        }
    }
}

Comme vous le voyez, on va faire "patenter" notre fonction un nombre aléatoire de millisecondes. Mais pendant que notre thread "attend", l'autre thread continue de tourner etc... On a donc par exemple, la sortie suivante:

B
A
B
B
B
A
A
B
B
B
A
B
B
A
B
A
A
A
A
A

Vous ne pouvez plus savoir avec certitude quand est-ce que le thread va effectuer une action, tout ce que vous savez, c'est qu'il va l'effectuer!

Il existe d'autres manières de créer des threads. Vous pouvez par exemple, créer un thread directement dans votre code de la façon suivante:

public class Main {

    public static void main(String[] args) {

        System.out.println("Je vais rentrer dans un thread");

        new Thread(() -> {
            System.out.println("Je suis dans le thread");
        }).start();

        System.out.println("Je ne suis plus dans le thread");
    }
}

Ce code va afficher la sortie suivante:

Je vais rentrer dans un thread
Je ne suis plus dans le thread
Je suis dans le thread

Comme vous le voyez, Je suis dans le thread apparaît dans la console en dernier parce qu'il ne fait pas partie de la même pile d'exécution.

Attardons-nous une seconde sur la notation:

new Thread(() -> {
    System.out.println("Je suis dans le thread");
}).start();

On utilise ici une expression lambda () -> doSomething().

Une autre manière de l'écrire aurait été d'instancier un objet de type Runnable:

new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("Je suis dans le runnable");
    }
}).start();

Même si la notation est plus lourde, cette solution permet d'utiliser le thread de cette manière peu importe la version de Java utilisée. Et permet éventuellement de mieux comprendre le mécanisme utilisé par les threads.

Un cas d'utilisation

Avec notre exemple, il est assez difficile de comprendre pourquoi nous aurions besoin d'utiliser cette fonctionnalité. Prenons un autre exemple de la vie réelle et imaginons la chose suivante.

Dans notre application, lorsqu'un nouvel article est créé, on souhaite l'indexer dans une base de données ElasticSearch et on souhaite également envoyer un mail à l'auteur de l'article. Toutefois, on souhaite également que notre application renvoie une réponse le plus rapidement possible, peu importe si il a bien été indexé par ElasticSearch et peu importe si le mail a bien été envoyé à l'auteur. Le plus important pour nous, c'est de savoir que l'article a bien été créé.

Commençons donc avec la structure basique de notre application. Nous aurons à notre disposition une classe Article ainsi qu'une classe User:

public class User {
    public String pseudo;

    public User (String pseudo) {
        this.pseudo = pseudo;
    }
}
public class Article {
    public String title;
    public String content;
    public User user;

    public Article (String title, String content, User user) {
        this.title = title;
        this.content = content;
        this.user = user;
    }
}

Et voici la base de notre main():

public class Main {

    public static void main(String[] args) {
        User user = new User("zizou94");
        Article article = new Article("Mon titre", "Mon contenu", user);
        System.out.println("L'article " + article.title);
    }
}

Dans ce main, on va ajouter quelques lignes afin que notre console affiche le temps d'exécution de notre code:

public class Main {

    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();

        User user = new User("zizou94");
        Article article = new Article("Mon titre", "Mon contenu", user);
        System.out.println("L'article " + article.title);

        long endTime   = System.currentTimeMillis();
        long totalTime = endTime - startTime;
        System.out.println(totalTime);
    }
}

Ce code aura la sortie suivante:

L'article Mon titre
1

Ce code n'a mis qu'une milliseconde à s'exécuter.

Ajoutons maintenant une méthode à notre Article qui va permettre de l'indexer sur ElasticSearch, ainsi qu'une méthode à User qui va permettre de lui envoyer un email. On va également ajouter un temps de latence de une seconde en utilisant la fonction wait() afin de simuler une action qui prendrait une seconde à se faire:

public class Article {
    public String title;
    public String content;
    public User user;

    public Article (String title, String content, User user) {
        this.title = title;
        this.content = content;
        this.user = user;
    }

    public void index () {
        try {
            wait(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Indexation de l'article.");
    }
}
public class User {
    public String pseudo;

    public User (String pseudo) {
        this.pseudo = pseudo;
    }

    public void sendMail () {
        try {
            wait(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Le mail pour " + this.pseudo + " est bien parti");
    }
}

Et ajoutons maintenant l'utilisation de ces méthodes dans notre main:

public class Main {

    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();

        User user = new User("zizou94");
        Article article = new Article("Mon titre", "Mon contenu", user);

        article.index();
        user.sendMail();

        System.out.println("L'article " + article.title);

        long endTime   = System.currentTimeMillis();
        long totalTime = endTime - startTime;
        System.out.println(totalTime);
    }
}

Comme vous le voyez, notre programme a mis plus de deux secondes à nous afficher le temps d'exécution parce qu'il a été retardé par l'indexation et l'envoi de l'email.

Utilisons maintenant des threads:

public class Main {

    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();

        User user = new User("zizou94");
        Article article = new Article("Mon titre", "Mon contenu", user);

        new Thread(() -> {
            article.index();
        }).start();

        new Thread(() -> {
            user.sendMail();
        }).start();

        System.out.println("L'article " + article.title);

        long endTime   = System.currentTimeMillis();
        long totalTime = endTime - startTime;
        System.out.println(totalTime);
    }
}

Nous retrouverons ici le résultat suivant:

L'article Mon titre
44
Indexation de l'article.
Le mail pour zizou94 est bien parti

Comme vous le voyez, j'obtiens maintenant un temps d'exécution de 44ms, ce qui est bien inférieur aux plus de 2000ms que l'on obtenait tout à l'heure. On a donc résolu notre problème de performances. Ici, l'indexation de l'article ainsi que l'envoie de l'email se feront simultanément.

BONUS:

Le dernier code peut également s'écrire de la manière suivante:

public class Main {

    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();

        User user = new User("zizou94");
        Article article = new Article("Mon titre", "Mon contenu", user);

        new Thread(article::index).start();

        new Thread(user::sendMail).start();

        System.out.println("L'article " + article.title);

        long endTime   = System.currentTimeMillis();
        long totalTime = endTime - startTime;
        System.out.println(totalTime);
    }
}

Remarquez new Thread(article::index).start();.

  Tags

   java