13 votes

Sécurité thread Java de la liste

J'ai une liste, qui doit être utilisée soit dans un contexte thread-safe, soit dans un contexte non thread-safe. Il est impossible de déterminer à l'avance lequel des deux sera utilisé.

Dans ce cas particulier, chaque fois que la liste entre dans un contexte non threadé, je l'enveloppe en utilisant la fonction

Collections.synchronizedList(...)

Mais je ne veux pas l'envelopper, si elle n'entre pas dans un contexte non-thread-safe. Par exemple, parce que la liste est énorme et utilisée de manière intensive.

J'ai lu à propos de Java que sa politique d'optimisation est stricte en ce qui concerne le multithreading - si vous ne synchronisez pas votre code correctement, il n'est pas garanti qu'il sera exécuté correctement dans le contexte inter-thread - il peut réorganiser le code de manière significative, en assurant la cohérence dans le contexte d'un seul thread (cf. http://java.sun.com/docs/books/jls/third_edition/html/memory.html#17.3 ). F.e.,

op1 ; op2 ; op3 ;

peut être réorganisé en

op3 ; op2 ; op1 ;

s'il produit le même résultat (dans le contexte d'un thread).

Maintenant je me demande, si je

  1. remplir ma liste avant de l'envelopper par synchronizedList,

  2. puis l'emballer,

  3. puis utilisation par un autre fil

Est-ce qu'il y a une possibilité que cette liste ne soit remplie que partiellement ou pas du tout dans un autre fil ? La JVM pourrait-elle reporter (1) après (3) ? Existe-t-il une méthode correcte et rapide Comment faire en sorte qu'une (grande) liste non sécurisée par un thread devienne sécurisée par un thread ?

12voto

Paŭlo Ebermann Points 35526

Lorsque vous donnez votre liste à un autre thread par des moyens sûrs (par exemple en utilisant un bloc synchronisé, une variable volatile ou un AtomicReference ), il est garanti que le second thread voit la liste entière dans l'état où elle se trouvait lors du transfert (ou tout état ultérieur, mais pas un état antérieur).

Si vous ne le modifiez pas par la suite, vous n'avez pas non plus besoin de votre liste synchronisée.


Edit (après quelques commentaires, pour étayer mes dires) :

Je suppose ce qui suit :

  • nous avons une variable volatile list .

    volatile List<String> list = null;
  • Fil A :

    1. crée une liste L et remplit L d'éléments.
    2. fixe list pour pointer vers L (ce qui signifie écrit L à list )
    3. n'effectue aucune autre modification sur L.

    Source de l'échantillon :

    public void threadA() {
       List<String> L = new ArrayList<String>();
       L.add("Hello");
       L.add("World");
       list = l;
    }
  • Fil B :

    1. lit K à partir de list
    2. itère sur K, en imprimant les éléments.

    Source de l'échantillon :

    public void threadB() {
         List<String> K = list;
         for(String s : K) {
             System.out.println(s);
         }
    }
  • Tous les autres fils ne touchent pas la liste.

Maintenant, nous avons ceci :

  • Les actions 1-A et 2-A du fil A sont ordonnées par ordre de programme donc 1 vient avant 2.

  • Les actions 1-B et 2-B dans le fil B sont ordonnées par ordre de programme donc 1 vient avant 2.

  • L'action 2-A dans le fil A et l'action 1-B dans le fil sont ordonnées par ordre de synchronisation donc 2-A vient avant 1-B, puisque

    Une écriture dans une variable volatile (§8.3.1.4) v se synchronise avec toutes les lectures ultérieures de v par n'importe quel thread (où suivant est défini selon l'ordre de synchronisation).

  • El se passe avant -L'ordre est la fermeture transitive des ordres de programme des threads individuels et de l'ordre de synchronisation. Nous avons donc :

    1-A se produit avant 2-A se produit avant 1-B se produit avant 2-B

    et donc 1-A se produit avant 2-B.

  • Enfin,

    Si une action se produit avant une autre, alors la première est visible et ordonnée avant la seconde.

Ainsi, notre fil itératif peut vraiment voir toute la liste, et pas seulement certaines parties de celle-ci. Ainsi, transmettre la liste avec une seule variable volatile est suffisant, et nous n'avons pas besoin de synchronisation dans ce cas simple.


Une dernière modification (ici, puisque j'ai plus de liberté de formatage que dans les commentaires) concernant l'ordre des programmes du fil A. (J'ai également ajouté un exemple de code ci-dessus).

A partir du JLS (section ordre de programme ) :

Parmi toutes les actions inter-filières effectuées par chaque fil t, l'ordre du programme de t est un ordre total qui reflète l'ordre dans lequel ces actions seraient exécutées selon la sémantique intra-fil de t.

Alors, quels sont les sémantique intra-fil du fil A ?

Quelques paragraphes ci-dessus :

Le modèle de mémoire détermine quelles valeurs peuvent être lues à chaque point du programme. Les actions de chaque thread isolément doivent se comporter de la manière gouvernée par la sémantique de ce thread, à l'exception du fait que les valeurs vues par chaque lecture sont déterminées par le modèle de mémoire. Lorsque nous faisons référence à cela, nous disons que le programme obéit à sémantique intra-fil . La sémantique intra-fil est la sémantique des programmes à un seul fil. programmes monofilaires, et permettent la prédiction complète du comportement d'un fil. un thread basé sur les valeurs vues par les actions de lecture au sein du thread. Pour déterminer si les actions du thread t dans une exécution sont légales, nous évaluons simplement la l'implémentation du thread t telle qu'elle serait exécutée dans un contexte de thread unique, comme défini dans le reste de cette spécification.

Le reste de la présente spécification comprend section 14.2 (Blocs) :

Un bloc est exécuté en exécutant chacune des déclarations de variables locales et les autres instructions dans l'ordre de la première à la dernière (de gauche à droite).

Donc, le ordre de programme est en effet l'ordre dans lequel les déclarations/expressions sont données dans le code source du programme.

Ainsi, dans notre exemple de source, les actions mémoire créer une nouvelle ArrayList , ajouter "Bonjour" , Ajouter "Monde". et affecter à list (les trois premières consistent en un plus grand nombre de sous-actions) sont en effet en cette commande de programme .

(La VM ne doit pas nécessairement exécuter les actions dans cet ordre, mais ce ordre de programme contribue toujours à la se passe avant ordre, et donc à la visibilité pour les autres fils).

4voto

biziclop Points 21446

Si vous remplissez votre liste et que vous l'enroulez ensuite dans le même fil, vous serez en sécurité.

Cependant, il y a plusieurs choses à prendre en compte :

  1. Collections.synchronizedList() ne vous garantit qu'une sécurité de bas niveau pour les threads. Les opérations complexes, comme if ( !list.contains( elem ) ) list.add( elem ); auront toujours besoin d'un code de synchronisation personnalisé.
  2. Même cette garantie est nulle si un fil peut obtenir une référence à la liste originale. Faites en sorte que cela ne se produise pas.
  3. Commencez par vous occuper de la fonctionnalité, puis vous pourrez commencer à vous inquiéter de la lenteur de la synchronisation. J'ai très rarement rencontré du code où la vitesse de la synchronisation Java était un facteur important.

Mise à jour : J'aimerais ajouter quelques extraits du JLS pour espérer clarifier un peu les choses.

Si x et y sont des actions du même fil et que x vient avant y dans l'ordre du programme, alors hb(x, y).

C'est pourquoi remplir la liste et l'envelopper ensuite dans le même fil est une option sûre. Mais plus important encore :

Il s'agit d'une garantie extrêmement forte pour les programmeurs. Les programmeurs n'ont pas besoin de raisonner sur les réordonnances pour déterminer que leur code contient des courses de données. Par conséquent, ils n'ont pas besoin de raisonner sur les réorganisations pour déterminer si leur code est correctement synchronisé. Une fois qu'il a déterminé que le code est correctement synchronisé, le programmeur n'a pas besoin de s'inquiéter que les réarrangements affectent son code.

Le message est clair : assurez-vous que votre programme, exécuté dans l'ordre dans lequel vous avez écrit votre code, ne contient pas de courses de données, et ne vous préoccupez pas du réordonnancement.

2voto

willcodejavaforfood Points 20365

Si les traversées se produisent plus souvent qu'à l'écrit, j'examinerais les possibilités suivantes CopyOnWriteArrayList .

Une variante thread-safe de ArrayList en dans laquelle toutes les opérations mutatives (add, set, et ainsi de suite) sont implémentées en en faisant une nouvelle copie du tableau sous-jacent sous-jacent.

0voto

pnt Points 1314

Regardez comment AtomicInteger (et autres) sont implémentés pour être thread safe et non synchronisés. Le mécanisme n'introduit pas de synchronisation, mais si une synchronisation est nécessaire, il la gère de manière élégante.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X