IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Navigation

Inscrivez-vous gratuitement
pour pouvoir participer, suivre les réponses en temps réel, voter pour les messages, poser vos propres questions et recevoir la newsletter

Java Discussion :

Erreur d'implantation EventListenerList dans JDK?


Sujet :

Java

Vue hybride

Message précédent Message précédent   Message suivant Message suivant
  1. #1
    Membre Expert
    Avatar de ®om
    Profil pro
    Inscrit en
    Janvier 2005
    Messages
    2 815
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Janvier 2005
    Messages : 2 815
    Par défaut Erreur d'implantation EventListenerList dans JDK?
    D'après moi, en regardant le code source de EventListenerList, il y a une erreur (ou plutôt un cas non géré) :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    public synchronized <T extends EventListener> void add(Class<T> t, T l) {
        // c'est synchronized
    }
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    public synchronized <T extends EventListener> void remove(Class<T> t, T l) {
        // c'est synchronized
    }
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // non synchronized
    public <T extends EventListener> T[] getListeners(Class<T> t) {
        Object[] lList = listenerList; 
        int n = getListenerCount(lList, t); 
        T[] result = (T[])Array.newInstance(t, n); 
        int j = 0; 
        for (int i = lList.length-2; i>=0; i-=2) {
            if (lList[i] == t) {
    	    result[j++] = (T)lList[i+1];
            }
        }
        return result;   
    }
    Le problème se trouve sur:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    Object[] lList = listenerList;
    En effet, sur une machine MULTI-processeurs, un processeur P1 peut faire un getListeners, puis un autre P2 peut faire un add, suivi plus tard par un getListeners par le processeur P1.
    Le 2e getListeners de P1 peut potentiellement ne pas voir la modification effectuée par le add de P2, même si le getListeners a lieu bien après (même si en pratique la probabilité est très faible). Ceci se produit si le cache de P1 n'a pas été invalidé (du moins pour la variable listenerList), ce qui est possible vu que la méthode getListeners n'est pas synchronisée et que listenerList n'est pas volatile.

    Même si on peut considérer que getListeners est faite en théorie pour être exécutée dans l'EDT, les add et remove peuvent être appelés de l'extérieur (sinon ils ne seraient pas synchronized).

    On pourrait certes considérer que le niveau de cohérence ne soit pas la "cohérence séquentielle", et que les lectures peuvent être anciennes, mais ça serait une hypothèse un peu trop faible...

    Je peux me tromper, mais ça ne serait pas la première fois que la synchronisation n'est correcte dans java (avant java 1.5, volatile ne marchait pas, et les moniteurs ne pouvaient avoir qu'une variable condition).

    Qu'en pensez-vous?

  2. #2
    Membre émérite
    Profil pro
    Inscrit en
    Juillet 2006
    Messages
    548
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Juillet 2006
    Messages : 548
    Par défaut
    Ici le but du synchronized c'est de ne pas pouvoir faire un add et un remove en même temps, c'est tout.
    Ce n'est pas forcément utile de garantir qu'un appel à une méthode voit systématiquement les modifications faites par les appels précédents (ce que tu appelles "cohérence séquentielle"). Dans ton exemple, le fait que P1 ne voit pas la modif de P2 n'a pas vraiment d'importante, de toute façon s'il avait commencé un peu plus tôt la modif n'avait même pas commencé.
    Ici le plus important c'est que que si deux add (ou deux remove, ou un add et un remove) se font "en même temps", les opérations correspondantes sont bien effectuées. Sans le synchronized tu peux par exemple avoir un add qui écrase la modification d'un autre add.

  3. #3
    Membre Expert
    Avatar de ®om
    Profil pro
    Inscrit en
    Janvier 2005
    Messages
    2 815
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Janvier 2005
    Messages : 2 815
    Par défaut
    Citation Envoyé par the-gtm
    Ici le but du synchronized c'est de ne pas pouvoir faire un add et un remove en même temps, c'est tout.
    Certes, mais en plus ça met en cohérence les caches.

    Citation Envoyé par the-gtm
    Ce n'est pas forcément utile de garantir qu'un appel à une méthode voit systématiquement les modifications faites par les appels précédents (ce que tu appelles "cohérence séquentielle"). Dans ton exemple, le fait que P1 ne voit pas la modif de P2 n'a pas vraiment d'importante, de toute façon s'il avait commencé un peu plus tôt la modif n'avait même pas commencé.
    Mais ce qui est génant, c'est par exemple:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    Processeur 1
    ------------
    (1) a = 3
    (2) getListeners()
    (3) println(a)
    (4) getListeners()
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    Processeur 2
    ------------
    (5) addListener(...)
    (6) a = 6
    Si l'ordre d'exécution est 1 2 5 6 3 4, la ligne 3 affiche bien 6, mais le getListeners() de la ligne 4 peut ne pas voir l'ajout fait par P2, ce qui ici est incohérent (au sens de la cohérence séquentielle) car l'ajout est effectué avant l'affectation de 6 à a...

    Citation Envoyé par the-gtm
    Ici le plus important c'est que que si deux add (ou deux remove, ou un add et un remove) se font "en même temps", les opérations correspondantes sont bien effectuées. Sans le synchronized tu peux par exemple avoir un add qui écrase la modification d'un autre add.
    Ça on n'est d'accord, les synchronized de add et remove sont nécessaires... mais pas suffisants sur un multiprocesseur...

  4. #4
    Membre émérite
    Profil pro
    Inscrit en
    Juillet 2006
    Messages
    548
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Juillet 2006
    Messages : 548
    Par défaut
    Le problème dans dans exemple est que tu as deux objets : "a" et la ListenerList que tu veux garder cohérents entre eux (si addListener a été appelé alors a = 6, sinon a = 3).
    Pour ça la seule solution possible est de créer un verrou qui englobe les opérations d'écritures et de lecture sur ces deux variables.

    Cela dit il n'y a jamais eu de contrat de "cohérence séquentielle" dans Java alors je ne vois pas où est le problème. Ce n'est pas parce qu'une méthode commence avant une autre qu'elle doit finir avant aussi.

  5. #5
    Membre Expert
    Avatar de ®om
    Profil pro
    Inscrit en
    Janvier 2005
    Messages
    2 815
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Janvier 2005
    Messages : 2 815
    Par défaut
    Citation Envoyé par the-gtm
    Le problème dans dans exemple est que tu as deux objets : "a" et la ListenerList que tu veux garder cohérents entre eux (si addListener a été appelé alors a = 6, sinon a = 3).
    Pour ça la seule solution possible est de créer un verrou qui englobe les opérations d'écritures et de lecture sur ces deux variables.

    Cela dit il n'y a jamais eu de contrat de "cohérence séquentielle" dans Java alors je ne vois pas où est le problème. Ce n'est pas parce qu'une méthode commence avant une autre qu'elle doit finir avant aussi.
    Ici le problème n'est pas "une méthode commence avant une autre doit finir avant"... (heureusement que ça n'est pas vrai)

    C'est l'ordre d'exécution au sein d'un seul processeur...
    En gros, au départ a = 0 et b = 0 :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    P2:
    print(b)
    print(a)
    résultat :
    b = 2
    a = 0

    (EDIT: l'ordre des print est important pour montrer le problème)

    C'est incohérent séquentiellement car si b = 2, la ligne a = 1 a forcément été exécutée.


    Pour reprendre le problème avec les listeners sur un multiprocesseur, voici ce qui pourrait se produire (avec une très faible proba, je te l'accorde):
    - P2 fait un addListener(listener).
    - 5 minutes après, P1 fait un getListeners(), et ne voit pas le listener ajouté par P2, pour P1 getListeners() renvoie toujours un tableau vide.

    Et ça, c'est génant...



    EDIT: Le niveau de cohérence garanti dans ce cas, si je ne me trompe pas, est REPEATABLE_READ (lectures fantômes acceptées), ce qui est un peu faible quand même...

  6. #6
    Membre émérite
    Profil pro
    Inscrit en
    Juillet 2006
    Messages
    548
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Juillet 2006
    Messages : 548
    Par défaut
    Citation Envoyé par ®om
    C'est l'ordre d'exécution au sein d'un seul processeur...
    En gros, au départ a = 0 et b = 0 :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    P2:
    print(a)
    print(b)
    résultat :
    a = 0
    b = 2

    C'est incohérent séquentiellement car si b = 2, la ligne a = 1 a forcément été exécutée.

    Ton exemple c'est toujours la même chose : tu as deux variables et tu veux soit (a,b) = (0, 0), soit (a, b) = (1, 2), il faut donc mettre un verrou autour des ecritures lectures.

    Tu dis que si P2 affiche 0 puis 2 alors "C'est incohérent séquentiellement car si b = 2, la ligne a = 1 a forcément été exécutée."
    Mais c'est bien le cas ! seulement au moment ou P2 a affiché a, on était dans l'état intermédiaire (a, b) = (0, 2)

    Pour eviter ça on met un verrou :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    P1:
    synchronized(verrou) {
      a = 0
      b = 2
    }
     
    P2 :
    synchronized(verrou) {
      print(a)
      print(b)
    }
    Et cela dit, pas besoin d'une machine multiprocesseur pour avoir des problèmes de concurrence, il suffit d'avoir plusieurs threads.

    Pour reprendre le problème avec les listeners sur un multiprocesseur, voici ce qui pourrait se produire (avec une très faible proba, je te l'accorde):
    - P2 fait un addListener(listener).
    - 5 minutes après, P1 fait un getListeners(), et ne voit pas le listener ajouté par P2, pour P1 getListeners() renvoie toujours un tableau vide.

    Et ça, c'est génant...
    Si c'est génant, c'est qu'il te manque un verrou : ça veut dire qu'au moment où P1 s'attend à trouver un listener, P2 n'a pas encore fini de l'ajouter. Mais pour que P1 sache qu'un listener est censé être là, il lui faut une autre variable (je l'appelle "flag"), sinon il n'a aucun moyen de savoir que P2 a commencé addListener. Et on revient à ce que je dis : le problème est que "flag" et la ListenerList sont dans un état incohérent.

  7. #7
    Expert éminent
    Avatar de adiGuba
    Homme Profil pro
    Développeur Java/Web
    Inscrit en
    Avril 2002
    Messages
    13 938
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France

    Informations professionnelles :
    Activité : Développeur Java/Web
    Secteur : Transports

    Informations forums :
    Inscription : Avril 2002
    Messages : 13 938
    Billets dans le blog
    1
    Par défaut
    Salut,


    Pour moi il n'y a pas de problème. : le mot-clef synchronized n'est pas le seul à permettre un code thread-safe

    Je m'explique : on est d'accord sur le fait que les méthodes add() et remove() doivent être synchronisé pour éviter des "pertes" de données...

    Toutefois, si tu regardes bien le code de ces deux méthodes, tu t'aperçois qu'elles travailles sur un tableau d'objets temporaire, et que l'attribut listenerList n'est modifié qu'à la fin du traitement. Par exemple dans la méthode add() on peut trouver ceci (j'ai enlevé les vérifications et cas particuliers) :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    	    // Otherwise copy the array and add the new listener
    	    int i = listenerList.length;
    	    Object[] tmp = new Object[i+2];
    	    System.arraycopy(listenerList, 0, tmp, 0, i);
     
    	    tmp[i] = t;
    	    tmp[i+1] = l;
     
    	    listenerList = tmp;
    Ainsi à chaque opération add() ou remove(), un nouveau tableau est créé, et ensuite sa référence est assigné dans l'attribut listenerList. Or l'assignement est forcément une opération atomique (sauf pour les types long et double où cela peut dépendre de l'implémentation car ils sont codés sur 64bits).



    Et si tu regardes bien la méthodes getListeners(), elle copie la référence de listenerList dans une variable locale :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    Object[] lList = listenerList;
    Cette opératio n'a pas besoin d'être synchronisé puisque l'accès à listenerList est une opération atomique.

    Et comme le reste de la méthode travaille sur la variable locale lList. De ce fait si un add() ou remove() est appelé dans un autre thread, cela modifiera l'attribut listenerList mais n'aura aucun impact sur notre méthode getListeners() car elles travaille sur une copie :
    [list][*]Dans le meilleur des cas le add()/remove() modifie la référence de listenerList juste avant l'affection dans lList, et dans ce cas la méthode getListeners() travaillera avec les toutes dernières données.[*]Dans le pire des cas, listenerList sera modifié entre l'affection de lList et la boucle de traitement, et dans ce cas lList correspond à une version "obsolète" de listenerList, et possèdera alors un listener en plus ou en moins... mais cela ne pose pas de problème d'exécution et est acceptable (grosso modo cela reviendrait à ce que la méthode add()/remove() soit exécuté après le getListeners()).




    Le fait de ne modifier la référence de l'attribut qu'à la fin des méthodes add()/remove(), et d'utiliser une copie de la référence dans les autres méthodes est une optimisation permettant d'éviter de tout synchroniser.

    Un exemple un peu plus explicite :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    class MaClasse {
     
    	String value;
     
    	public synchronized void setValue(String value) {
    		this.value = value.trim();
     
    		// On remplace les caractères spéciaux HTML :
    		this.value = this.value.replaceAll("&", "&amp;");
    		this.value = this.value.replaceAll("<", "&lt;");
    		this.value = this.value.replaceAll(">", "&gt;");
     
    		// On remplace les fin de ligne par des <BR/>
    		this.value = this.value.replaceAll("\n", "<BR/>");
     
    		// On supprime les espaces multiples :
    		this.value = this.value.replaceAll("\\s+", " ");
    	}
     
    	public synchronized String getValue() {
    		return this.value;
    	}
     
    	public synchronized void method() {
     
    		// Plusieurs traitements sur this.value
     
    	}
     
    }
    Dans ce code, on est obligé de synchroniser les trois méthodes, sinon on pourrait avoir une valeur de this.value lorsqu'on y accède entre deux replaceAll()...

    Le problème c'est que toutes les méthodes de la classe qui utiliseront l'attribut this.value devront être synchronisé... ce qui rend le tout plus complexe et peut multiplier les 'conflits' sur les verrous...


    Or, dans ce cas, il est possible de faire un code thread-safe sans utiliser le mot-clef synchronized.

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    class MaClasse {
    	String value;
     
    	public void setValue(String value) {
    		String lValue = value.trim();
     
    		// On remplace les caractères spéciaux HTML :
    		lValue = lValue.replaceAll("&", "&amp;");
    		lValue = lValue.replaceAll("<", "&lt;");
    		lValue = lValue.replaceAll(">", "&gt;");
     
    		// On remplace les fin de ligne par des <BR/>
    		lValue = lValue.replaceAll("\n", "<BR/>");
     
    		// On supprime les espaces multiples :
    		lValue = lValue.replaceAll("\\s+", " ");
     
    		// On ne modifie l'attribut qu'à la fin du traitement :
    		this.value = lValue;
    	}
     
    	public String getValue() {
    		return this.value;
    	}
     
    	public void method() {
     
    		String lValue = this.value;
    		// Plusieurs traitement sur lValue
     
    	}
    }
    On respecte seulement deux règles simples :
    • Chaque méthode qui modifie l'attribut travaille sur une variable locale, et n'affecte la référence de l'attribut qu'à la fin de son traitement (une fois que la valeur est correcte).
    • Chaque méthode qui utilise l'attribut n'effecte qu'un accès à sa référence, en effectuant une copie dans une variable locale s'il a besoin d'effectuer plusieurs traitements.


    Et dans le pire des cas on travaille sur une copie 'obsolète' de l'attribut, mais cela peut être acceptable dans bien des cas (et permet d'éviter de tout synchronisé).



    a++

  8. #8
    Membre Expert
    Avatar de ®om
    Profil pro
    Inscrit en
    Janvier 2005
    Messages
    2 815
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Janvier 2005
    Messages : 2 815
    Par défaut
    En fait, adiGuba, avant c'est ce que je pensais.

    C'était d'ailleurs l'objet de 2 posts (très semblables) que j'avais fait sur le forum (le 1er, le 2e).

    Mais en fait, après en avoir discuté il y a quelques mois avec mon prof de synchronisation, je crois que cette phrase n'est pas vraie:
    Cette opération n'a pas besoin d'être synchronisée puisque l'accès à listenerList est une opération atomique.
    En effet, le synchronized effectue 2 choses : l'exclusion mutuelle (qui assure l'atomicité), mais pas seulement, il permet aussi de mettre en cohérence les caches des processeurs.
    Donc certes avec une opération atomique tu résouds un problème, mais pas le 2e.

    Sur un mono-processeur, ça n'est pas génant, et a priori, sur un dual core (qui partagent le même cache) non plus, mais sur un vrai multi-processeur, la cohérence n'est pas garantie...

Discussions similaires

  1. Recupérer une erreur d'un batch dans un vbs
    Par Pitbull7 dans le forum Windows
    Réponses: 1
    Dernier message: 06/10/2005, 21h10
  2. Réponses: 8
    Dernier message: 13/09/2005, 21h05
  3. librairie introuvable! Erreur"Pas d'objet dans ce contr
    Par vins111282 dans le forum Access
    Réponses: 5
    Dernier message: 16/05/2005, 14h07
  4. erreur de syntaxe javascript dans ma page
    Par Oluha dans le forum Général JavaScript
    Réponses: 7
    Dernier message: 01/02/2005, 14h53
  5. [CONNECTION] Erreur lors du connect dans le fichier C
    Par Petey dans le forum PostgreSQL
    Réponses: 1
    Dernier message: 19/04/2004, 18h13

Partager

Partager
  • Envoyer la discussion sur Viadeo
  • Envoyer la discussion sur Twitter
  • Envoyer la discussion sur Google
  • Envoyer la discussion sur Facebook
  • Envoyer la discussion sur Digg
  • Envoyer la discussion sur Delicious
  • Envoyer la discussion sur MySpace
  • Envoyer la discussion sur Yahoo