synchronized : petite question sur le verrou

Le problème exposé dans ce sujet a été résolu.

Bonjour à tous,

Le mot-réservé synchronized permet de définir une section critique (ie. : un seul thread peut y accéder à un instant t, les autres attendront leur tour) en se basant sur une clé (synonyme "verrou") : un thread voulant exécuter la section critique prendra cette clé, les autres essaieront mais verront que la clé a déjà été prise et devront donc attendre qu'il l'a rende disponible.

Ce verrou est un objet et est parfois implicitement indiqué, parfois explicitement indiqué (le cas échéant, il faut l'écrire entre parenthèse : synchronised(mon_verrou). Tout objet Java est un verrou.

Ma question est : puisque tout objet Java est un verrou, peut-on mettre n'importe quoi comme verrou dans : synchronized( mon_verrou ) (ie. : dans une définition explicite du verrou de la section critique) ? Imaginons par exemple que je veuille rendre synchronisé un ajout d'int dans une ArrayList. Je mets quoi comme verrou ? Tout ce que je veux ? Genre l'ArrayList elle-même, ou encore un String, ou pourquoi pas la classe courante : bref vraiment tout ce que je veux ?

Ou au contraire, y a-t-il des critères pour choisir le verrou ?

Merci d'avance.

+0 -0

Tu peux mettre l'objet que tu veux, a condition que ça apporte les garanties que tu cherche a obtenir (si par exemple tu utilise plusieurs verrous différents pour protéger la même ressource, ça ne va pas le faire). Mais en général :

  • Si le lock doit être général a une méthode, c'est l'objet courant (this) qui est utilisé implicitement lorsque tu déclare une méthode synchronized.
  • Pour une méthode statique, c'est la classe elle même qui est utilisée.
  • Lorsque tu as besoin d'une granularité plus fine, il est possible de déclarer de nouveaux objets qui serviront de verrous (exemple ici).

Dans ton cas, le ArrayList en question peut être une bonne idée, puisque c'est justement l'objet que tu veux protéger.

Mais je suis curieux de savoir ce que tu cherches exactement a faire. Il existe des structures de données synchronisées en Java qui pourraient éventuellement t’être utiles.

+0 -0

"si par exemple tu utilise plusieurs verrous différents pour protéger la même ressource, ça ne va pas le faire" : pourquoi ? est-ce que tu aurais un exemple en tête ?

"Mais je suis curieux de savoir ce que tu cherches exactement a faire. Il existe des structures de données synchronisées en Java qui pourraient éventuellement t’être utiles." J'essaie juste de comprendre le fonctionnement de synchronized en fait, tous les tutos que j'ai trouvés sur Internet parlent de verrous, de restriction d'accès (= de section critique) mais je ne comprends pas vraiment la philosophie de ce mot-clé…

Avant tout, il faut comprendre le fonctionnement d'un verrou : c'est un objet qui une fois acquis par un thread, ne peut plus être acquis par un autre thread avant que le premier thread ne l'ait libéré. Il faut aussi savoir que chaque objet en java posséde un verrou qui lui est propre.

La synchronisation en java fonctionne de cette façon : un bloc synchronized constitue une section critique, que l'on veut protéger pour ne pas que deux threads puissent y accéder en même temps. Lorsque tu entre dans un tel bloc, le verrou associé a l'objet est acquis, et ensuite plus aucun thread ne peut prendre le verrou en question tant que tu n'es pas sorti du bloc (ce qui a pour effet de libérer le verrou).

Maintenant supposons que tu as plusieurs threads qui se partagent le même objet, par exemple un certain ArrayList qu'il ne faut pas accéder en concurrence. Si tu utilise un objet local au thread pour faire la synchronisation sur l’accès a cette ressource, ça ne servira a rien, puisque les threads ne vont pas utiliser le même verrou, et vont donc pouvoir accéder en même temps a ta section critique.

Oui tu peux mettre l'objet que tu veux, mais comme Yosh l'a déjà dit, dans la majorité des cas on se contente de this pour les méthodes d'instance et this.class pour les méthodes statiques.

En interne, les blocs synchronisés utilisent les méthodes wait et notify. Wait endort un thread, et notify réveille un des threads endormis par un appel à wait au hasard s'il y en a un (évidemment pour les méthodes wait et notify appelées sur le même objet, pas des objets différents).

Pour la faire courte :

1
2
3
public synchronized void methode () {
/* le code protégé par la section critique */
}

équivaut à :

1
2
3
4
5
public void methode () {
synchronized(this){
/* le code protégé par la section critique */
}
}

et :

1
2
3
4
5
public class MaClasse {
public static synchronized void methodeStatique () {
/* le code protégé par la section critique */
}
}

revient à :

1
2
3
4
5
6
7
public class MaClasse {
public static void methodeStatique () {
Synchronized(MaClasse.class){
/* le code protégé par la section critique */
}
}
}

Si on décortique plus en profondeur, ceci :

1
2
3
synchronized(object){
/* le code protégé par la section critique */
}

équivaut grosso modo à :

1
2
3
while (verrou utilisé) object.wait();
/* le code protégé par la section critique */
object.notify();

où l'expression verrou utilisé est elle-même protégée contre les modifications concurrentes d'une façon bien précise. Si ça t'intéresse de connaître ça dans les détails, tu pourras sûrement trouver des cours au sujet de la programmation concurrente et du pattern utilisé par Java, qui s'appelle moniteur (monitor). Mais avant de comprendre les moniteurs, il faut d'abord déjà comprendre la base, les verrous simples et les sémaphores par exemple.

Dans la programmation de tous les jours on n'a pas besoin de savoir comment ça fonctionne dans les détails, il faut juste se souvenir qu'un thread qui arrive dans une section critique en cours d'utilisation attend jusqu'à ce qu'elle soit libérée (= qu'il n'y ait plus personne dedans).

La métaphore est peut-être un peu douteuse mais imagine que le code de la section critique soit celui qui te permet de poser une pêche… si la porte des WC est verrouillée quand tu arrives devant, tu attends que celui qui est dedans en sorte. Et une fois qu'il est parti, tu entres et tu verrouilles la porte à ton tour.

Pour les cas complexes, n'hésite pas à faire un tour du côté du package java.util.concurrent.locks dans la doc. Là-dedans il y a des éléments classiques de la programmation concurrente avec des verrous, éléments qu'on retrouve dans plusieurs autres langages de programmation et librairies. LE verrou le plus simple s'appelle ReentrantLock, les autres sont des constructions beaucoup plus élaborées qu'on utilise dans des cas bien spécifiques, dont certaines que j'ai vues en cours, mais que je n'ai jamais utilisées dans un projet concret qui va plus loin qu'un toy project.

+0 -0

Pour les cas complexes, n'hésite pas à faire un tour du côté du package java.util.concurrent.locks dans la doc. Là-dedans il y a des éléments classiques de la programmation concurrente avec des verrous, éléments qu'on retrouve dans plusieurs autres langages de programmation et librairies. LE verrou le plus simple s'appelle ReentrantLock, les autres sont des constructions beaucoup plus élaborées qu'on utilise dans des cas bien spécifiques, dont certaines que j'ai vues en cours, mais que je n'ai jamais utilisées dans un projet concret qui va plus loin qu'un toy project.

QuentinC

A mon avis, la seule construction réellement intéressante de java.util.concurrent.locks pour tous les jours, en dehors de ReentrantLock, ce sont les objets Condition. Ils permettent de retrouver la véritable puissance des moniteurs de Hoare avec une gestion beaucoup plus fine de la synchronisation (on peut avoir plusieurs conditions distinctes sur le même verrou), ce que les (pseudos) moniteurs natifs de Java ne permettent pas. (exemple d'utilisation ici).

En revanche, dans java.util.concurrent on trouve des tas de mécanismes et structures de plus haut niveau qui méritent le détour, et qui suffisent généralement à s'affranchir de la plupart des problèmes de synchronisation courants.

Lu'!

Le locking c'est une technique de synchronisation. Mais elle est plutôt coûteuse et pernicieuse à appliquer correctement. Notamment parce qu'il faut bien avoir compris que ce genre de mécanique, comme à peu près tous les types de synchronisation hors des modèles haut niveau comme BSP, ne doit JAMAIS se trouver dans une section de code de traitement.

Pourquoi est-ce que je dis ça ? Pour rebondir sur ce qu'a déjà dit @yoch :

En revanche, dans java.util.concurrent on trouve des tas de mécanismes et structures de plus haut niveau qui méritent le détour, et qui suffisent généralement à s'affranchir de la plupart des problèmes de synchronisation courants.

yoch

TL;DR : Jamais de synchronisation dans le code métier, utilisation de structures de données thread-safe pour permettre le dialogue. Structures de données qui dans 95% des cas n'ont pas besoin d'être implémentées car existantes.

Les primitives de synchronisation bas niveau type sémaphores, verrous et opération atomiques bas niveau, c'est pour implémenter ce genre de structures de données pas pour synchroniser des threads (bordel).

Le moyen le plus clean d'implémenter un programme concurrent c'est d'abord de garantir la séparation spatiale des données entre les threads et de placer des canaux de communications thread-safe entre eux (donc en l'occurrence, les structures de données mentionnées par @yoch).

Et si des données doivent absolument être partagée entre les threads, on place une structure de données gardienne qui conserve tout ça et ne donne jamais d'accès complet aux données stockées et qui, en cas d'accès ponctuel, garantit l'exclusivité. Deux options : soit avoir une structure de données qui nous fournit un point temporaire pour accéder une donnée pendant un temps (pas trivial pour avoir une garantie par typage en Java), ou encore demander l'exécution d'une certain méthode en la donnant à la SDD qui se charge de l'exécution (encore une fois en garantissant l'exclusivité).

Maintenant supposons que tu as plusieurs threads qui se partagent le même objet, par exemple un certain ArrayList qu'il ne faut pas accéder en concurrence. Si tu utilise un objet local au thread pour faire la synchronisation sur l’accès a cette ressource, ça ne servira a rien, puisque les threads ne vont pas utiliser le même verrou, et vont donc pouvoir accéder en même temps a ta section critique.

yoch

Qu'est-ce que tu appelles "ressource locale à un thread" ? Je sais que les processus UNIX créés par fork possèdent exactement le même code et donc, à leur création, les mêmes variables ; ces dernières ne sont toutefois pas partagées entre processus, on peut donc dire qu'elles sont locales.

Mais dans le cas des threads Java ?

Oui tu peux mettre l'objet que tu veux, mais comme Yosh l'a déjà dit, dans la majorité des cas on se contente de this pour les méthodes d'instance et this.class pour les méthodes statiques.

QuentinC

Pourquoi peut-on utiliser un objet this alors qu'a priori, un thread A n'aura pas le même objet this que son frère le thread B ? On m'a dit précédemment que le verrou doit être le même objet.

Quand à UneClasse.class, je ne savais pas que c'était un objet, je vais voir ça :o

Sinon merci beaucoup mais je n'en suis pas encore à utiliser les classes fournies par java.util.concurrent, j'essaie déjà de comprendre la base ^^

Qu'est-ce que tu appelles "ressource locale à un thread" ? Je sais que les processus UNIX créés par fork possèdent exactement le même code et donc, à leur création, les mêmes variables ; ces dernières ne sont toutefois pas partagées entre processus, on peut donc dire qu'elles sont locales.

Mais dans le cas des threads Java ?

Attention de ne pas tout mélanger (comparer process UNIX avec Thread java, bof).

Des ressources locales a un thread sont des ressources que seul le thread détient. C'est le même principe qu'une variable d'instance, a condition qu'elle ne pointe pas sur un objet partage.

Pour prendre un exemple bateau :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class X extends Thread {
     static Object a = new Object();  // a est partage entre tous les instances de X
     Object b;
     Object c;

     public X(Object c) {
        this.b = new Object();  // b est local au thread
        this.c = c;  // c vient de l'exterieur, il peut donc etre partage
    }
}

Pourquoi peut-on utiliser un objet this alors qu'a priori, un thread A n'aura pas le même objet this que son frère le thread B ? On m'a dit précédemment que le verrou doit être le même objet.

Le plus souvent, le synchronized n'est pas posé directement dans le thread, mais d'un objet dont on cherche a protéger la section critique. Si la section critique est locale a l'objet, alors this suffit.

+0 -0

Sinon merci beaucoup mais je n'en suis pas encore à utiliser les classes fournies par java.util.concurrent, j'essaie déjà de comprendre la base ^^

Lern-X

Justement, ce qu'on te dit c'est que tu crois que ce tu fais c'est les bases et que les classes de java.util.concurrent sont les techniques avancées, alors que c'est précisément l'inverse. Là tu t'intéresse à des détails sordides alors que tu n'en auras jamais besoin, parce que justement dans 95% des cas, les conteneurs existant font déjà ça, et en mieux que ce tu pourras écrire toi même.

Le principe de ces classes, c'est justement de ne pas avoir à se préoccuper d'une quelconque synchronisation (séparation temporelle) parce qu'elles le font déjà. Et que tu n'as qu'à te contenter d'assurer la séparation spatiale des données.

Je ne serais pas aussi catégorique, il n'est jamais inutile de savoir comment les choses fonctionnent sous le capot. Et il est très important de bien comprendre le threading-model de Java lorsque l'on touche aux threads. En l’occurrence, comprendre les locks implicites mis en jeu par synchronized, et le mécanisme de wait/notify (bref, les moniteurs quoi) me semble important.

Ce n'est d'ailleurs pas tout a fait pour rien que java.utils.concurrent est arrivé relativement tard dans le SDK (depuis java 1.5, avec la JSR-166). Ce sont des outils de haut niveau, a utiliser absolument des lors qu'ils existent, mais a condition de savoir quand, pourquoi et comment.

Voici un bon point d’entrée pour aborder la concurrence en Java : https://docs.oracle.com/javase/tutorial/essential/concurrency/

+0 -0

@Yoch : c peut être partagé ou bien local (respectivement si on donne le même c à 2 objets X, et si on donne deux c différents à 2 objets :X) non ?

"Le plus souvent, le synchronized n'est pas posé directement dans le thread, mais d'un objet dont on cherche a protéger la section critique. Si la section critique est locale a l'objet, alors this suffit."

Heum je crois voir une contradiction entre le nom "section critique" et le fait qu'elle puisse être locale… Est-ce que je peux te demander un exemple s'il te plaît ? =/ Du même genre que celui que tu as écrit pour synchronized, ça permet de comprendre de manière concrète ! :)

@Yoch : c peut être partagé ou bien local (respectivement si on donne le même c à 2 objets X, et si on donne deux c différents à 2 objets :X) non ?

Tout a fait.

"Le plus souvent, le synchronized n'est pas posé directement dans le thread, mais d'un objet dont on cherche a protéger la section critique. Si la section critique est locale a l'objet, alors this suffit."

Heum je crois voir une contradiction entre le nom "section critique" et le fait qu'elle puisse être locale… Est-ce que je peux te demander un exemple s'il te plaît ? =/ Du même genre que celui que tu as écrit pour synchronized, ça permet de comprendre de manière concrète ! :)

Lern-X

Il n'y a pas de contradiction, parce que deux threads peuvent très bien accéder au même objet (qui est donc partagé), et exécuter une section critique en même temps.

Un exemple bidon :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Counter {
    private int n = 0;

    synchronized public void increment() {  // Section critique
        n++;
    }

    public int getValue() {
        return n;
    }
}

class Task extends Thread {
    private Counter counter;

    public Task(Counter counter) {
        this.counter = counter;
    }

    public void run() {
        // do stuff
        counter.increment();
    }
}
+0 -0

Tu as parfaitement raison, mais dans mon exemple je n'utilise pas getValue() depuis un thread. On pourrait imaginer qu'il sera appelé depuis le thread principal une fois les autres threads terminés, mais j'aurais du le préciser.

+0 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte