web-dev-qa-db-fra.com

Synchronisation sur une valeur entière

Duplicata possible:
Quelle est la meilleure façon d'augmenter le nombre de verrous en Java

Supposons que je veuille verrouiller en fonction d'une valeur d'ID entier. Dans ce cas, il existe une fonction qui extrait une valeur d'un cache et effectue une récupération/stockage assez coûteuse dans le cache si la valeur n'est pas là.

Le code existant n'est pas synchronisé et pourrait potentiellement déclencher plusieurs opérations de récupération/stockage:

//psuedocode
public Page getPage (Integer id){
   Page p = cache.get(id);
   if (p==null)
   {
      p=getFromDataBase(id);
      cache.store(p);
   }
}

Ce que je voudrais faire, c'est synchroniser la récupération sur l'id, par ex.

   if (p==null)
   {
       synchronized (id)
       {
        ..retrieve, store
       }
   }

Malheureusement, cela ne fonctionnera pas car 2 appels distincts peuvent avoir la même valeur d'ID entier mais un objet Integer différent, donc ils ne partageront pas le verrou, et aucune synchronisation ne se produira.

Existe-t-il un moyen simple de s'assurer que vous disposez de la même instance Integer? Par exemple, cela fonctionnera-t-il:

 syncrhonized (Integer.valueOf(id.intValue())){

Le javadoc pour Integer.valueOf () semble impliquer que vous obtiendrez probablement la même instance, mais cela ne ressemble pas à une garantie:

Renvoie une instance de type Integer représentant la valeur int spécifiée. Si une nouvelle instance Integer n'est pas requise, cette méthode doit généralement être utilisée de préférence au constructeur Integer (int), car cette méthode est susceptible de produire des performances d'espace et de temps nettement meilleures en mettant en cache les valeurs fréquemment demandées.

Donc, toutes les suggestions sur la façon d'obtenir une instance Integer qui est garantie d'être la même, à part les solutions plus élaborées comme garder un WeakHashMap of Lock objets clavetés à l'int? (rien de mal à cela, il semble juste qu'il doit y avoir un one-liner évident que je manque).

32
Steve B.

Vous ne voulez vraiment pas synchroniser sur un Integer, car vous n'avez pas le contrôle sur les instances identiques et les instances différentes. Java ne fournit tout simplement pas une telle fonctionnalité (sauf si vous utilisez des nombres entiers dans une petite plage) qui est fiable sur différentes machines virtuelles Java. Si vous devez vraiment synchroniser sur un nombre entier, alors vous devez conservez une carte ou un ensemble de nombres entiers afin de garantir que vous obtenez l'instance exacte que vous souhaitez.

Il serait préférable de créer un nouvel objet, peut-être stocké dans un HashMap qui est saisi par le Integer, pour se synchroniser. Quelque chose comme ça:

public Page getPage(Integer id) {
  Page p = cache.get(id);
  if (p == null) {
    synchronized (getCacheSyncObject(id)) {
      p = getFromDataBase(id);
      cache.store(p);
    }
  }
}

private ConcurrentMap<Integer, Integer> locks = new ConcurrentHashMap<Integer, Integer>();

private Object getCacheSyncObject(final Integer id) {
  locks.putIfAbsent(id, id);
  return locks.get(id);
}

Pour expliquer ce code, il utilise ConcurrentMap, ce qui permet d'utiliser putIfAbsent. Vous pouvez faire ceci:

  locks.putIfAbsent(id, new Object());

mais alors vous encourez le (petit) coût de création d'un objet pour chaque accès. Pour éviter cela, je viens d'enregistrer l'entier lui-même dans le Map. Qu'est-ce que cela permet? Pourquoi est-ce différent de l'utilisation de l'entier lui-même?

Lorsque vous effectuez une get() à partir d'une Map, les touches sont comparées à equals() (ou au moins la méthode utilisée est l'équivalent de l'utilisation de equals()). Deux instances entières différentes de la même valeur seront égales l'une à l'autre. Ainsi, vous pouvez passer n'importe quel nombre d'instances Integer différentes de "new Integer(5)" comme paramètre à getCacheSyncObject et vous ne récupérerez toujours que la toute première instance qui a été transmise et qui contenait cette valeur .

Il y a des raisons pour lesquelles vous ne voudrez peut-être pas synchroniser sur Integer ... vous pouvez entrer dans des blocages si plusieurs threads se synchronisent sur des objets Integer et utilisent donc involontairement les mêmes verrous quand ils veulent utilisez différents verrous. Vous pouvez résoudre ce risque en utilisant le

  locks.putIfAbsent(id, new Object());

et donc engendrer un (très) petit coût pour chaque accès au cache. En faisant cela, vous garantissez que cette classe effectuera sa synchronisation sur un objet sur lequel aucune autre classe ne se synchronisera. Toujours une bonne chose.

50
Eddie

Utilisez une carte thread-safe, telle que ConcurrentHashMap. Cela vous permettra de manipuler une carte en toute sécurité, mais utilisez un verrou différent pour effectuer le vrai calcul. De cette façon, vous pouvez avoir plusieurs calculs exécutés simultanément avec une seule carte.

Utilisation ConcurrentMap.putIfAbsent, mais au lieu de placer la valeur réelle, utilisez plutôt un Future avec une construction légère. Peut-être l'implémentation FutureTask. Exécutez le calcul, puis get le résultat, qui bloquera le thread en toute sécurité jusqu'à ce qu'il soit terminé.

4

Integer.valueOf() renvoie uniquement les instances mises en cache pour une plage limitée. Vous n'avez pas spécifié votre plage, mais en général, cela ne fonctionnera pas.

Cependant, je vous recommande fortement de ne pas adopter cette approche, même si vos valeurs sont dans la plage correcte. Étant donné que ces instances Integer mises en cache sont disponibles pour tout code, vous ne pouvez pas contrôler complètement la synchronisation, ce qui pourrait entraîner un blocage. C'est le même problème que les gens ont essayé de verrouiller sur le résultat de String.intern().

Le meilleur verrou est une variable privée. Étant donné que seul votre code peut le référencer, vous pouvez garantir qu'aucun blocage ne se produira.

Soit dit en passant, l'utilisation d'un WeakHashMap ne fonctionnera pas non plus. Si l'instance servant de clé n'est pas référencée, elle sera récupérée. Et s'il est fortement référencé, vous pouvez l'utiliser directement.

4
erickson

L'utilisation synchronisée sur un nombre entier semble vraiment mauvaise par conception.

Si vous devez synchroniser chaque élément individuellement uniquement pendant la récupération/stockage, vous pouvez créer un ensemble et y stocker les éléments actuellement verrouillés. En d'autres mots,

// this contains only those IDs that are currently locked, that is, this
// will contain only very few IDs most of the time
Set<Integer> activeIds = ...

Object retrieve(Integer id) {
    // acquire "lock" on item #id
    synchronized(activeIds) {
        while(activeIds.contains(id)) {
            try { 
                activeIds.wait();   
            } catch(InterruptedExcption e){...}
        }
        activeIds.add(id);
    }
    try {

        // do the retrieve here...

        return value;

    } finally {
        // release lock on item #id
        synchronized(activeIds) { 
            activeIds.remove(id); 
            activeIds.notifyAll(); 
        }   
    }   
}

Il en va de même pour le magasin.

L'essentiel est: il n'y a pas une seule ligne de code qui résout ce problème exactement comme vous en avez besoin.

3
Antonio

Vous pouvez jeter un œil à ce code pour créer un mutex à partir d'un ID. Le code a été écrit pour les ID de chaîne, mais peut facilement être modifié pour les objets Integer.

1
McDowell

Steve,

votre code proposé a un tas de problèmes de synchronisation. (Antonio le fait aussi).

Résumer:

  1. Vous devez mettre en cache un objet coûteux.
  2. Vous devez vous assurer que pendant qu'un thread effectue la récupération, un autre thread n'essaie pas également de récupérer le même objet.
  3. Pour les n-threads qui tentent tous d'obtenir l'objet, un seul objet est récupéré et renvoyé.
  4. Cela pour les threads demandant différents objets qu'ils ne se disputent pas.

pseudo-code pour y arriver (en utilisant un ConcurrentHashMap comme cache):

ConcurrentMap<Integer, Java.util.concurrent.Future<Page>> cache = new ConcurrentHashMap<Integer, Java.util.concurrent.Future<Page>>;

public Page getPage(Integer id) {
    Future<Page> myFuture = new Future<Page>();
    cache.putIfAbsent(id, myFuture);
    Future<Page> actualFuture = cache.get(id);
    if ( actualFuture == myFuture ) {
        // I am the first w00t!
        Page page = getFromDataBase(id);
        myFuture.set(page);
    }
    return actualFuture.get();
}

Remarque:

  1. Java.util.concurrent.Future est une interface
  2. Java.util.concurrent.Future n'a pas réellement de set () mais regardez les classes existantes qui implémentent Future pour comprendre comment implémenter votre propre Future (ou utilisez FutureTask)
  3. Pousser la récupération réelle vers un thread de travail sera presque certainement une bonne idée.
1
Pat

Voir la section 5.6 dans Java Concurrency in Practice : "Construire un cache de résultats efficace et évolutif". Il traite du problème exact que vous essayez de résoudre. En particulier, consultez le modèle de mémorisation.

alt text
(source: md.ed )

1
Julien Chastang

Comme vous pouvez le voir dans la variété des réponses, il existe différentes façons de dépouiller ce chat:

  • L'approche de Goetz et al de garder un cache de FutureTasks fonctionne très bien dans des situations comme celle-ci où vous "cachez quelque chose de toute façon" alors ne vous occupez pas de construire une carte des objets FutureTask (et si cela vous dérangeait la croissance de la carte, au moins il est facile de l'élaguer simultanément)
  • En tant que réponse générale à "comment verrouiller l'ID", l'approche décrite par Antonio présente l'avantage d'être évidente lorsque la carte des verrous est ajoutée à/supprimée.

Vous devrez peut-être faire attention à un problème potentiel avec implémentation d'Antonio, à savoir que notifyAll () réveillera les threads en attente tous ID lorsque n = d'entre eux devient disponible, ce qui peut ne pas évoluer très bien en cas de forte contention. En principe, je pense que vous pouvez résoudre ce problème en ayant un objet Condition pour chaque ID actuellement verrouillé, qui est alors la chose que vous attendez/signalez. Bien sûr, si dans la pratique, il y a rarement plus d'une ID en attente à un moment donné, ce n'est pas un problème.

1
Neil Coffey

Que diriez-vous d'un ConcurrentHashMap avec les objets entiers comme clés?

1
starblue