Conception pour la sécurité du fil

Il y a six mois, j'ai commencé une série d'articles sur la conception de classes et d'objets. Dans la rubrique Techniques de conception de ce mois-ci , je continuerai cette série en examinant les principes de conception qui concernent la sécurité des threads. Cet article vous explique ce qu'est la sécurité des threads, pourquoi vous en avez besoin, quand vous en avez besoin et comment l'obtenir.

Qu'est-ce que la sécurité des threads?

La sécurité des threads signifie simplement que les champs d'un objet ou d'une classe conservent toujours un état valide, comme observé par d'autres objets et classes, même lorsqu'ils sont utilisés simultanément par plusieurs threads.

L'une des premières directives que j'ai proposées dans cette colonne (voir «Conception de l'initialisation des objets») est que vous devez concevoir des classes de telle sorte que les objets conservent un état valide, du début de leur durée de vie à la fin. Si vous suivez ce conseil et créez des objets dont les variables d'instance sont toutes privées et dont les méthodes n'effectuent que des transitions d'état appropriées sur ces variables d'instance, vous êtes en bonne forme dans un environnement à thread unique. Mais vous pouvez avoir des problèmes lorsque d'autres threads arrivent.

Plusieurs threads peuvent causer des problèmes pour votre objet car souvent, lorsqu'une méthode est en cours d'exécution, l'état de votre objet peut être temporairement invalide. Lorsqu'un seul thread appelle les méthodes de l'objet, une seule méthode à la fois sera exécutée, et chaque méthode sera autorisée à se terminer avant qu'une autre méthode ne soit appelée. Ainsi, dans un environnement monothread, chaque méthode aura une chance de s'assurer que tout état temporairement invalide est changé en un état valide avant le retour de la méthode.

Cependant, une fois que vous avez introduit plusieurs threads, la JVM peut interrompre le thread exécutant une méthode alors que les variables d'instance de l'objet sont toujours dans un état temporairement invalide. La JVM pourrait alors donner à un thread différent une chance de s'exécuter, et ce thread pourrait appeler une méthode sur le même objet. Tout votre travail acharné pour rendre vos variables d'instance privées et vos méthodes n'effectuant que des transformations d'état valides ne suffira pas à empêcher ce deuxième thread d'observer l'objet dans un état invalide.

Un tel objet ne serait pas sûr pour les threads, car dans un environnement multithread, l'objet pourrait être corrompu ou avoir un état non valide. Un objet thread-safe est un objet qui conserve toujours un état valide, comme observé par d'autres classes et objets, même dans un environnement multithread.

Pourquoi s'inquiéter de la sécurité des fils?

Il y a deux grandes raisons pour lesquelles vous devez penser à la sécurité des threads lorsque vous concevez des classes et des objets en Java:

  1. La prise en charge de plusieurs threads est intégrée au langage Java et à l'API

  2. Tous les threads d'une machine virtuelle Java (JVM) partagent le même tas et la même zone de méthode

Étant donné que le multithreading est intégré à Java, il est possible que toute classe que vous concevez soit éventuellement utilisée simultanément par plusieurs threads. Vous n'avez pas besoin (et ne devriez pas) rendre chaque classe que vous concevez sûre pour les threads, car la sécurité des threads n'est pas gratuite. Mais vous devriez au moins penser à la sécurité des threads chaque fois que vous concevez une classe Java. Vous trouverez une discussion sur les coûts de la sécurité des threads et des directives concernant le moment de rendre les classes thread-safe plus loin dans cet article.

Compte tenu de l'architecture de la JVM, vous ne devez vous préoccuper des variables d'instance et de classe que lorsque vous vous inquiétez de la sécurité des threads. Étant donné que tous les threads partagent le même tas et que le tas est l'endroit où toutes les variables d'instance sont stockées, plusieurs threads peuvent tenter d'utiliser simultanément les variables d'instance du même objet. De même, étant donné que tous les threads partagent la même zone de méthode et que la zone de méthode est l'endroit où toutes les variables de classe sont stockées, plusieurs threads peuvent tenter d'utiliser les mêmes variables de classe simultanément. Lorsque vous choisissez de rendre une classe thread-safe, votre objectif est de garantir l'intégrité - dans un environnement multithread - des variables d'instance et de classe déclarées dans cette classe.

Vous n'avez pas à vous soucier de l'accès multithread aux variables locales, aux paramètres de méthode et aux valeurs de retour, car ces variables résident sur la pile Java. Dans la JVM, chaque thread se voit attribuer sa propre pile Java. Aucun thread ne peut voir ou utiliser des variables locales, des valeurs de retour ou des paramètres appartenant à un autre thread.

Étant donné la structure de la JVM, les variables locales, les paramètres de méthode et les valeurs de retour sont intrinsèquement «thread-safe». Mais les variables d'instance et les variables de classe ne seront thread-safe que si vous concevez votre classe de manière appropriée.

RGBColor # 1: prêt pour un seul fil

Comme exemple de classe non thread-safe, considérez la RGBColorclasse ci-dessous. Les instances de cette classe représentent une couleur stockée dans trois variables d'instance privées: r, get b. Compte tenu de la classe ci-dessous, un RGBColorobjet commencerait sa vie dans un état valide et subirait uniquement des transitions d'état valide, du début de sa vie à la fin - mais uniquement dans un environnement à un seul thread.

// Dans le fichier threads / ex1 / RGBColor.java // Les instances de cette classe ne sont PAS thread-safe. classe publique RGBColor {int r privé; private int g; private int b; public RGBColor (int r, int g, int b) {checkRGBVals (r, g, b); this.r = r; this.g = g; this.b = b; } public void setColor (int r, int g, int b) {checkRGBVals (r, g, b); this.r = r; this.g = g; this.b = b; } / ** * renvoie la couleur dans un tableau de trois entiers: R, V et B * / public int [] getColor () {int [] retVal = new int [3]; retVal [0] = r; retVal [1] = g; retVal [2] = b; return retVal; } public void invert () {r = 255 - r; g = 255 - g; b = 255 - b; } checkRGBVals vide statique privé (int r, int g, int b) {if (r 255 || g 255 || b <0 || b> 255) {throw new IllegalArgumentException (); }}}

Comme les trois variables d'instance, ints r, get b, sont privées, le seul moyen pour les autres classes et objets d'accéder ou d'influencer les valeurs de ces variables est via RGBColorle constructeur et les méthodes de. La conception du constructeur et des méthodes garantit que:

  1. RGBColorLe constructeur du constructeur donnera toujours aux variables des valeurs initiales appropriées

  2. Méthodes setColor()et invert()effectuera toujours des transformations d'état valides sur ces variables

  3. La méthode getColor()retournera toujours une vue valide de ces variables

Notez que si de mauvaises données sont passées au constructeur ou à la setColor()méthode, elles se termineront brusquement avec un InvalidArgumentException. La checkRGBVals()méthode, qui jette cette exception, en effet définit ce que cela signifie pour un RGBColorobjet soit valide: les valeurs des trois variables r, get b, doit être compris entre 0 et 255, inclus. De plus, pour être valide, la couleur représentée par ces variables doit être la couleur la plus récente soit passée au constructeur ou à la setColor()méthode, soit produite par la invert()méthode.

Si, dans un environnement monothread, vous invoquez setColor()et passez en bleu, l' RGBColorobjet sera bleu au setColor()retour. Si vous invoquez ensuite getColor()sur le même objet, vous deviendrez bleu. Dans une société à un seul fil, les instances de cette RGBColorclasse se comportent bien.

Lancer une clé concurrente dans les travaux

Malheureusement, cette image heureuse d'un RGBColorobjet bien élevé peut devenir effrayante lorsque d'autres threads entrent dans l'image. Dans un environnement multithread, les instances de la RGBColorclasse définie ci-dessus sont susceptibles de deux types de mauvais comportement: les conflits d'écriture / écriture et les conflits de lecture / écriture.

Conflits d'écriture / écriture

Imaginez que vous ayez deux fils, un fil nommé «rouge» et un autre nommé «bleu». Les deux fils tentent de définir la couleur du même RGBColorobjet: Le fil rouge tente de définir la couleur sur le rouge; le fil bleu essaie de définir la couleur sur bleu.

Ces deux threads essaient d'écrire simultanément dans les variables d'instance du même objet. Si le planificateur de threads entrelace ces deux threads de la bonne manière, les deux threads interféreront par inadvertance, produisant un conflit d'écriture / écriture. Dans le processus, les deux threads corrompent l'état de l'objet.

L' applet non synchroniséRGBColor

L'applet suivant, nommé RGBColor non synchronisé , illustre une séquence d'événements pouvant entraîner un RGBColorobjet corrompu . Le fil rouge essaie innocemment de définir la couleur sur le rouge tandis que le fil bleu essaie innocemment de définir la couleur sur le bleu. Au final, l' RGBColorobjet ne représente ni le rouge ni le bleu mais la couleur inquiétante, le magenta.

Pour une raison quelconque, votre navigateur ne vous permettra pas de voir cette applet Java cool.

Pour parcourir la séquence d'événements menant à un RGBColorobjet corrompu , appuyez sur le bouton Étape de l'applet. Appuyez sur Retour pour sauvegarder une étape et sur Réinitialiser pour revenir au début. Au fur et à mesure, une ligne de texte en bas de l'applet vous expliquera ce qui se passe à chaque étape.

For those of you who can't run the applet, here's a table that shows the sequence of events demonstrated by the applet:

Thread Statement r g b Color
none object represents green 0 255 0  
blue blue thread invokes setColor(0, 0, 255) 0 255 0  
blue checkRGBVals(0, 0, 255); 0 255 0  
blue this.r = 0; 0 255 0  
blue this.g = 0; 0 255 0  
blue blue gets preempted 0 0 0  
red red thread invokes setColor(255, 0, 0) 0 0 0  
red checkRGBVals(255, 0, 0); 0 0 0  
red this.r = 255; 0 0 0  
red this.g = 0; 255 0 0  
red this.b = 0; 255 0 0  
red red thread returns 255 0 0  
blue later, blue thread continues 255 0 0  
blue this.b = 255 255 0 0  
blue blue thread returns 255 0 255  
none object represents magenta 255 0 255  

As you can see from this applet and table, the RGBColor is corrupted because the thread scheduler interrupts the blue thread while the object is still in a temporarily invalid state. When the red thread comes in and paints the object red, the blue thread is only partially finished painting the object blue. When the blue thread returns to finish the job, it inadvertently corrupts the object.

Read/write conflicts

Another kind of misbehavior that may be exhibited in a multithreaded environment by instances of this RGBColor class is read/write conflicts. This kind of conflict arises when an object's state is read and used while in a temporarily invalid state due to the unfinished work of another thread.

For example, note that during the blue thread's execution of the setColor() method above, the object at one point finds itself in the temporarily invalid state of black. Here, black is a temporarily invalid state because:

  1. It is temporary: Eventually, the blue thread intends to set the color to blue.

  2. It is invalid: No one asked for a black RGBColor object. The blue thread is supposed to turn a green object into blue.

If the blue thread is preempted at the moment the object represents black by a thread that invokes getColor() on the same object, that second thread would observe the RGBColor object's value to be black.

Here's a table that shows a sequence of events that could lead to just such a read/write conflict:

Thread Statement r g b Color
none object represents green 0 255 0  
blue blue thread invokes setColor(0, 0, 255) 0 255 0  
blue checkRGBVals(0, 0, 255); 0 255 0  
blue this.r = 0; 0 255 0  
blue this.g = 0; 0 255 0  
blue blue gets preempted 0 0 0  
red red thread invokes getColor() 0 0 0  
red int[] retVal = new int[3]; 0 0 0  
red retVal[0] = 0; 0 0 0  
red retVal[1] = 0; 0 0 0  
red retVal[2] = 0; 0 0 0  
red return retVal; 0 0 0  
red red thread returns black 0 0 0  
blue later, blue thread continues 0 0 0  
blue this.b = 255 0 0 0  
blue blue thread returns 0 0 255  
none object represents blue 0 0 255  

As you can see from this table, the trouble begins when the blue thread is interrupted when it has only partially finished painting the object blue. At this point the object is in a temporarily invalid state of black, which is exactly what the red thread sees when it invokes getColor() on the object.

Three ways to make an object thread-safe

There are basically three approaches you can take to make an object such as RGBThread thread-safe:

  1. Synchronize critical sections
  2. Make it immutable
  3. Use a thread-safe wrapper

Approach 1: Synchronizing the critical sections

The most straightforward way to correct the unruly behavior exhibited by objects such as RGBColor when placed in a multithreaded context is to synchronize the object's critical sections. An object's critical sections are those methods or blocks of code within methods that must be executed by only one thread at a time. Put another way, a critical section is a method or block of code that must be executed atomically, as a single, indivisible operation. By using Java's synchronized keyword, you can guarantee that only one thread at a time will ever execute the object's critical sections.

To take this approach to making your object thread-safe, you must follow two steps: you must make all relevant fields private, and you must identify and synchronize all the critical sections.

Step 1: Make fields private

La synchronisation signifie qu'un seul thread à la fois sera en mesure d'exécuter un peu de code (une section critique). Ainsi, même si vous souhaitez coordonner l'accès aux champs entre plusieurs threads, le mécanisme de Java pour le faire coordonne en fait l'accès au code. Cela signifie que ce n'est que si vous rendez les données privées que vous pourrez contrôler l'accès à ces données en contrôlant l'accès au code qui manipule les données.