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 :

Faire une synchronized sur une valeur d'une hashtable sans tout verrouiller


Sujet :

Java

Vue hybride

Message précédent Message précédent   Message suivant Message suivant
  1. #1
    Modérateur

    Avatar de Bktero
    Homme Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Juin 2009
    Messages
    4 493
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 38
    Localisation : France, Loire Atlantique (Pays de la Loire)

    Informations professionnelles :
    Activité : Ingénieur développement logiciels

    Informations forums :
    Inscription : Juin 2009
    Messages : 4 493
    Billets dans le blog
    1
    Par défaut Faire une synchronized sur une valeur d'une hashtable sans tout verrouiller
    Bonjour,

    Je suis en train de réfléchir à un moyen de synchroniser des threads. Ces threads traitent des enregistrements (lus dans des fichiers) avant de les insérer dans une base de données. Pour le moment, les threads ignorent ce que traitent les autres threads à ce moment là. On peut se retrouver avec des lignes en doublons dans la table (certains champs de l'enregistrement constituant une clé de dédoublonnage).

    Une première idée que j'ai mûri avec un collègue est d'utiliser une HashTable, qui prend en clé la clé de dédoublonnage des enregistrements et en valeur une instance d'une classe maison. Cette classe, appelé Enregistrement (rien à voir avec l'enregistrement traité, le nom n'est pas choisi) contient un objet quelconque qui sert de mutex ainsi que d'autres informations utiles.

    Chaque thread va lire la HashTable pour déterminer si un enregistrement similaire au sien est en cours de traitement.
    - Si non, il écrit dans la hashtable et prend le mutex sur l'objet qu'il vient d'insérer. Si le relache quand il a finit.
    - Si oui, on demande le mutex sur l'objet existant et on attend de l'obtenir pour traiter.

    Chaque thread fait des pré-traitements sur son enregistrement, puis appelle la fonction suivante (code test et non code définitif, d'où des sleep à la place de vrais traitements) :
    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
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    private static HashTable<Integer, Enregistrement> synchronizer  = new HashTable<Integer, Enregistrement>();
              public boolean utiliserSynchronizer() {
     
    	  boolean estPresent = false;
     
    	  // Creation de l'enregistrement a mettre la Synchronizer
    	  Enregistrement monRec = new Enregistrement(threadMessage);
     
    	// Creation de l'enregistrement qu'on recuperera du Synchronizer
    	  Enregistrement existant;
     
    	  // Verrouillage du Synchronizer ( http://rom.developpez.com/java-synchronisation/#LII-A )
    	  synchronized(synchronizer) {
     
    		  if(true==synchronizer.containsKey(threadId)) {
    			  estPresent = true;
    		  }
    		  else {
    			  synchronizer.put(threadId, monRec);
    			  estPresent = false;
    		  }
    	  }
    	  /* On libere au plus vite la HashTable. Ainsi, d'autres threads peuvent y accéder.
    	   * Dans la suite, on fait l'action necessaire en fonction de la variable "estPresent"
    	   */
    	  if(false==estPresent) {
    		  synchronized(monRec.getVerrou()) {
    			  // On simule le traitement d'encodage du record
    			  try {
    				  System.out.println("On commence a encoder (" + threadMessage + ")");
    				  sleep(10000);
    				  System.out.println("On termine d'encoder (" + threadMessage + ")");
    			  }
    			  catch(Exception ex){}
    		  }		  
    	  }
    	  else {
    		  // Recuperation de cet enregistrement existant
    		  existant = synchronizer.get(threadId);
    		  System.out.println( "\tJ'ai lu : " + existant.toString() + "(" + threadMessage + ")");	
     
    		  // Comparer la date
    		  //if( monRec.getDate().after(existant.getDate())==false ) {
    		  if(false) {
    			  //System.out.println("On rejette l'enregistrement  (" + threadMessage + ")");
    		  }
    		  else { 
    			  // On se met en attente de l'enregistrement
    			  synchronized(existant.getCompteur()) {
    				  existant.setCompteur(existant.getCompteur() + 1);
    			  }	  	  
     
    			  // On attend notre tour...
    			  System.out.println("On attend notre tour  (" + threadMessage + ")");
    			  synchronized(existant.getVerrou()) {
    				  System.out.println("On a obtenu le verrou  (" + threadMessage + ")");
    				  // On n'est plus en attente donc on decremente le verrou
    				  existant.setCompteur(existant.getCompteur() - 1);
     
    				  // On remplace avec nos donnees
    				  existant.setDebugTxt( existant.getDebugTxt() + "_" + monRec.getDebugTxt() );
     
    				  // On simule le traitement d'encode du record
    				  try {
    					  System.out.println("On commence a encoder (" + threadMessage + ")");
    					  sleep(10000);
    					  System.out.println("On termine d'encoder (" + threadMessage + ")");
    				  }
    				  catch(Exception ex){}					  					  					  					  
    			  } // section critique sur l'enregistrement
    		  }  
    	  }//esPresent ?
    	  return estPresent;
      }
    Ca fonctionne bien à un gros détail près : l'appel à synchronized(monRec.getVerrou()) bloque complètement l'accès à la hashtable.

    Conséquence : 2 threads ne peut pas être en cours d'encodage en même temps. C'est pas terrible

    Auriez-vous une suggestion pour contourner ce problème ? Merci d'avance !

  2. #2
    Membre Expert
    Inscrit en
    Mai 2006
    Messages
    1 364
    Détails du profil
    Informations forums :
    Inscription : Mai 2006
    Messages : 1 364
    Par défaut
    Salut,

    En lisant ton code en diagonale, il y a plusieurs points qui me semblent bizarres. Deja, le traitement long dans un bloc synchronisé. Dans un cas comme ca, je ne synchroniserais que la lecture/ecriture d'un flag pour savoir si l'enregistrement est en cours de traitement. Si oui, je ne le traite pas (mais ca, je n'ai pas bien compris si c'est ce que tu veux faire ou non).

    Ensuite, tu fais 2 fois
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    existant.setCompteur(...)
    en utilisant des verrous différents. Ca ressemble à une erreur...

    a+

  3. #3
    Modérateur

    Avatar de Bktero
    Homme Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Juin 2009
    Messages
    4 493
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 38
    Localisation : France, Loire Atlantique (Pays de la Loire)

    Informations professionnelles :
    Activité : Ingénieur développement logiciels

    Informations forums :
    Inscription : Juin 2009
    Messages : 4 493
    Billets dans le blog
    1
    Par défaut
    Bonjour,

    Le problème venait de la classe Enregistrement. En effet, la méthode Enregistrement.getVerrou() renvoie l'objet verrou de la classe Enregistrement qui est de type Integer. Dans le constructeur (unique) de cette classe, je faisais verrou=0. Je l'ai remplacé le type Integer par le type Object et j'ai fait dans le constructeur verrou = new Object();. L'explication est dans cette page : https://www.securecoding.cert.org/co...+may+be+reused

    Merci pour tes remarques, je vais vérifier ça. Je pense que n'est peut être pas été assez complet dans les explications. Prenons un exemple de 3 threads traitant 3 enregistrements de même clé de dédoublonnage (mais pas forcément identique, donc devant tous être traités). Pour l'instant, chaque thread bosse dans son coin et insère la ligne en base avec le statut valide. Or, je ne dois avoir qu'une seule ligne valide par clé de dédoublonnage (jai tout une batterie de règles pour déterminer lequel des 3 est celui qui sera valide).

    L'idée est donc de donner une méthode à un thread pour savoir si un autre thread traite un enregistrement de même clé de dédoublonnage. Deux cas :

    1. Aucun autre thread en cours = pas d'entrée dans la HashTable
      • Insertion dans la HashTable
      • Prise du verrou (getVerrou)
      • Traitement
      • Si le compteur est toujours à zéro (ce compteur est celui du nombre de threads en attente du verrou, alors suppression de l'entrée dans la HashTable
      • Relâchement du verrou
    2. Au moins un thread en cours = une entrée dans la HashTable
      • Récupération de l'enregistrement
      • Synchronisation du compteur pour l'incrémenter et indiquer au thread en cours de traitement qu'on est en attente
      • Demander la prise du verrou et attendre que le premier thread le libère
      • Une fois le verrou obtenu, décrémenter le compteur pour indiquer qu'on l'a obtenu
      • Traitement
      • Même sortie que précédemment


    Il faut bien attendre la libération du verrou car tous les enregistrements doivent être traités. Le traitement n'est pas si long que mon sleep, je l'ai exagéré pour bien voir. En production, on n'excède rarement la seconde, au pire la seconde et demi. En règle général, on traite 50 enregistrements secondes.

    Si le code t'intéresse, je peux le mettre en entier

  4. #4
    Membre Expert
    Inscrit en
    Mai 2006
    Messages
    1 364
    Détails du profil
    Informations forums :
    Inscription : Mai 2006
    Messages : 1 364
    Par défaut
    D'apres ce que je comprends, le traitement long dans le bloc synchronisé est voulu.
    Ca ne change rien à ma remarque ou on ecrit le compteur en prenant des verrous differents (ca, ca m'a pas l'air bon meme si ca ne change rien au probleme).

    Par contre, je ne vois pas pourquoi getVerrou bloquerait la hashtable. Peut etre que le probleme vient d'ailleurs. Est ce que tu as un code minimal qui reproduit ce fonctionnement ? Et au moins la methode getVerrou?

  5. #5
    Modérateur

    Avatar de Bktero
    Homme Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Juin 2009
    Messages
    4 493
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 38
    Localisation : France, Loire Atlantique (Pays de la Loire)

    Informations professionnelles :
    Activité : Ingénieur développement logiciels

    Informations forums :
    Inscription : Juin 2009
    Messages : 4 493
    Billets dans le blog
    1
    Par défaut
    Faut vraiment que je revois cette histoire de compteur alors ^^

    En fait, je ne pense pas que ça bloque la hashtable en entier. C'est juste que l'objet verrou est le même pour toute les instances de la classe Enregistrement (pour les raisons expliqués dans le lien) car verrou=1 est en toute logique associé à la même instance de la classe englobante Integer.

    Ainsi, tous les threads demandaient une section sur le même objet (celui renvoyé par getVerrou()). Quand ils traitent des enregistrements en doublon, c'est normal ; ce ne l'est pas quand 'ils traitent des enregistrements avec des clés de dédoublonnages différentes.

    Voici le code tel qu'il est actuellement et qui fonctionne (Enregistrement devient EnreSynchro btw) :

    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
    33
    34
    35
    36
    37
    public class EnreSynchro {
    	private final Object verrou;
    	private Integer compteur;	
    	private String debugTxt;
     
    	public EnreSynchro(String message) {
    		verrou = new Object();
    		compteur = new Integer(0);		
    		debugTxt = new String(message);
    	}
     
    	/* Methode d'affichage	 */
    	public String toString() {
    		return "E[Txt=" + debugTxt + ", compteur=" + compteur + "]";
    	}
     
    	/* Ensemble de getters et setters pour les variables de classes */
    	public synchronized Object getVerrou() {
    		return verrou;
    	}
     
    	public synchronized Integer getCompteur() {
    		return compteur;
    	}
     
    	public synchronized void setCompteur(Integer compteur) {
    		this.compteur = compteur;
    	}
     
    	public synchronized String getDebugTxt() {
    		return debugTxt;
    	}
     
    	public synchronized void setDebugTxt(String debugTxt) {
    		this.debugTxt = debugTxt;
    	}
    }
    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
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    import java.util.Hashtable;
    import java.util.ArrayList;
    import java.util.List;
    import java.util.Date; // debug
     
    public class Traiteur extends Thread {
     
     
      private String threadMessage;
      private Integer threadId;
      //private static Hashtable<Integer, Enregistrement> synchronizer  = new Hashtable<Integer, Enregistrement>();
      private static Hashtable<Integer, Enregistrement> synchronizer  = new Hashtable<Integer, Enregistrement>();
     
     
     
      /* Constructeur avec parametres */
      public Traiteur(Integer identifiant, String message) {
        this.threadMessage = message;
        this.threadId=identifiant;
      }
     
     
     
      /* La fonction regarde si les proprietes du thread sont deja dans le synchronizer */
      public boolean utiliserSynchronizer() {
     
    	  boolean estPresent = false;
     
    	  // Creation de l'enregistrement a mettre la Synchronizer
    	  Enregistrement monRec = new Enregistrement(threadMessage);
     
    	// Creation de l'enregistrement qu'on recuperera du Synchronizer
    	  Enregistrement existant;
     
    	  // Verrouillage du Synchronizer ( http://rom.developpez.com/java-synchronisation/#LII-A )
    	  synchronized(synchronizer) {
     
    		  //DEBUG
    		  //System.out.println("\tJe suis : " + monRec.toString());
     
    		  if(true==synchronizer.containsKey(threadId)) {
    			  estPresent = true;
    			  // Le traitement se fait plus tard
    		  }
    		  else {
    			  // Ajout de notre enregistrement dans Synchronizer
    			  synchronizer.put(threadId, monRec);
    			  estPresent = false;
    		  }
    	  }
    	  /* On libere au plus vite la Hashtable. Ainsi, d'autres threads peuvent y accéder.
    	   * Dans la suite, on fait l'action necessaire en fonction de la variable "estPresent"
    	   */
    	  if(false==estPresent) {
     
    		  // On peut car on l'a deja insere l'enregistrement 
    		  //existant = synchronizer.get(threadId);
    		  //Object eVerrou = existant.getVerrou();
     
     
    		  //synchronized(eVerrou) {
    		  synchronized (monRec.getVerrou() ) {
    			  // On simule le traitement d'encode du record
    			  try {
    				  System.out.println("On commence a encoder (" + threadMessage + ")");
    				  sleep(100);
    				  System.out.println("On termine d'encoder (" + threadMessage + ")");
    			  }
    			  catch(Exception ex){}
    		  }
     
    	  }
    	  else {
    		  // Recuperation de cet enregistrement
    		  existant = synchronizer.get(threadId);
    		  Object eVerrou = existant.getVerrou();
    		  Integer eCompteur = existant.getCompteur();
     
    		  System.out.println( "\tJ'ai lu : " + existant.toString() + "(" + threadMessage + ")");	
     
    		  // Comparer la date
    		  //if( monRec.getDate().after(existant.getDate())==false )
    		  if(false) {
    			  //System.out.println("On rejette l'enregistrement  (" + threadMessage + ")");
    		  }
    		  else { 
    			  // On se met en attente de l'enregistrement
    			  synchronized(eCompteur) {
    				  existant.setCompteur(existant.getCompteur() + 1);
    				  System.out.println("On attend notre tour...  (" + threadMessage + ")");
    			  }	  	  		  
     
    			  // On attend notre tour...
    			  synchronized(eVerrou) {
    				  System.out.println("On a obtenu le verrou  (" + threadMessage + ")");
    				  // On n'est plus en attente donc on decremente le verrou
    				  existant.setCompteur(existant.getCompteur() - 1);
     
    				  // On remplace avec nos donnees
    				  existant.setDebugTxt( existant.getDebugTxt() + "_" + monRec.getDebugTxt() );
     
    				  // On simule le traitement d'encode du record
    				  try {
    					  System.out.println("On commence a encoder.... (" + threadMessage + ")");
    					  sleep(100);
    					  System.out.println("On termine d'encoder (" + threadMessage + ")");
    				  }
    				  catch(Exception ex){}					  					  					  					  
    			  } // section critique sur l'enregistrement
    		  }  
    	  }//esPresent ?
    	  return estPresent;
      }
     
     
      /* Fonction qui sera execute par chaque thread */
      @Override
      public void run() {
    	  //System.out.println("Run du thread " + threadId + "/" + threadMessage);  	  
    	  try {
    		  sleep(20); // simule les traitements avant l'encodage du record
    		  utiliserSynchronizer();	// on y encode le record	  
    	  }
    	  catch(Exception ex) {
    		  System.out.println("Le thread " + threadId + " a rencontre une exception !");
    		  ex.printStackTrace();
    	  }    
    	  //System.out.println("Fin du thread " + threadId + "/" + threadMessage);
      }
     
      /* Programme de test */
      public static void main (String [] args)
    	{
    		System.out.println("Debut du programme PocVerrou...");
    		System.out.println(new Date() + "\n\n");
     
    		// Conteneur des threads
    		List<Traiteur> mesTraiteurs = new ArrayList<Traiteur>();
     
    		// Lancement de threads
        	Traiteur t1 = new Traiteur(1, "Premier thread");
        	t1.start();
            mesTraiteurs.add(t1);
     
            try { sleep(5); } catch(Exception ex){}
     
        	Traiteur t2 = new Traiteur(1, "Second thread");
        	t2.start();
            mesTraiteurs.add(t2);
     
            try { sleep(5); } catch(Exception ex){}
     
        	Traiteur t3 = new Traiteur(1, "Troisieme thread");
        	t3.start();
            mesTraiteurs.add(t3);
     
            try { sleep(5); } catch(Exception ex){}
     
        	Traiteur t4 = new Traiteur(1, "Quatrieme thread");
        	t4.start();
            mesTraiteurs.add(t4);
     
            try { sleep(5); } catch(Exception ex){}
     
        	Traiteur t5 = new Traiteur(1, "Cinquieme thread");
        	t5.start();
            mesTraiteurs.add(t5);
     
    		/*
            for (int i=1; i<=15; i++) {
            	Traiteur monTraiteur = new Traiteur(i, String.valueOf(i)+"_A");
            	monTraiteur.start();
                mesTraiteurs.add(monTraiteur);
            }
            */
     
            // Attente de tous les threads
            for (Traiteur monTraiteur : mesTraiteurs) {
            	try {
            		monTraiteur.join();
            	}
            	catch(Exception ex) {
        		  ex.printStackTrace();
            	}
            }
     
    		 // Terminer le main
    		 System.out.println("\nContenu de Synchronizer :");
    		 System.out.print(synchronizer.toString());	 
    		 System.out.println("\n\nFin du programme PocVerrou");
    		 System.out.println(new Date() );
    	}
     
    }// class Traiteur
    Dans une seconde version (que je n'ai pas sous la main, j'ai supprimé la seconde section synchronisée. Dans la première (sur la hashtable), j'écris l'objet EnreSynchro ou je le lis et j'incrémente le compteur. La seconde (sur getVerrou() ) reste la même.

  6. #6
    Membre Expert
    Inscrit en
    Mai 2006
    Messages
    1 364
    Détails du profil
    Informations forums :
    Inscription : Mai 2006
    Messages : 1 364
    Par défaut
    D'apres ce que je comprends, le probleme est résolu. Néanmoins, une petite remarque.

    Dans ce code, il y a :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    Integer eCompteur = existant.getCompteur();
    Ca, c'est pas bien. En effet, tu accedes à une ressource protegée sans bloc synchro. De plus, tu utilises setCompteur en etant protégé par eCompteur ou eVerrou. Autrement dit, 2 threads peuvent acceder au compteur en meme temps...

    J'ajouterais qu'a chaque fois que tu fais setCompteur, tu remplaces l'objet compteur donc l'instance sur laquelle tu synchronise les acces. A verifier que c'est bien le comportement voulu.

+ Répondre à la discussion
Cette discussion est résolue.

Discussions similaires

  1. Réponses: 4
    Dernier message: 23/05/2013, 00h07
  2. Réponses: 1
    Dernier message: 05/01/2010, 22h33
  3. Filtrer une liste sur les valeurs d'une colonne
    Par julien.63 dans le forum SharePoint
    Réponses: 3
    Dernier message: 13/02/2009, 08h43
  4. Réponses: 6
    Dernier message: 04/11/2008, 22h35
  5. Réponses: 2
    Dernier message: 24/10/2008, 08h04

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