Principes de base du traitement parallèle Java

introduction

Avec l'introduction de l'API de streaming dans la norme Java8, il est devenu un environnement où la programmation de traitement parallèle, qui a tendance à être compliquée, devient de plus en plus facile. Je ressens aussi beaucoup cet avantage. Alors que ces méthodes de codage de traitement parallèle deviennent de plus en plus puissantes, malheureusement cette fois, je voudrais écrire un examen des bases, pas de la dernière technologie. Si vous le connaissez, veuillez le lire. Si vous créez une application Android mais que vous n'êtes pas familier avec le traitement parallèle, je l'ai écrite dans l'espoir que vous la lirez attentivement.

Fil sûr

Soudainement, est-il acceptable que les classes suivantes soient accessibles par plusieurs threads?

public class SomeSequence {

    private int value;

    public int getNextValue() {
        return value++;
    }
}

La réponse est non. Dans un environnement à un seul thread, il n'y a pas de problème particulier. Cependant, dans le cas d'un environnement multi-thread, nous ne pouvons pas garantir qu'il fonctionnera correctement. En supposant que l'incrémentation de «value ++» n'est pas une seule opération, mais «value = 1»

  1. Lisez la valeur actuelle et elle sera retournée (tmp = valeur)
  2. 1 est ajouté à la valeur de retour (tmp + 1)
  3. Ecrivez une nouvelle valeur (valeur = tmp) Je fais quelque chose comme ça. Par exemple, s'il y a un thread A et un thread B, un thread appelle getNextValue, et le thread B appelle également getNextValue à un moment malchanceux (par exemple, un thread traite 1-2), value est défini. La même valeur incrémentée est renvoyée. Le résultat attendu du thread B est d'incrémenter la valeur au thread A, ce n'est donc plus une méthode qui renvoie le résultat attendu. Ce phénomène est appelé ** condition de conflit ** (condition de race). Dans ce cas, les spécifications ne supposent pas que les valeurs seront dupliquées, et la situation d'une pénurie continuera pendant longtemps, et ce sera un triste état.

La seule façon de résoudre ce problème est de se synchroniser.

public class SomeSequence {

    private int value;

    public synchronized int getNextValue() {
        return value++;
    }
}

Si vous utilisez la méthode synchronized pour getNextValue (), le problème ci-dessus se produira. En termes simples, avec synchronized, cette méthode ne peut exécuter qu'un seul thread à la fois. Même si les threads A et B accèdent à getNextValue, le thread B ne peut pas accéder aux méthodes de la classe jusqu'à ce que le thread A quitte le traitement. En conséquence, la valeur peut être mise à jour dans un état normal. Cette classe garantit qu'elle se comporte correctement même lorsqu'elle est accédée par plusieurs threads. Cela s'appelle ** thread safe **. Ensuite, je pense que vous devriez ajouter synchronized à tous. Cependant, il y a quelques écueils. J'expliquerai cela plus tard.

Atomique

J'ai une opération comme «value ++» dans l'exemple de code ci-dessus, mais j'ai trouvé que si ce n'est pas thread-safe, il n'est pas garanti de fonctionner correctement lorsqu'il est appelé à partir de plusieurs threads. Cette opération doit être ** atomique ** pour rendre ce processus thread-safe sans provoquer de conditions de conflit. Atomic est appelé une opération indivisible, et cela signifie qu'elle peut être exécutée une fois que l'opération de modification en cours est sûrement terminée sans interrompre les autres threads. Les opérations telles que valeur ++ sont appelées des opérations ** lecture-modification-écriture ** (lecture-modification-écriture), mais elles sont susceptibles de provoquer des conflits et doivent être effectuées de manière atomique.

De plus, l'initialisation retardée suivante est également appelée ** check-then-act et est susceptible de provoquer un état conflictuel. En outre, l'état dans lequel plusieurs éléments tels que diriger, modifier, écrire, vérifier, zen et agir sont combinés est appelé ** action composée **.

public class AppComponent {
    
    private AppComponent instance = null;
    
    public AppComponent getInstance() {
        if (instance == null) {
            instance = new Instance();
        }
        return instance;
    }
}

Comme mentionné précédemment, si le thread A et le thread B exécutent getInstance, à des moments malchanceux, ils seront exécutés avant d'être définis dans le champ d'instance et seront exécutés séparément sur deux appelsgetInstance. Vous recevrez l'objet. Ceci est également appelé ** observation obsolète ** (ancienne valeur juste avant la mise à jour). Pour une exécution atomique, considérez ** Lock **, qui est le mécanisme de base de Java. Vous pouvez également assurer la sécurité des threads en utilisant la * classe de variable atomique * existante, comme indiqué ci-dessous. Toutes les actions qui accèdent à getNextValue seront définitivement atomiques.

public class SomeSequence {

    private AtomicInteger value = new AtomicInteger(0);

    public int getNextValue() {
        return value.getAndIncrement();
    }
}

Fermer à clé

Java a un mécanisme pour forcer l'atomicité en fonction du langage lui-même. C'est le "synchronisé" que j'ai écrit plus tôt. synchronized a deux rôles. ** Verrouiller ** et ** Bloquer **. Dans la syntaxe ci-dessous, le "this" transmis "synchronized" est une référence à un objet qui agit comme un verrou (clé), et un bloc de code protégé par ce verrou. Dans l'exemple de code ci-dessus, il est appelé une méthode synchronisée (méthode synchronisée) avec «synchronized» devant le nom de la méthode, et ce qui suit est appelé un bloc synchronisé (bloc synchronisé). L'exemple verrouille tout le code, il a donc la même signification que la méthode synchronisée ci-dessus.

public class SomeSequence {

    private int value;

    public int getNextValue() {
        synchronized(this) {
            return value++;
        }
    }
}

Tous les objets Java (instances singleton et toutes les classes) agissent comme des verrous pour la synchronisation et sont pris en charge par le langage. C'est ce qu'on appelle un ** verrou unique ** (verrouillage du moniteur). Seul le thread qui peut acquérir le verrou peut exécuter le bloc de synchronisation (méthode) et libérer le verrou lorsque le bloc de synchronisation est terminé (même si une exception est levée). Si le thread A a acquis le verrou, le thread B attend que le thread A libère le verrou. Cela s'appelle ** Mutex ** (verrouillage mutuellement exclusif). Si le fil A ne libère pas le verrou, le fil B attendra indéfiniment (dead lock).

Au fait, le code suivant sera-t-il bloqué?

public class Sequence {
    public synchronized int getNextValue() {
        return value++;
    }
}

public class LoggerSequence extends Sequence {
    public synchronized int getNextValue() {
         log.d("[in] ", "-- calling getNextValue()");
         return super.getNextValue();
    }
}

Dans le thread A, LoggerSequence acquiert le verrou de la classe Sequence source d'extension lors de l'appel de la méthode de synchronisation getNextValue. En outre, la méthode de la super classe dans le code de bloc tente également d'acquérir le verrou de classe de séquence. Depuis que j'ai acquis le verrou de séquence avec Logger Sequence, je suis susceptible d'attendre un verrou qui ne pourra jamais être acquis. Cependant, le blocage ne se produit pas réellement. Les verrous uniques de Java ont la propriété ** réentrant ** (réentrant), et la requête aboutit lorsque le thread qui détient le verrou tente d'acquérir le même verrou. La JVM garde la trace du thread propriétaire, incrémentant le décompte à chaque fois qu'elle acquiert un verrou et le décrémentant une fois le verrou libéré. Si le même thread tente d'acquérir à nouveau le même verrou, le nombre de verrous sera incrémenté ou décrémenté.

En outre, si vous verrouillez une variable d'état à laquelle accèdent plusieurs threads, les objets de verrouillage doivent être identiques. Par exemple, si vous avez un code comme celui-ci, ce n'est pas du tout thread-safe. Lorsque le thread A accède à getNextValue et effectue le traitement, si quelqu'un accède àsetObject et que l'objet de verrouillage change, l'état de verrouillage change et un autre thread peut accéder au bloc de code. Ce sera.

public class SomeSequence {

    private Object lock = new Object;
    private int value;

    pubic void setObject(Object obj) {
       lock = obj;
    }

    public int getNextValue() {
        synchronized(lock) {
            return value++;
        }
    }
}

Si vous voulez d'abord réaliser en toute sécurité la sécurité des threads, pourquoi ne pas ajouter synchronized à tous? Je pense que j'ai écrit. Par exemple, si vous avez le code suivant: La méthode (bloc) qui était "synchronisée" précédemment ne peut exécuter qu'un seul thread à la fois. Lorsque les threads A, B et C accèdent à ʻaddNumber` dans cette classe, les threads B et C peuvent attendre le traitement du thread A, peut-être pendant un long moment. En d'autres termes, bien que simple, les performances d'exécution sont très médiocres.

public class SomeService {

    private BigInteger number;
    
    public synchronized BigInteger getNumber() {
        return number;
    }

    public synchronized void addNumber() {
        number.add(BigInteger.TEN);
        someHeavyRequest();
    }
}

Le code ci-dessous a été modifié pour ajouter «synchronisé» uniquement au numéro de la variable partagée et le mettre à jour dans ce bloc. En supposant que le code en dehors du bloc accède uniquement aux variables locales, elles ne sont pas partagées par plusieurs threads et n'ont pas besoin d'être synchronisées. Le thread A libère le verrou avant d'exécuter someHeavyRequest, donc il ne compromet pas la concurrence et peut maintenir la sécurité des threads.

public class SomeService {
    private BigInteger number;
    
    public synchronized BigInteger getNumber() {
        return number;
    }

    public void addNumber() {
        synchronized(this) {
            number.add(BigInteger.TEN);
        }
        someHeavyRequest();
    }
}

La concurrence telle que rendre le bloc «synchronisé» plus fin améliore les performances d'exécution (notez que le code devient également compliqué en conséquence). Chaque fois que vous utilisez un verrou, vous devez être conscient que le code à l'intérieur du bloc synchronized le fait, mais l'acquisition et la libération d'un verrou impliquent une surcharge, donc le casser trop peut réduire les performances d'exécution. De plus, même si vous souhaitez améliorer les performances d'exécution, sacrifier la simplicité (bloquer toute la méthode) peut compromettre la sécurité, donc lorsque vous décidez de la taille du bloc «synchronisé», c'est un objectif de conception. Nous avons également besoin d'un compromis. Cependant, il est NG d'avoir un verrou tout en effectuant des traitements qui prennent beaucoup de temps (traitement de calculs lourds, communication, etc.).

à la fin

En gros, je ne sais pas pourquoi, mais cela se produit une fois toutes les 1000 fois, comme un bug qui se produit? La plupart des bogues dont j'ai parlé sont des systèmes de traitement parallèles ici. Dans un tel cas, si vous avez une compréhension du traitement parallèle de base, il peut ou non être un raccourci pour résoudre le problème. Les éléments liés au multi-thread tels que l'API de streaming de Java sont de plus en plus puissants, mais si vous comprenez ces bases, vous pourrez peut-être les adopter plus rapidement. Et s'il vous plaît dites-vous. Eh bien, j'ai écrit une critique du traitement parallèle de Java. En ce qui concerne le traitement parallèle, j'écris les bases des bases. Il y en a beaucoup plus. Ceux qui veulent en savoir plus sont vieux, mais je recommande de lire les livres suivants.

[Programmation de traitement parallèle JAVA](https://www.amazon.co.jp/Java%E4%B8%A6%E8%A1%8C%E5%87%A6%E7%90%86%E3%83%97 % E3% 83% AD% E3% 82% B0% E3% 83% A9% E3% 83% 9F% E3% 83% B3% E3% 82% B0-% E2% 80% 95% E3% 81% 9D% E3% 81% AE% E3% 80% 8C% E5% 9F% BA% E7% 9B% A4% E3% 80% 8D% E3% 81% A8% E3% 80% 8C% E6% 9C% 80% E6% 96% B0API% E3% 80% 8D% E3% 82% 92% E7% A9% B6% E3% 82% 81% E3% 82% 8B% E2% 80% 95-Brian-Goetz / dp / 4797337206)

Recommended Posts

Principes de base du traitement parallèle Java
Les bases de Java
Les bases de Java
bases de la programmation Java
Principes de base de Java JAR
Notions de base orientées objet (Java)
Principes de base du réseau Java (communication)
Muscle Java Basics Jour 1
Principes de base de l'utilisation des caractères (Java)
Java
Résumé des bases du langage Java
Instruction de base de la programmation Java Practice-Switch
Premiers pas avec les bases de Java
Bases du développement Java ~ Exercice (tableau) ~
Je ne suis pas sûr du traitement parallèle Java
Java
[Java11] Résumé de l'utilisation du flux -Basics-
[Notions de base Java] Qu'est-ce que la classe?
Java Performance Chapitre 5 Bases de la récupération de place
Apprendre Java (0)
Étudier Java ―― 3
Java protégé
[Java] Annotation
Notions de base sur les rails
Module [Java]
Tableau Java
Étudier Java ―― 9
Java scratch scratch
Astuces Java, astuces
Méthodes Java
Méthode Java
java (constructeur)
Tableau Java
[Java] ArrayDeque
Bases de Ruby
java (remplacement)
Journée Java 2018
Chaîne Java
java (tableau)
Java statique
Sérialisation Java
java débutant 4
JAVA payé
Mémorandum du nouveau diplômé SES [Java basics]
Étudier Java ―― 4
Java (ensemble)
Notions de base sur les fragments
tri shell java
[Java] compareTo
Étudier Java -5
Principes de base de JPA 1
java (interface)
Méthode de concurrence en Java avec exemple de base
Mémorandum Java
Tableau Java
Étudier Java ―― 1
[Java] Array
Principes de base de Docker
Principes de base de ViewPager