Maison > Java > javaDidacticiel > Analyse approfondie de la synchronisation des threads Java et de la communication entre les threads

Analyse approfondie de la synchronisation des threads Java et de la communication entre les threads

高洛峰
Libérer: 2017-01-05 15:18:31
original
1291 Les gens l'ont consulté

Synchronisation des threads Java
Lorsque deux threads ou plus doivent partager des ressources, ils ont besoin d'un moyen de déterminer que la ressource est occupée par un seul thread à un moment donné. Le processus pour y parvenir s’appelle la synchronisation. Comme vous pouvez le constater, Java fournit une prise en charge unique au niveau du langage.

La clé de la synchronisation est le concept de moniteur (également appelé sémaphore). Un moniteur est un objet verrouillé mutuellement exclusif, ou mutex. Un seul thread peut obtenir le moniteur à un moment donné. Lorsqu'un thread a besoin d'un verrou, il doit entrer dans le moniteur. Tous les autres fils essayant d'entrer dans un tube verrouillé doivent rester suspendus jusqu'à ce que le premier fil sorte du tube. Ces autres threads sont appelés moniteurs d'attente. Un thread qui possède un moniteur peut entrer à nouveau dans le même moniteur s'il le souhaite.

Si vous avez utilisé la synchronisation dans d'autres langages comme le C ou le C, vous saurez que c'est un peu bizarre à utiliser. En effet, de nombreuses langues ne prennent pas elles-mêmes en charge la synchronisation. En revanche, pour les threads synchronisés, le programme doit utiliser le langage source du système d’exploitation. Heureusement, Java implémente la synchronisation via des éléments de langage et la majeure partie de la complexité associée à la synchronisation est éliminée.

Vous pouvez synchroniser le code de deux manières. Les deux incluent l'utilisation du mot-clé synchronisé. Les deux méthodes sont expliquées ci-dessous.
Utiliser les méthodes de synchronisation

La synchronisation en Java est simple car tous les objets ont leurs moniteurs implicites correspondants. Entrer dans le moniteur d'un objet revient à appeler la méthode modifiée par le mot-clé synchronisé. Lorsqu'un thread se trouve dans une méthode synchronisée, tous les autres threads de la même instance qui tentent d'appeler cette méthode (ou d'autres méthodes synchronisées) doivent attendre. Afin de quitter le moniteur et de céder le contrôle de l'objet à d'autres threads en attente, le thread propriétaire du moniteur revient simplement de la méthode synchronisée.

Pour comprendre la nécessité de la synchronisation, commençons par un exemple simple où la synchronisation devrait être utilisée mais ne l'est pas. Le programme suivant comporte trois classes simples. Le premier est Callme, qui a une méthode simple call(). La méthode call() a un paramètre String nommé msg. Cette méthode tente d'imprimer la chaîne msg entre crochets. La chose intéressante est qu'après avoir appelé call() pour imprimer le crochet gauche et la chaîne msg, Thread.sleep(1000) est appelé, ce qui met le thread en cours en pause pendant 1 seconde.

Le constructeur de la classe suivante, Caller, fait référence à une instance de Callme et à une String, qui sont respectivement stockées dans target et msg. Le constructeur crée également un nouveau thread qui appelle la méthode run() de l'objet. Le fil démarre immédiatement. La méthode run() de la classe Caller appelle la méthode call() de la cible de l'instance Callme via la chaîne msg du paramètre. Enfin, la classe Synch commence par créer une instance simple de Callme et trois instances de Caller avec des chaînes de message différentes.

La même instance de Callme est transmise à chaque instance de l'appelant.

// This program is not synchronized.
class Callme {
  void call(String msg) {
    System.out.print("[" + msg);
    try {
      Thread.sleep(1000);
    } catch(InterruptedException e) {
      System.out.println("Interrupted");
    }
    System.out.println("]");
  }
}
 
class Caller implements Runnable {
  String msg;
  Callme target;
  Thread t;
  public Caller(Callme targ, String s) {
    target = targ;
    msg = s;
    t = new Thread(this);
    t.start();
  }
  public void run() {
    target.call(msg);
  }
}
 
class Synch {
  public static void main(String args[]) {
    Callme target = new Callme();
    Caller ob1 = new Caller(target, "Hello");
    Caller ob2 = new Caller(target, "Synchronized");
    Caller ob3 = new Caller(target, "World");
    // wait for threads to end
    try {
     ob1.t.join();
     ob2.t.join();
     ob3.t.join();
    } catch(InterruptedException e) {
     System.out.println("Interrupted");
    }
  }
}
Copier après la connexion

Le résultat de ce programme est le suivant :

Hello[Synchronized[World]
]
]
Copier après la connexion

Dans cet exemple, la méthode call() permet à l'exécution de passer à un autre thread en appelant sleep(). Le résultat est une sortie mixte des trois chaînes de message. Dans ce programme, aucune méthode n'empêche trois threads d'appeler la même méthode du même objet en même temps. Il s'agit d'une condition de concurrence car trois threads sont en compétition pour terminer la méthode. L'exemple utilise sleep() pour rendre cet effet reproductible et évident. Dans la plupart des cas, les conflits sont plus complexes et imprévisibles car vous ne pouvez pas être sûr du moment où un changement de contexte se produira. Cela entraîne parfois un bon fonctionnement du programme et parfois un échec.

Afin d'atteindre l'objectif de l'exemple ci-dessus, vous devez avoir le droit d'utiliser call() en continu. Autrement dit, à un moment donné, il doit être limité à un seul fil qui peut le dominer. Pour cela, il suffit d'ajouter le mot-clé synchronisé avant la définition de call(), de la manière suivante :

class Callme {
  synchronized void call(String msg) {
    ...
Copier après la connexion

Cela empêche l'utilisation de call ( ) lorsque d'autres threads entrent call(). Une fois synchronisé ajouté devant call(), le résultat du programme est le suivant :

[Hello]
[Synchronized]
[World]
Copier après la connexion

À tout moment dans une situation multithread, vous avez une ou plusieurs méthodes Les méthodes qui manipulent l'état interne d'un objet doivent utiliser le mot-clé synchronisé pour empêcher la concurrence d'état. N'oubliez pas qu'une fois qu'un thread entre dans une méthode synchronisée d'une instance, aucun autre thread ne peut entrer dans une méthode synchronisée de la même instance. Cependant, d'autres méthodes asynchrones de l'instance peuvent toujours être appelées.
Déclaration de synchronisation

Bien que créer une méthode de synchronisation à l'intérieur de la classe créée soit un moyen simple et efficace d'obtenir la synchronisation, cela ne fonctionne pas tout le temps. Veuillez réfléchir aux raisons de cela. Supposons que vous souhaitiez obtenir un accès synchronisé à un objet de classe qui n'est pas conçu pour un accès multithread, c'est-à-dire que la classe n'utilise pas la méthode synchronisée. De plus, la classe n’a pas été créée par vous mais par un tiers, et vous ne pouvez pas obtenir son code source. De cette façon, vous ne pouvez pas ajouter le modificateur synchronisé avant la méthode appropriée. Comment synchroniser un objet de cette classe ? Heureusement, la solution est simple : il suffit de placer les appels aux méthodes définies par cette classe dans un bloc synchronisé.

Ce qui suit est la forme courante de relevé synchronisé :

synchronized(object) {
  // statements to be synchronized
}
Copier après la connexion

其中,object是被同步对象的引用。如果你想要同步的只是一个语句,那么不需要花括号。一个同步块确保对object成员方法的调用仅在当前线程成功进入object管程后发生。

下面是前面程序的修改版本,在run( )方法内用了同步块:

// This program uses a synchronized block.
class Callme {
  void call(String msg) {
    System.out.print("[" + msg);
    try {
      Thread.sleep(1000);
    } catch (InterruptedException e) {
      System.out.println("Interrupted");
    }
    System.out.println("]");
  }
}
 
class Caller implements Runnable {
  String msg;
  Callme target;
  Thread t;
  public Caller(Callme targ, String s) {
    target = targ;
    msg = s;
    t = new Thread(this);
    t.start();
  }
 
  // synchronize calls to call()
  public void run() {
    synchronized(target) { // synchronized block
      target.call(msg);
    }
  }
}
 
class Synch1 {
  public static void main(String args[]) {
    Callme target = new Callme();
    Caller ob1 = new Caller(target, "Hello");
    Caller ob2 = new Caller(target, "Synchronized");
    Caller ob3 = new Caller(target, "World");
 
    // wait for threads to end
    try {
      ob1.t.join();
      ob2.t.join();
      ob3.t.join();
    } catch(InterruptedException e) {
      System.out.println("Interrupted");
    }
  }
}
Copier après la connexion

这里,call( )方法没有被synchronized修饰。而synchronized是在Caller类的run( )方法中声明的。这可以得到上例中同样正确的结果,因为每个线程运行前都等待先前的一个线程结束。

Java线程间通信
多线程通过把任务分成离散的和合乎逻辑的单元代替了事件循环程序。线程还有第二优点:它远离了轮询。轮询通常由重复监测条件的循环实现。一旦条件成立,就要采取适当的行动。这浪费了CPU时间。举例来说,考虑经典的序列问题,当一个线程正在产生数据而另一个程序正在消费它。为使问题变得更有趣,假设数据产生器必须等待消费者完成工作才能产生新的数据。在轮询系统,消费者在等待生产者产生数据时会浪费很多CPU周期。一旦生产者完成工作,它将启动轮询,浪费更多的CPU时间等待消费者的工作结束,如此下去。很明显,这种情形不受欢迎。

为避免轮询,Java包含了通过wait( ),notify( )和notifyAll( )方法实现的一个进程间通信机制。这些方法在对象中是用final方法实现的,所以所有的类都含有它们。这三个方法仅在synchronized方法中才能被调用。尽管这些方法从计算机科学远景方向上来说具有概念的高度先进性,实际中用起来是很简单的:
wait( ) 告知被调用的线程放弃管程进入睡眠直到其他线程进入相同管程并且调用notify( )。
notify( ) 恢复相同对象中第一个调用 wait( ) 的线程。
notifyAll( ) 恢复相同对象中所有调用 wait( ) 的线程。具有最高优先级的线程最先运行。

这些方法在Object中被声明,如下所示:

final void wait( ) throws InterruptedException
final void notify( )
final void notifyAll( )
Copier après la connexion


wait( )存在的另外的形式允许你定义等待时间。

下面的例子程序错误的实行了一个简单生产者/消费者的问题。它由四个类组成:Q,设法获得同步的序列;Producer,产生排队的线程对象;Consumer,消费序列的线程对象;以及PC,创建单个Q,Producer,和Consumer的小类。

// An incorrect implementation of a producer and consumer.
class Q {
  int n;
  synchronized int get() {
    System.out.println("Got: " + n);
    return n;
  }
  synchronized void put(int n) {
    this.n = n;
    System.out.println("Put: " + n);
  }
}
class Producer implements Runnable {
  Q q;
  Producer(Q q) {
    this.q = q;
    new Thread(this, "Producer").start();
  }
  public void run() {
    int i = 0;
    while(true) {
      q.put(i++);
    }
  }
}
class Consumer implements Runnable {
  Q q;
  Consumer(Q q) {
    this.q = q;
    new Thread(this, "Consumer").start();
  }
  public void run() {
    while(true) {
      q.get();
    }
  }
}
class PC {
  public static void main(String args[]) {
    Q q = new Q();
    new Producer(q);
    new Consumer(q);
    System.out.println("Press Control-C to stop.");
  }
}
Copier après la connexion

尽管Q类中的put( )和get( )方法是同步的,没有东西阻止生产者超越消费者,也没有东西阻止消费者消费同样的序列两次。这样,你就得到下面的错误输出(输出将随处理器速度和装载的任务而改变):

Put: 1
Got: 1
Got: 1
Got: 1
Got: 1
Got: 1
Put: 2
Put: 3
Put: 4
Put: 5
Put: 6
Put: 7
Got: 7
Copier après la connexion

生产者生成1后,消费者依次获得同样的1五次。生产者在继续生成2到7,消费者没有机会获得它们。

用Java正确的编写该程序是用wait( )和notify( )来对两个方向进行标志,如下所示:

// A correct implementation of a producer and consumer.
class Q {
  int n;
  boolean valueSet = false;
  synchronized int get() {
    if(!valueSet)
      try {
        wait();
      } catch(InterruptedException e) {
        System.out.println("InterruptedException caught");
      }
      System.out.println("Got: " + n);
      valueSet = false;
      notify();
      return n;
    }
    synchronized void put(int n) {
      if(valueSet)
      try {
        wait();
      } catch(InterruptedException e) {
        System.out.println("InterruptedException caught");
      }
      this.n = n;
      valueSet = true;
      System.out.println("Put: " + n);
      notify();
    }
  }
  class Producer implements Runnable {
    Q q;
    Producer(Q q) {
    this.q = q;
    new Thread(this, "Producer").start();
  }
  public void run() {
    int i = 0;
    while(true) {
      q.put(i++);
    }
  }
}
class Consumer implements Runnable {
  Q q;
  Consumer(Q q) {
    this.q = q;
    new Thread(this, "Consumer").start();
  }
  public void run() {
    while(true) {
      q.get();
    }
  }
}
class PCFixed {
  public static void main(String args[]) {
    Q q = new Q();
    new Producer(q);
    new Consumer(q);
    System.out.println("Press Control-C to stop.");
  }
}
Copier après la connexion

内部get( ), wait( )被调用。这使执行挂起直到Producer 告知数据已经预备好。这时,内部get( ) 被恢复执行。获取数据后,get( )调用notify( )。这告诉Producer可以向序列中输入更多数据。在put( )内,wait( )挂起执行直到Consumer取走了序列中的项目。当执行再继续,下一个数据项目被放入序列,notify( )被调用,这通知Consumer它应该移走该数据。

下面是该程序的输出,它清楚的显示了同步行为:

Put: 1
Got: 1
Put: 2
Got: 2
Put: 3
Got: 3
Put: 4
Got: 4
Put: 5
Got: 5
Copier après la connexion

    

更多深入解析Java的线程同步以及线程间通信相关文章请关注PHP中文网!


Étiquettes associées:
source:php.cn
Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal