2009-10-12 11 views
3

Exemple de scénario:problème avec des collections synchronisées de Java lorsque vous faites égaux() dans l'ordre inverse de plusieurs threads

  • Créer deux SynchronizedSets (s1 et s2)
  • les passer à deux fils (T1 et T2)
  • Démarrer les fils

exécution de T1(): temps (toujours) s1.equals (S2)

de course de T2(): while (pour toujours) s2.equals (s1)

Qu'est-ce qui se passe? - les égaux de SynchronizedSet acquiert verrou sur lui-même

  • Il calcule la longueur du param qui est passé et aussi ce qu'il contient pour déterminer si c'est égal [Note: ceci est une estimation basée sur les journaux que j'analysé] Si le paramètre transmis est aussi un SynchronizedSet, les appels à size() et containAll() impliquent un verrou de ce qui doit également être acquis.

  • Dans l'exemple ci-dessus, blocage de l'acquisition des commandes de T1 et T2 sont les suivantes:

    T1: s1 -> s2 T2: s2 -> s1

Ofc, elle conduit à une impasse.

Ce problème n'est pas spécifique aux collections synchronisées uniquement. Cela peut arriver même avec Hashtable ou Vector. Je crois qu'il s'agit d'une limitation Java API (conception). Comment surmonter cela? Comment puis-je m'assurer que cela ne se produise pas dans ma demande? Y a-t-il un principe de conception que je devrais suivre sans entrer dans cette situation?

Répondre

2

Vous pouvez verrouiller les deux ensembles dans le même ordre dans chaque thread:

  synchronized(s1) { 
       synchronized(s2) { 
        s1.equals(s2); 
       } 
      } 

et

  synchronized(s1) { 
       synchronized(s2) { 
        s2.equals(s1); 
       } 
      } 
2

Je peut suggérer d'utiliser un bloc synchronisé() {].

quelque chose comme ceci:

while(forever){ 
    synchronized(s1){ 
     s1.equals(s2); 
    } 
} 

et

while(forever){ 
    synchronized(s1){ 
    s2.equals(s1); 
    } 
} 
4

Je crois que c'est une limitation de l'API Java (conception).

Je crois que vous avez tort.Un exigence fondamentale de chaque schéma de verrouillage de niveau PL que j'ai jamais utilisé est que les threads doivent verrouiller les ressources dans le même ordre ou risque de blocage. Cela s'applique également aux bases de données.

En fait, la seule façon dont je pense que vous pourriez éviter ce serait soit:

  • exigent l'application d'acquérir toutes les serrures dont il avait besoin en une seule opération atomique ou
  • font tout verrouillage en utilisant un seul verrou global.

Ces deux approches sont peu pratiques et non-scalables.

Comment remédier à cela?

Codez votre application de sorte que les verrous soient acquis par tous les threads dans le même ordre. @ Les réponses de Maurice et @ Nettogrof donnent des exemples de la façon de procéder, bien que cela puisse être plus difficile si vous avez beaucoup d'ensembles à vous inquiéter.

1

Stephen C est une bonne chose. Informations complémentaires: Si vous ne savez pas dans quel sens autour des ensembles sont, vous pouvez utiliser un verrou « global » à chaque fois que les deux ensembles vont être comparés:

private static final Object lock = new Object(); // May be context-local. 

[...] 

    synchronized (lock) { 
     synchronized (s1) { 
      synchronized (s2) { 
       return s1.equals(s2); 
      } 
      } 
    } 

Si les jeux sont susceptibles d'être soutenu, vous peut plus de l'ordre de temps par le code de hachage d'identité et retombez au verrouillage:

int h1 = System.identityHashCode(s1); 
    int h2 = System.identityHashCode(s2); 
    return 
     h1<h2 ? lockFirstEquals(h1, h2) : 
     h2<h1 ? lockFirstEquals(h2, h1) : 
     globalLockEquals(h1, h2); 

Parfois, vous pouvez utiliser un autre algorithme. IIRC, StringBuffer peut bloquer avec append (bien que la combinaison d'opérations sur les objets n'a pas beaucoup de sens). Il pourrait être mis en œuvre:

public StringBuffer append(StringBuffer other) { 
    if (other == null) { 
     return append("null"); 
    } 
    int thisHash = System.identityHashCode(this); 
    int otherHash = System.identityHashCode(other); 
    if (thisHash < otherHash) { 
     synchronized (this) { 
      synchronized (other) { 
       appendImpl(other); 
      } 
     } 
    } else if (otherHash < thisHash) { 
     synchronized (other) { 
      synchronized (this) { 
       appendImpl(other); 
      } 
     } 
    } else { 
     append(other.toString()); // Or append((Object)other); 
    } 
    return this; 
} 

La meilleure solution serait sans doute de changer votre stratégie de filetage de sorte que vous ne avez pas besoin de verrouillage ici.

+0

@Tom: mais notez que vous pouvez toujours avoir des ennuis avec un verrou maître si vous n'acquérez pas les verrous subsidiaires en même temps. Par exemple, un blocage est possible si un thread qui détient déjà le verrou 's2' exécute le premier de vos extraits de code ... –

+0

@Tom: en fait, je pense que votre premier extrait manquait de mon point. J'essayais de dire que l'un des moyens de garantir qu'il n'y a pas d'interblocage est d'utiliser un et un seul verrou pour toutes les structures de données. Bien sûr, c'est impraticable parce que (pour commencer) ça ne va pas. –

+0

Si un thread détient déjà un verrou et appelle un code aléatoire, il est quand même en difficulté. Je suppose que vous pourriez le coder si vous utilisiez 'java.util.concurrent.locks', mais j'essaierais de coder quelque chose de beaucoup plus sensé. –

1

Vous pouvez commander les postes par leur identiyHashCode() avant de faire l'appel equals(). De cette façon, l'ordre d'acquisition des serrures sera toujours le même.

+0

@ La solution de Tom fait allusion au problème avec votre solution. Il existe une probabilité faible mais limitée que les deux ensembles auront la même valeur de code d'identification. –

+0

En raison de l'ensemble limité de valeurs de hachage que vous obtenez réellement, le "paradoxe d'anniversaire", faisant l'opération plusieurs fois dans une course et s'exécutant sur beaucoup de machines, cela arrivera assez souvent. Même si vous ne le remarquez pas, tous les incidents d'interblocage potentiels ne sont pas bloquants. –

+0

Donc, dangereusement faux ... –