Maison  >  Article  >  Java  >  Comment implémenter la programmation multithread en Java

Comment implémenter la programmation multithread en Java

PHPz
PHPzavant
2023-05-01 18:22:071397parcourir

1. Démarrez un fil de discussion dans le constructeur

J'ai vu ce problème dans de nombreux codes. Démarrez un fil de discussion dans le constructeur, similaire à celui-ci :

public class A{     public A(){        this.x=1;        this.y=2;        this.thread=new MyThread();        this.thread.start();     }       }

Quels problèmes cela va-t-il causer ? S'il existe une classe B qui hérite de la classe A, selon l'ordre d'initialisation de la classe Java, le constructeur de A sera certainement appelé avant l'appel du constructeur de B, puis le thread du thread sera également démarré avant que B ne soit complètement initialisé. en cours d'exécution Si vous utilisez certaines variables dans la classe A, vous ne pouvez pas utiliser les valeurs attendues, car vous pouvez attribuer de nouvelles valeurs à ces variables dans le constructeur de B. En d’autres termes, deux threads utiliseront ces variables à ce moment-là, mais ces variables ne seront pas synchronisées.

Il existe deux façons de résoudre ce problème : définir A comme final et non héritable ; ou fournir une méthode de démarrage distincte pour démarrer le thread au lieu de le placer dans le constructeur.

2. Synchronisation incomplète

Nous savons tous que le moyen efficace de synchroniser une variable est de la protéger avec Synchronized peut être un verrou d'objet ou un verrou de classe, selon que vous êtes une méthode de classe ou une instance. méthode. Cependant, lorsque vous synchronisez une variable dans la méthode A, vous devez également la synchroniser ailleurs où la variable apparaît, à moins que vous n'autorisiez une faible visibilité ou même produisiez des valeurs d'erreur. Code similaire à celui-ci :

class A{    int x;    public int getX(){       return x;    }    public synchronized void setX(int x)    {       this.x=x;    }  }

La méthode setter de x est synchronisée, mais la méthode getter ne l'est pas, il n'y a donc aucune garantie que x obtenu par d'autres threads via getX soit la valeur absolue. En fait, la synchronisation de setX ici n'est pas nécessaire, car l'écriture de int est atomique, ce que garantit la spécification JVM, et les synchronisations multiples n'ont bien sûr aucun sens, s'il ne s'agit pas d'un int, mais d'un double ou d'un long, Ensuite, getX et setX devront être synchronisés, car double et long sont tous deux 64 bits, et l'écriture et la lecture sont divisées en deux 32 bits (cela dépend de l'implémentation jvm. Certaines implémentations jvm peuvent garantir une lecture longue et longue de Double et l'écriture sont atomiques), et l'atomicité n'est pas garantie. Un code comme celui ci-dessus peut en fait être résolu en déclarant la variable comme volatile.

3. Lors de l'utilisation d'un objet comme verrou, la référence de l'objet est modifiée, provoquant un échec de synchronisation.

Il s'agit également d'une erreur très courante, similaire au code suivant :

synchronized(array[0])  {     ......     array[0]=new A();     ......  }

Le bloc synchronisé utilise le tableau[0] comme verrou, mais la référence pointée par le tableau[0] est modifiée dans le bloc synchronisé. En analysant ce scénario, le premier thread acquiert le verrou du tableau[0], le deuxième thread attend car il ne peut pas acquérir le tableau[0], et après avoir modifié la référence du tableau[0], le troisième thread acquiert les verrous du nouveau tableau [0], les verrous détenus par les premier et troisième threads sont différents et l'objectif de synchronisation et d'exclusion mutuelle n'a pas du tout été atteint. De telles modifications de code impliquent généralement de déclarer le verrou comme variable finale ou d'introduire un objet de verrouillage indépendant de l'entreprise pour garantir que la référence ne sera pas modifiée dans le bloc synchronisé.

4. Wait() n'est pas appelé dans la boucle.

wait et notify sont utilisés pour implémenter des variables de condition. Vous savez peut-être que wait et notify doivent être appelés dans un bloc synchronisé pour garantir que les changements de conditions sont atomiques et visibles. Je vois souvent beaucoup de code qui est synchronisé, mais n'appelle pas wait dans la boucle, mais utilise if ou même pas de jugement conditionnel :

synchronized(lock)  {     if(isEmpty()       lock.wait();       }

Le jugement conditionnel est d'utiliser if. Quels problèmes cela va-t-il causer ? Vous pouvez appeler notify ou notifyAll avant de juger la condition. La condition est alors remplie et il n'y aura pas d'attente. Lorsque les conditions ne sont pas remplies, la méthode wait() est appelée pour libérer le verrou et entrer en état de veille d'attente. Si le thread est réveillé dans des circonstances normales, c'est-à-dire après que les conditions ont été modifiées, alors il n'y a aucun problème et les opérations logiques suivantes continuent d'être exécutées si les conditions sont remplies. Le problème est que le thread peut être réveillé accidentellement ou même de manière malveillante. Puisque la condition n'est pas évaluée à nouveau, le thread effectue des opérations ultérieures lorsque la condition n'est pas remplie. Un réveil inattendu peut être provoqué par l'appel de notifyAll, quelqu'un peut se réveiller de manière malveillante, ou il peut s'agir d'un réveil automatique dans de rares cas (appelé « pseudo-réveil »). Par conséquent, afin d'éviter que des opérations ultérieures ne soient effectuées si les conditions ne sont pas remplies, il est nécessaire de juger à nouveau les conditions après avoir été réveillé. Si les conditions ne sont pas remplies, continuez à entrer dans l'état d'attente, puis procédez aux opérations suivantes. si les conditions sont remplies.

synchronized(lock)  {     while(isEmpty()       lock.wait();       }

La situation de l'appel de wait sans jugement conditionnel est plus grave, car notify peut avoir été appelé avant d'attendre, donc après avoir appelé wait et entré dans l'état de veille d'attente, il n'y a aucune garantie que le thread se réveillera.

5. La plage de synchronisation est trop petite ou trop grande.

Si la portée de la synchronisation est trop petite, l'objectif de la synchronisation peut ne pas être atteint du tout ; si la portée de la synchronisation est trop grande, les performances peuvent être affectées. Un exemple courant de portée de synchronisation trop petite est la croyance erronée que deux méthodes synchronisées seront synchronisées lorsqu'elles seront appelées ensemble. Ce qu'il faut retenir est Atomic+Atomic!=Atomic.

Map map=Collections.synchronizedMap(new HashMap());  if(!map.containsKey("a")){           map.put("a", value);  }

这是一个很典型的错误,map是线程安全的,containskey和put方法也是线程安全的,然而两个线程安全的方法被组合调用就不一定是线程安全的了。因为在containsKey和put之间,可能有其他线程抢先put进了a,那么就可能覆盖了其他线程设置的值,导致值的丢失。解决这一问题的方法就是扩大同步范围,因为对象锁是可重入的,因此在线程安全方法之上再同步相同的锁对象不会有问题。

Map map = Collections.synchronizedMap(new HashMap());  synchronized (map) {       if (!map.containsKey("a")) {           map.put("a", value);       }   }

注意,加大锁的范围,也要保证使用的是同一个锁,不然很可能造成死锁。 Collections.synchronizedMap(new HashMap())使用的锁是map本身,因此没有问题。当然,上面的情况现在更推荐使用ConcurrentHashMap,它有putIfAbsent方法来达到同样的目的并且满足线程安全性。

同步范围过大的例子也很多,比如在同步块中new大对象,或者调用费时的IO操作(操作数据库,webservice等)。不得不调用费时操作的时候,一定要指定超时时间,例如通过URLConnection去invoke某个URL时就要设置connect timeout和read timeout,防止锁被独占不释放。同步范围过大的情况下,要在保证线程安全的前提下,将不必要同步的操作从同步块中移出。

6、正确使用volatile

在jdk5修正了volatile的语义后,volatile作为一种轻量级的同步策略就得到了大量的使用。volatile的严格定义参考jvm spec,这里只从volatile能做什么,和不能用来做什么出发做个探讨。

volatile可以用来做什么?

1)状态标志,模拟控制机制。常见用途如控制线程是否停止:

private volatile boolean stopped;  public void close(){     stopped=true;  }   public void run(){      while(!stopped){        //do something     }       }

前提是do something中不会有阻塞调用之类。volatile保证stopped变量的可见性,run方法中读取stopped变量总是main memory中的***值。

2)安全发布,如修复DLC问题。

private volatile IoBufferAllocator instance;  public IoBufferAllocator getInsntace(){      if(instance==null){          synchronized (IoBufferAllocator.class) {              if(instance==null)                  instance=new IoBufferAllocator();          }      }      return instance;  }

3)开销较低的读写锁

public class CheesyCounter {      private volatile int value;       public int getValue() { return value; }       public synchronized int increment() {          return value++;      }  }

synchronized保证更新的原子性,volatile保证线程间的可见性。

volatile不能用于做什么?

1)不能用于做计数器

public class CheesyCounter {      private volatile int value;       public int getValue() { return value; }       public int increment() {          return value++;      }  }

因为value++其实是有三个操作组成的:读取、修改、写入,volatile不能保证这个序列是原子的。对value的修改操作依赖于value的***值。解决这个问题的方法可以将increment方法同步,或者使用AtomicInteger原子类。

2)与其他变量构成不变式

一个典型的例子是定义一个数据范围,需要保证约束lower< upper。

public class NumberRange {      private volatile int lower, upper;       public int getLower() { return lower; }      public int getUpper() { return upper; }       public void setLower(int value) {           if (value > upper)               throw new IllegalArgumentException();          lower = value;      }       public void setUpper(int value) {           if (value < lower)               throw new IllegalArgumentException();          upper = value;      }  }

尽管讲lower和upper声明为volatile,但是setLower和setUpper并不是线程安全方法。假设初始状态为(0,5),同时调用setLower(4)和setUpper(3),两个线程交叉进行,***结果可能是(4,3),违反了约束条件。修改这个问题的办法就是将setLower和setUpper同步:

public class NumberRange {      private volatile int lower, upper;       public int getLower() { return lower; }      public int getUpper() { return upper; }       public synchronized void setLower(int value) {           if (value > upper)               throw new IllegalArgumentException();          lower = value;      }       public synchronized void setUpper(int value) {           if (value < lower)               throw new IllegalArgumentException();          upper = value;      }  }

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Cet article est reproduit dans:. en cas de violation, veuillez contacter admin@php.cn Supprimer