Introduction à la synchronisation en Java

La synchronisation est une fonctionnalité Java qui empêche plusieurs threads d'essayer d'accéder aux ressources partagées en même temps. Ici, les ressources partagées font référence au contenu d'un fichier externe, à des variables de classe ou à des enregistrements de base de données.

La synchronisation est largement utilisée dans la programmation multithread. «Synchronisé» est le mot-clé qui fournit à votre code la possibilité de permettre à un seul thread de fonctionner sur lui sans interférence de tout autre thread pendant cette période.

Pourquoi avons-nous besoin de la synchronisation en Java?

  • Java est un langage de programmation multithread. Cela signifie que deux threads ou plus peuvent s'exécuter simultanément vers la fin d'une tâche. Lorsque les threads s'exécutent simultanément, il y a de fortes chances qu'un scénario se produise où votre code puisse fournir des résultats inattendus.
  • Vous pourriez vous demander si le multithreading peut provoquer des sorties erronées, alors pourquoi est-il considéré comme une caractéristique importante de Java?
  • Le multithreading accélère votre code en exécutant plusieurs threads en parallèle, réduisant ainsi le temps d'exécution de vos codes et offrant des performances élevées. Cependant, l'utilisation de l'environnement multithreading conduit à des sorties inexactes en raison d'une condition communément appelée condition de concurrence critique.

Qu'est-ce qu'une condition de course?

Lorsque deux threads ou plus s'exécutent en parallèle, ils ont tendance à accéder aux ressources partagées et à les modifier à ce moment-là. Les séquences dans lesquelles les threads sont exécutés sont décidées par l'algorithme de programmation des threads.

De ce fait, on ne peut pas prédire l'ordre dans lequel les threads seront exécutés car il est contrôlé uniquement par le planificateur de threads. Cela affecte la sortie du code et entraîne des sorties incohérentes. Étant donné que plusieurs threads sont en course les uns avec les autres pour terminer l'opération, la condition est appelée «condition de course».

Par exemple, considérons le code ci-dessous:

Class Modify:
package JavaConcepts;
public class Modify implements Runnable(
private int myVar=0;
public int getMyVar() (
return myVar;
)
public void setMyVar(int myVar) (
this.myVar = myVar;
)
public void increment() (
myVar++;
)
@Override
public void run() (
// TODO Auto-generated method stub
this.increment();
System.out.println("Current thread being executed "+ Thread.currentThread().getName() + "Current Thread value " + this.getMyVar());
)
)
Class RaceCondition:
package JavaConcepts;
public class RaceCondition (
public static void main(String() args) (
Modify mObj = new Modify();
Thread t1 = new Thread(mObj, "thread 1");
Thread t2 = new Thread(mObj, "thread 2");
Thread t3 = new Thread(mObj, "thread 3");
t1.start();
t2.start();
t3.start();
)
)

Lors de l'exécution consécutive du code ci-dessus, les sorties seront les suivantes:

Ourput1:

Thread en cours d'exécution Thread 1 Current Thread value 3

Thread en cours d'exécution Thread 3 Current Thread value 2

Fil en cours d'exécution fil 2 Valeur de fil actuelle 3

Sortie2:

Thread en cours d'exécution Thread 3 Current Thread value 3

Fil en cours d'exécution fil 2 Valeur de fil actuelle 3

Thread en cours d'exécution Thread 1 Current Thread value 3

Sortie3:

Fil en cours d'exécution fil 2 Valeur de fil actuelle 3

Thread en cours d'exécution Thread 1 Current Thread value 3

Thread en cours d'exécution Thread 3 Current Thread value 3

Output4:

Thread en cours d'exécution Thread 1 Current Thread value 2

Thread en cours d'exécution Thread 3 Current Thread value 3

Thread en cours d'exécution Thread 2 Current Thread value 2

  • À partir de l'exemple ci-dessus, vous pouvez conclure que les threads sont exécutés au hasard et que la valeur est également incorrecte. Selon notre logique, la valeur doit être incrémentée de 1. Cependant, ici, la valeur de sortie dans la plupart des cas est 3 et dans quelques cas, elle est 2.
  • Ici, la variable «myVar» est la ressource partagée sur laquelle plusieurs threads s'exécutent. Les threads accèdent et modifient la valeur de «myVar» simultanément. Voyons ce qui se passe si nous commentons les deux autres fils.

La sortie dans ce cas est:

Le thread en cours d'exécution thread 1 Valeur actuelle du thread 1

Cela signifie que lorsqu'un seul thread s'exécute, la sortie est comme prévu. Cependant, lorsque plusieurs threads sont en cours d'exécution, la valeur est modifiée par chaque thread. Par conséquent, il faut restreindre le nombre de threads travaillant sur une ressource partagée à un seul thread à la fois. Ceci est réalisé en utilisant la synchronisation.

Comprendre ce qu'est la synchronisation en Java

  • La synchronisation en Java est réalisée à l'aide du mot clé "synchronized". Ce mot-clé peut être utilisé pour des méthodes ou des blocs ou des objets mais ne peut pas être utilisé avec des classes et des variables. Un morceau de code synchronisé permet à un seul thread d'y accéder et de le modifier à un moment donné.
  • Cependant, un morceau de code synchronisé affecte les performances du code car il augmente le temps d'attente des autres threads essayant d'y accéder. Un morceau de code ne doit donc être synchronisé que lorsqu'il existe une possibilité qu'une condition de concurrence critique se produise. Sinon, il faut l'éviter.

Comment fonctionne la synchronisation en Java en interne?

  • La synchronisation interne en Java a été mise en œuvre à l'aide du concept de verrouillage (également connu sous le nom de moniteur). Chaque objet Java a son propre verrou. Dans un bloc de code synchronisé, un thread doit acquérir le verrou avant de pouvoir exécuter ce bloc de code particulier. Une fois qu'un thread acquiert le verrou, il peut exécuter ce morceau de code.
  • À la fin de l'exécution, il libère automatiquement le verrou. Si un autre thread nécessite d'opérer sur le code synchronisé, il attend que le thread en cours d'exécution libère le verrou. Ce processus d'acquisition et de libération de verrous est pris en charge en interne par la machine virtuelle Java. Un programme n'est pas responsable de l'acquisition et de la libération des verrous par le thread. Les threads restants peuvent cependant exécuter simultanément tout autre morceau de code non synchronisé.

Synchronisons notre exemple précédent en synchronisant le code à l'intérieur de la méthode run en utilisant le bloc synchronisé dans la classe "Modify" comme ci-dessous:

Class Modify:
package JavaConcepts;
public class Modify implements Runnable(
private int myVar=0;
public int getMyVar() (
return myVar;
)
public void setMyVar(int myVar) (
this.myVar = myVar;
)
public void increment() (
myVar++;
)
@Override
public void run() (
// TODO Auto-generated method stub
synchronized(this) (
this.increment();
System.out.println("Current thread being executed "
+ Thread.currentThread().getName() + " Current Thread value " + this.getMyVar());
)
)
)

Le code de la classe «RaceCondition» reste le même. Maintenant, lors de l'exécution du code, la sortie est la suivante:

Sortie1:

Le thread en cours d'exécution thread 1 Valeur actuelle du thread 1

Le thread en cours d'exécution thread 2 Valeur actuelle du thread 2

Le thread en cours d'exécution thread 3 Valeur actuelle du thread 3

Sortie2:

Le thread en cours d'exécution thread 1 Valeur actuelle du thread 1

Le thread en cours d'exécution thread 3 Valeur actuelle du thread 2

Le thread en cours d'exécution thread 2 Valeur actuelle du thread 3

Notez que notre code fournit la sortie attendue. Ici, chaque thread incrémente la valeur de 1 pour la variable «myVar» (dans la classe «Modify»).

Remarque: la synchronisation est requise lorsque plusieurs threads fonctionnent sur le même objet. Si plusieurs threads fonctionnent sur plusieurs objets, la synchronisation n'est pas requise.

Par exemple, modifions le code de la classe «RaceCondition» comme ci-dessous et travaillons avec la classe précédemment non synchronisée «Modify».

package JavaConcepts;
public class RaceCondition (
public static void main(String() args) (
Modify mObj = new Modify();
Modify mObj1 = new Modify();
Modify mObj2 = new Modify();
Thread t1 = new Thread(mObj, "thread 1");
Thread t2 = new Thread(mObj1, "thread 2");
Thread t3 = new Thread(mObj2, "thread 3");
t1.start();
t2.start();
t3.start();
)
)

Production:

Le thread en cours d'exécution thread 1 Valeur actuelle du thread 1

Le thread en cours d'exécution thread 2 Valeur actuelle du thread 1

Le thread en cours d'exécution thread 3 Valeur actuelle du thread 1

Types de synchronisation en Java:

Il existe deux types de synchronisation de threads, l'un s'excluant mutuellement et l'autre la communication inter-threads.

1.Mutuellement exclusif

  • Méthode synchronisée.
  • Méthode synchronisée statique
  • Bloc synchronisé.

2. Coordination du fil (communication inter-threads en java)

Exclusif:

  • Dans ce cas, les threads obtiennent le verrou avant d'opérer sur un objet, évitant ainsi de travailler avec des objets dont les valeurs ont été manipulées par d'autres threads.
  • Cela peut être réalisé de trois manières:

je. Méthode synchronisée: Nous pouvons utiliser le mot-clé «synchronisé» pour une méthode, ce qui en fait une méthode synchronisée. Chaque thread qui appelle la méthode synchronisée obtiendra le verrou de cet objet et le relâchera une fois son opération terminée. Dans l'exemple ci-dessus, nous pouvons synchroniser notre méthode «run ()» en utilisant le mot-clé «synchronized» après le modificateur d'accès.

@Override
public synchronized void run() (
// TODO Auto-generated method stub
this.increment();
System.out.println("Current thread being executed "
+ Thread.currentThread().getName() + " Current Thread value " + this.getMyVar());
)

La sortie pour ce cas sera:

Le thread en cours d'exécution thread 1 Valeur actuelle du thread 1

Le thread en cours d'exécution thread 3 Valeur actuelle du thread 2

Le thread en cours d'exécution thread 2 Valeur actuelle du thread 3

ii. Méthode synchronisée statique: Pour synchroniser les méthodes statiques, il faut acquérir son verrou de niveau classe. Une fois qu'un thread a obtenu le verrou de niveau de classe uniquement, il sera en mesure d'exécuter une méthode statique. Alors qu'un thread détient le verrou au niveau de la classe, aucun autre thread ne peut exécuter une autre méthode synchronisée statique de cette classe. Cependant, les autres threads peuvent exécuter n'importe quelle autre méthode régulière ou méthode statique régulière ou même méthode synchronisée non statique de cette classe.

Par exemple, considérons notre classe «Modifier» et y apportons des modifications en convertissant notre méthode «incrément» en une méthode synchronisée statique. Les changements de code sont les suivants:

package JavaConcepts;
public class Modify implements Runnable(
private static int myVar=0;
public int getMyVar() (
return myVar;
)
public void setMyVar(int myVar) (
this.myVar = myVar;
)
public static synchronized void increment() (
myVar++;
System.out.println("Current thread being executed " + Thread.currentThread().getName() + " Current Thread value " + myVar);
)
@Override
public void run() (
// TODO Auto-generated method stub
increment();
)
)

iii. Bloc synchronisé: l' un des principaux inconvénients de la méthode synchronisée est qu'elle augmente le temps d'attente des threads, ce qui affecte les performances du code. Par conséquent, pour pouvoir synchroniser uniquement les lignes de code requises à la place de la méthode entière, il faut utiliser un bloc synchronisé. L'utilisation du bloc synchronisé réduit le temps d'attente des threads et améliore également les performances. Dans l'exemple précédent, nous avons déjà utilisé le bloc synchronisé lors de la première synchronisation de notre code.

Exemple:
public void run() (
// TODO Auto-generated method stub
synchronized(this) (
this.increment();
System.out.println("Current thread being executed "
+ Thread.currentThread().getName() + " Current Thread value " + this.getMyVar());
)
)

Coordination du fil:

Pour les threads synchronisés, la communication entre les threads est une tâche importante. Les méthodes intégrées qui aident à réaliser la communication inter-thread pour le code synchronisé sont à savoir:

  • attendez()
  • notifier ()
  • notifyAll ()

Remarque: Ces méthodes appartiennent à la classe d'objet et non à la classe de thread. Pour qu'un thread puisse invoquer ces méthodes sur un objet, il doit maintenir le verrou sur cet objet. En outre, ces méthodes obligent un thread à libérer son verrou sur l'objet sur lequel il est appelé.

wait (): Un thread sur l'appel de la méthode wait (), libère le verrou sur l'objet et passe en état d'attente. Il a deux surcharges de méthode:

  • public final void wait () lève InterruptedException
  • public final void wait (long timeout) lève InterruptedException
  • public final void wait (long timeout, int nanos) throws InterruptedException

notify (): Un thread envoie un signal à un autre thread en attente en utilisant la méthode notify (). Il envoie la notification à un seul thread de sorte que ce thread puisse reprendre son exécution. Le thread qui recevra la notification parmi tous les threads en attente dépend de la machine virtuelle Java.

  • annulation finale publique notifier ()

notifyAll (): Lorsqu'un thread invoque la méthode notifyAll (), chaque thread dans son état d'attente est notifié. Ces threads seront exécutés l'un après l'autre en fonction de l'ordre décidé par la machine virtuelle Java.

  • annulation finale publique notifierAll ()

Conclusion

Dans cet article, nous avons vu comment le travail dans un environnement multithread peut entraîner une incohérence des données en raison d'une condition de concurrence. Comment la synchronisation nous aide à surmonter cela en limitant un seul thread à fonctionner sur une ressource partagée à la fois. Aussi comment les threads synchronisés communiquent entre eux.

Articles recommandés:

Cela a été un guide pour Qu'est-ce que la synchronisation en Java?. Nous discutons ici de l'introduction, de la compréhension, des besoins, du fonctionnement et des types de synchronisation avec un exemple de code. Vous pouvez également consulter nos autres articles suggérés pour en savoir plus -

  1. Sérialisation en Java
  2. Qu'est-ce que les génériques en Java?
  3. Qu'est-ce que l'API en Java?
  4. Qu'est-ce qu'un arbre binaire en Java?
  5. Exemples et fonctionnement des génériques en C #