58 votes

Pourquoi la mutabilité partagée est-elle mauvaise?

Je regardais une présentation sur l'île de Java, et à un point, le conférencier a dit:

"La mutabilité est OK, le partage, c'est sympa, partagé la mutabilité est le travail du diable."

Ce qu'il faisait référence à est le morceau de code suivant, qu'il considérait comme une "très mauvaise habitude":

//double the even values and put that into a list.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 1, 2, 3, 4, 5);
List<Integer> doubleOfEven = new ArrayList<>();

numbers.stream()
       .filter(e -> e % 2 == 0)
       .map(e -> e * 2)
       .forEach(e -> doubleOfEven.add(e));

Il a ensuite écrit le code qui doit être utilisé, qui est:

List<Integer> doubleOfEven2 =
      numbers.stream()
             .filter(e -> e % 2 == 0)
             .map(e -> e * 2)
             .collect(toList());

Je ne comprends pas pourquoi le premier morceau de code est "mauvaise habitude". Pour moi, ils ont tous deux poursuivent le même but.

45voto

Aomine Points 42709

Explication pour le premier exemple d'extrait de

Le problème vient en jeu lors de l'exécution d'un traitement parallèle.

//double the even values and put that into a list.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 1, 2, 3, 4, 5);
List<Integer> doubleOfEven = new ArrayList<>();

numbers.stream()
       .filter(e -> e % 2 == 0)
       .map(e -> e * 2)
       .forEach(e -> doubleOfEven.add(e)); // <--- Unnecessary use of side-effects!

Cette inutilement utilise des effets secondaires , bien que pas tous les effets secondaires sont mauvais, s'ils sont utilisés correctement lors de l'utilisation de flux, on doit fournir un comportement qui est sûr pour exécuter simultanément sur différents morceaux de l'entrée. c'est à dire l'écriture de code qui n'a pas accès partagé mutable données pour faire son travail.

La ligne:

.forEach(e -> doubleOfEven.add(e)); // Unnecessary use of side-effects!

utilise inutilement les effets secondaires et lorsqu'il est exécuté en parallèle, la non-thread-safety de l' ArrayList entraînerait des résultats incorrects.

Un temps, j'ai lu un blog par Henrik Eichenhardt répondre à pourquoi partagé mutable état est la racine de tous les maux.

C'est un court raisonnement pour expliquer pourquoi partagé la mutabilité est pas bonne; extrait du blog.

le non-déterminisme = traitement parallèle + mutable état

Cette équation signifie que à la fois le traitement parallèle et mutable état de résultat combiné de non-déterministe, le comportement du programme. Si vous venez de faire le traitement en parallèle et ont seulement état immuable tout est beau et il est facile de raisonner sur les programmes. Sur le d'autre part, si vous voulez faire un traitement en parallèle avec les données mutable vous besoin de synchroniser l'accès aux variables qui mutable rend essentiellement ces sections du programme mono-thread. Ce n'est pas vraiment nouveau, mais je n'ai pas vu ce concept exprime de façon élégante. Un non-déterministe programme est cassé.

Ce blog va dériver de l'intérieur les détails du pourquoi de programmes parallèles sans synchronisation appropriée sont brisés, que vous pouvez trouver dans le lien ajouté.

Explication le deuxième exemple de l'extrait de

List<Integer> doubleOfEven2 =
      numbers.stream()
             .filter(e -> e % 2 == 0)
             .map(e -> e * 2)
             .collect(toList()); // No side-effects! 

Il utilise une collection de réduction de l' opération sur les éléments de ce flux en utilisant un Collector.

Ce est beaucoup plus sûr, plus efficaceet plus propice à la parallélisation.

16voto

Eugene Points 6271

Le truc, c'est que le discours est un peu mal en même temps. L'exemple qu'il a fourni les utilisations forEach, ce qui est documenté comme:

Le comportement de cette opération est explicitement non déterministe. En parallèle stream, cette opération n'est pas garantie à l'égard de la rencontre de l'ordre du flux, afin d'éviter de sacrifier au profit de parallélisme...

Vous pouvez utiliser:

 numbers.stream()
            .filter(e -> e % 2 == 0)
            .map(e -> e * 2)
            .parallel()
            .forEachOrdered(e -> doubleOfEven.add(e));

Et vous auriez toujours le même résultat garanti.

D'autre part l'exemple qui utilise Collectors.toList est mieux, parce que les Collectionneurs égard encounter order, de sorte qu'il fonctionne très bien.

Point intéressant, Collectors.toList utilise ArrayList en dessous, c'est pas un thread safe collection. C'est juste qu'il utilise beaucoup d'entre eux (pour le traitement en parallèle) et se confond à la fin.

Une dernière remarque en parallèle et séquentielle n'influence pas la rencontre de l'ordre, c'est l'opération appliquée à l' Stream qui ne. Excellente lecture ici.

Nous avons également besoin de penser que, même à l'aide d'un thread-safe collection est toujours pas à l'abri des courants entièrement, en particulier lorsque vous êtes en s'appuyant sur side-effects.

 List<Integer> numbers = Arrays.asList(1, 3, 3, 5);
    Set<Integer> seen = Collections.synchronizedSet(new HashSet<>());
    List<Integer> collected = numbers.stream()
            .parallel()
            .map(e -> {
                if (seen.add(e)) {
                    return 0;
                } else {
                    return e;
                }
            })
            .collect(Collectors.toList());

    System.out.println(collected);

collected à ce stade pourrait être [0,3,0,0] OU [0,0,3,0] ou quelque chose d'autre.

6voto

gnasher729 Points 5011

Supposons deux threads effectuer cette tâche dans le même temps, le deuxième fil d'une instruction derrière la première.

Le premier thread crée doubleOfEven. Le deuxième thread crée doubleOfEven, l'instance créée par le premier thread sera nettoyée. Puis les deux fils va ajouter le double de tous les nombres pairs de doubleOfEvent, de sorte qu'il contiendra 0, 0, 4, 4, 8, 8, 12, 12, ... au lieu de 0, 4, 8, 12... (En réalité, ces fils ne sera pas parfaitement synchronisé, donc tout ce qui peut aller mal ira mal).

Pas que la deuxième solution est beaucoup mieux. Vous disposez de deux fils de définir la même mondial. Dans ce cas, elles se situent à la fois logiquement des valeurs égales, mais si ils l'ont mis pour deux valeurs différentes, alors que vous ne connaissez pas la valeur que vous avez par la suite. Un thread sera pas obtenir le résultat qu'il veut.

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