Heim  >  Artikel  >  Java  >  So implementieren Sie Multithread-Programmierung in Java

So implementieren Sie Multithread-Programmierung in Java

PHPz
PHPznach vorne
2023-05-01 18:22:071397Durchsuche

1. Starten Sie einen Thread im Konstruktor

Ich habe dieses Problem in vielen Codes gesehen, ähnlich wie hier: #🎜 🎜#

public class A{     public A(){        this.x=1;        this.y=2;        this.thread=new MyThread();        this.thread.start();     }       }
Welche Probleme wird das verursachen? Wenn es eine Klasse B gibt, die Klasse A erbt, wird gemäß der Reihenfolge der Java-Klasseninitialisierung der Konstruktor von A definitiv vor dem Aufruf des Konstruktors von B aufgerufen, und der Thread-Thread wird auch gestartet, bevor B vollständig initialisiert ist Wenn Sie einige Variablen in Klasse A verwenden, verwenden Sie möglicherweise nicht die erwarteten Werte, da Sie diesen Variablen im Konstruktor von B möglicherweise neue Werte zuweisen. Mit anderen Worten: Zu diesem Zeitpunkt verwenden zwei Threads diese Variablen, diese Variablen sind jedoch nicht synchronisiert.

Es gibt zwei Möglichkeiten, dieses Problem zu lösen: Legen Sie A als endgültig und nicht vererbbar fest oder stellen Sie eine separate Startmethode bereit, um den Thread zu starten, anstatt ihn im Konstruktor zu platzieren.

2. Unvollständige Synchronisierung

Wir alle wissen, dass der wirksame Weg, eine Variable zu synchronisieren, darin besteht, sie mit „Synchronized“ zu schützen Objektsperre oder eine Klassensperre, je nachdem, ob es sich um eine Klassenmethode oder eine Instanzmethode handelt. Wenn Sie jedoch eine Variable in Methode A synchronisieren, müssen Sie sie auch an anderer Stelle synchronisieren, wo die Variable erscheint, es sei denn, Sie lassen eine schwache Sichtbarkeit zu oder erzeugen sogar Fehlerwerte. Code wie dieser:

class A{    int x;    public int getX(){       return x;    }    public synchronized void setX(int x)    {       this.x=x;    }  }
Die Setter-Methode von Tatsächlich ist die Synchronisierung von setX hier nicht erforderlich, da das Schreiben von int atomar ist, was die JVM-Spezifikation garantiert, und mehrere Synchronisierungen natürlich keine Bedeutung haben, wenn es sich nicht um ein int, sondern um ein double oder long handelt. Dann müssen sowohl getX als auch setX synchronisiert werden, da double und long beide 64-Bit sind und das Schreiben und Lesen in zwei 32-Bit-Bits unterteilt ist (dies hängt von der JVM-Implementierung ab. Einige JVM-Implementierungen garantieren möglicherweise Long- und Long-Double-Lesungen und write sind atomar), und die Atomizität ist nicht garantiert. Code wie der obige kann tatsächlich gelöst werden, indem die Variable als flüchtig deklariert wird.



3. Wenn ein Objekt als Sperre verwendet wird, wird die Referenz des Objekts geändert, was zu einem Synchronisierungsfehler führt.

Dies ist auch ein sehr häufiger Fehler, ähnlich dem folgenden Code:

synchronized(array[0])  {     ......     array[0]=new A();     ......  }
Der Synchronisationsblock verwendet Array[0] als Sperre, ändert sich jedoch im Synchronisationsblock Die Referenz, auf die Array[0] zeigt. Bei der Analyse dieses Szenarios erhält der erste Thread die Sperre von Array[0], der zweite Thread wartet, weil er Array[0] nicht abrufen kann, und nach dem Ändern der Referenz von Array[0] erhält der dritte Thread die Sperren des neuen Arrays [0], die vom ersten und dritten Thread gehaltenen Sperren sind unterschiedlich und der Zweck der Synchronisierung und des gegenseitigen Ausschlusses wurde überhaupt nicht erreicht. Solche Codeänderungen umfassen normalerweise die Deklaration der Sperre als endgültige Variable oder die Einführung eines geschäftsunabhängigen Sperrobjekts, um sicherzustellen, dass die Referenz innerhalb des synchronisierten Blocks nicht geändert wird.

4. Wait() wird nicht in der Schleife aufgerufen.

Wait und Notify werden zum Implementieren von Bedingungsvariablen verwendet. Möglicherweise wissen Sie, dass Wait und Notify in einem synchronisierten Block aufgerufen werden müssen, um sicherzustellen, dass Änderungen in den Bedingungen atomar und sichtbar sind. Ich sehe oft viel Code, der synchronisiert ist, aber nicht „wait“ in der Schleife aufruft, sondern „if“ oder gar keine bedingte Beurteilung verwendet:

synchronized(lock)  {     if(isEmpty()       lock.wait();       }
Die bedingte Beurteilung besteht darin, „if“ zu verwenden Ursache? Sie können notify oder notifyAll aufrufen, bevor Sie die Bedingung beurteilen. Dann ist die Bedingung erfüllt und es gibt kein Problem. Wenn die Bedingungen nicht erfüllt sind, wird die Methode wait () aufgerufen, um die Sperre aufzuheben und in den Warteschlafzustand zu wechseln. Wenn der Thread unter normalen Umständen, also nach einer Änderung der Bedingungen, aktiviert wird, liegt kein Problem vor und die folgenden logischen Operationen werden weiterhin ausgeführt, wenn die Bedingungen erfüllt sind. Das Problem besteht darin, dass der Thread möglicherweise versehentlich oder sogar böswillig aktiviert wird. Da die Bedingung nicht erneut beurteilt wird, führt der Thread nachfolgende Vorgänge aus, wenn die Bedingung nicht erfüllt ist. Ein unerwartetes Aufwachen kann durch den Aufruf von notifyAll verursacht werden, jemand kann böswillig aufwachen oder es kann in seltenen Fällen ein automatisches Aufwachen sein (sogenanntes „Pseudo-Aufwecken“). Um zu verhindern, dass nachfolgende Vorgänge ausgeführt werden, wenn die Bedingungen nicht erfüllt sind, ist es daher erforderlich, die Bedingungen nach dem Aufwachen erneut zu beurteilen. Wenn die Bedingungen nicht erfüllt sind, wechseln Sie weiter in den Wartezustand und fahren Sie dann mit den nachfolgenden Vorgängen fort wenn die Voraussetzungen erfüllt sind.

synchronized(lock)  {     while(isEmpty()       lock.wait();       }
Die Situation, in der Wait ohne bedingte Beurteilung aufgerufen wird, ist ernster, da notify möglicherweise vor dem Warten aufgerufen wurde. Daher gibt es nach dem Aufruf von Wait und dem Eintritt in den Warteschlafzustand keine Garantie dafür, dass der Thread aufwacht hoch.

5. Der Synchronisationsbereich ist zu klein oder zu groß.

Wenn der Synchronisierungsumfang zu klein ist, wird der Zweck der Synchronisierung möglicherweise überhaupt nicht erreicht. Wenn der Synchronisierungsumfang zu groß ist, kann die Leistung beeinträchtigt werden. Ein häufiges Beispiel für einen zu kleinen Synchronisationsbereich ist die irrige Annahme, dass zwei synchronisierte Methoden synchronisiert werden, wenn sie gemeinsam aufgerufen werden. Man muss sich daran erinnern, dass Atomic+Atomic!=Atomic ist.

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;      }  }

Das obige ist der detaillierte Inhalt vonSo implementieren Sie Multithread-Programmierung in Java. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Dieser Artikel ist reproduziert unter:yisu.com. Bei Verstößen wenden Sie sich bitte an admin@php.cn löschen