JAvaマルチスレッドプログラミングの一般的な落とし穴(回転~~~)

227748 ワード

1、コンストラクション関数でスレッドを起動する
多くのコードでこのような問題を見ています.コンストラクション関数でスレッドを起動します.

  
  
  
  
  1. public class A{  
  2.    public A(){  
  3.       this.x=1;  
  4.       this.y=2;  
  5.       this.thread=new MyThread();  
  6.       this.thread.start();  
  7.    }  
  8.      
  9. }    

これはどんな問題を引き起こすのでしょうか.クラスBがクラスAを継承し、javaクラスの初期化の順序に従って、Aのコンストラクション関数が必ずBのコンストラクション関数呼び出しの前に呼び出されると、threadスレッドもBが完全に初期化される前に起動し、threadが実行されるとクラスAのいくつかの変数が使用され、Bのコンストラクション関数にこれらの変数に新しい値を割り当てる可能性があるため、Bのコンストラクション関数では予想外の値が使用される可能性があります.つまり、この時点で2つのスレッドがこれらの変数を使用していますが、これらの変数は同期されていません.
この問題を解決するには2つの方法がある:Aをfinalに設定し、継承できない;あるいは、構造関数ではなくスレッドを起動するための個別のstartメソッドを提供します.
2、不完全な同期
変数の同期に有効な方法はsynchronizedで保護され、synchronizedはオブジェクトロックかクラスロックか、クラスメソッドかインスタンスメソッドかがわかります.しかし、ある変数をAメソッドで同期すると、変数が現れる他の場所でも、弱い可視性を許可したり、エラー値を生成したりしない限り、同期する必要があります.このようなコード:

  
  
  
  
  1. class A{  
  2.   int x;  
  3.   public int getX(){  
  4.      return x;  
  5.   }  
  6.   public synchronized void setX(int x)  
  7.   {  
  8.      this.x=x;  
  9.   }  
  10. }     

xのsetterメソッドには同期があるが,getterメソッドがないと,他のスレッドがgetXで得たxが最新の値であることは保証できない.実際、ここでのsetXの同期は必要ありません.intへの書き込みは原子的であるため、JVM仕様は、複数の同期に意味がないことを保証しています.もちろん、ここでintではなくdoubleまたはlongであれば、getXとsetXは同期する必要があります.doubleとlongは64ビットであり、書き込みと読み取りは2つの32ビットに分けて行われるためです(この点はjvmの実装に依存し、あるjvm実装ではlongとdoubleのread、writeが原子であることを保証する可能性があります).原子性は保証されません.上記のようなコードは、変数をvolatileと宣言することで解決できます.3、あるオブジェクトを使用してロックすると、オブジェクトの参照が変更され、同期が無効になります.
これもよくあるエラーです.次のコードに似ています.

  
  
  
  
  1. synchronized(array[0])  
  2. {  
  3.    ......  
  4.    array[0]=new A();  
  5.    ......  
  6. }    

同期ブロックはarray[0]をロックとして使用するが、同期ブロックではarray[0]が指す参照が変更される.このシナリオを解析すると,最初のスレッドはarray[0]のロックを取得し,2番目のスレッドはarray[0]を取得できずに待機し,array[0]の参照を変更すると,3番目のスレッドは新しいarray[0]のロックを取得し,1番目と3番目の2つのスレッドが持つロックは異なり,同期反発の目的は全く達成されなかった.このようなコードの変更は、通常、ロックをfinal変数として宣言したり、トラフィックに関係のないロックオブジェクトを導入したりして、同期ブロック内で参照が変更されないことを保証します.
4、ループでwait()が呼び出されていません.
waitとnotifyは条件変数を実現するために使用され、条件の変更が原子性と可視性を保証するために、同期ブロックでwaitとnotifyを呼び出す必要があることを知っているかもしれません.多くのコードが同期されているのに、ループでwaitを呼び出すのではなく、ifを使用して条件判断さえしていないことがよく見られます.

  
  
  
  
  1. synchronized(lock)  
  2. {  
  3.    if(isEmpty()  
  4.      lock.wait();  
  5.      
  6. }  
  7.      

条件の判断はifを使うことですが、これはどんな問題を引き起こすのでしょうか.条件を判断する前にnotifyまたはnotifyAllを呼び出す可能性があります.条件が満たされているので、待つことはありません.これは問題ありません.条件が満たされていない場合、wait()メソッドが呼び出され、lockロックが解放され、待機スリープ状態に入る.スレッドが通常の場合、すなわち条件が変更された後に起動される場合、条件が満たされて次の論理操作が継続されるという問題はありません.問題は,スレッドが予期せぬ悪意により呼び出される可能性があり,条件判断が再行われていないため,条件が満たされていない場合,スレッドは後続の動作を実行することである.予期せぬ目覚ましの場合、notifyAllが呼び出されたのか、悪意のある目覚ましがあったのか、少ない場合の自動目覚まし(「偽目覚まし」と呼ばれる)かもしれない.従って、このような条件が満たされていない場合に後続操作を実行することを防止するためには、起動後に条件を再判断し、条件が満たされていない場合には、待機状態に入り続け、条件が満たされてから後続操作を行う必要がある.

  
  
  
  
  1. synchronized(lock)  
  2. {  
  3.    while(isEmpty()  
  4.      lock.wait();  
  5.      
  6. }     

条件判断を行わずにwaitを呼び出す場合は、待機前にnotifyが呼び出された可能性があるため、waitが呼び出された後に待機休眠状態に入るとスレッドが蘇る保証がない.
5、同期の範囲が小さすぎたり、大きすぎたりします.
同期の範囲が小さすぎて、同期の目的を完全に達成していない可能性があります.同期の範囲が大きすぎると、パフォーマンスに影響を与える可能性があります.同期範囲が小さすぎる一般的な例は、2つの同期メソッドが一緒に呼び出されても同期すると勘違いしていることであり、Atomic+Atomicであることを覚えておく必要があります!=Atomic.

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

これは典型的なエラーであり、mapはスレッドセキュリティであり、containskeyとputメソッドもスレッドセキュリティであるが、2つのスレッドセキュリティのメソッドが組み合わせて呼び出されると、スレッドセキュリティとは限らない.containsKeyとputの間には、他のスレッドが先にputをaに入れる可能性があるため、他のスレッド設定の値を上書きし、値の損失を招く可能性があります.この問題を解決する方法は、オブジェクトロックが再読み込み可能であるため、スレッドセキュリティメソッドの上で同じロックオブジェクトを再同期することは問題ありません.

  
  
  
  
  1. Map map = Collections.synchronizedMap(new HashMap());  
  2. synchronized (map) {  
  3.      if (!map.containsKey("a")) {  
  4.          map.put("a", value);  
  5.      }  
  6.  }    

ロックの範囲を大きくしても、同じロックが使用されていることを保証しなければなりません.そうしないと、デッドロックになる可能性があります.Collections.synchronizedMap(new HashMap()で使用されるロックはmapそのものなので問題ありません.もちろん、上記の状況では、同じ目的を達成し、スレッドのセキュリティを満たすためにputIfAbsentメソッドを持つConcurrentHashMapの使用が推奨されています.
同期範囲が大きすぎる例も多い.例えば、同期ブロックにおいてnewが大きいオブジェクトや、呼び出しに時間がかかるIO操作(データベース、webserviceなど)などである.料金を調整せざるを得ない場合は、必ずタイムアウト時間を指定します.例えば、URLConnectionでinvokeのURLに行く場合はconnect timeoutとread timeoutを設定し、ロックが独占的に解放されないようにします.同期範囲が大きすぎる場合、スレッドのセキュリティを保証する前提で、同期ブロックから同期する必要のない操作を削除します.
6、volatileを正しく使う
jdk 5がvolatileの意味を修正した後,volatileは軽量レベルの同期戦略として多くの使用を得た.volatileの厳密な定義はjvm specを参照し、ここではvolatileが何ができるか、何に使えないかから検討する.
volatileは何に使えますか?
1)状態フラグ、アナログ制御機構.スレッドが停止するかどうかを制御するなど、一般的な用途:

  
  
  
  
  1. private volatile boolean stopped;  
  2. public void close(){  
  3.    stopped=true;  
  4. }  
  5.  
  6. public void run(){  
  7.  
  8.    while(!stopped){  
  9.       //do something  
  10.    }  
  11.      

do somethingにブロック呼び出しなどがないことを前提とします.volatileはstopped変数の可視性を保証し、runメソッドでstopped変数は常にmain memoryの最新値を読み出す.
2)DLCの問題を修正するなど、安全なリリース.

  
  
  
  
  1. private volatile IoBufferAllocator instance;  
  2. public IoBufferAllocator getInsntace(){  
  3.     if(instance==null){  
  4.         synchronized (IoBufferAllocator.class) {  
  5.             if(instance==null)  
  6.                 instance=new IoBufferAllocator();  
  7.         }  
  8.     }  
  9.     return instance;  

3)コストの低い読み書きロック

  
  
  
  
  1. public class CheesyCounter {  
  2.     private volatile int value;  
  3.  
  4.     public int getValue() { return value; }  
  5.  
  6.     public synchronized int increment() {  
  7.         return value++;  
  8.     }  
  9. }  

synchronizedは更新の原子性を保証し,volatileはスレッド間の可視性を保証する.
volatileは何に使えませんか?
1)カウンタには使用できません

  
  
  
  
  1. public class CheesyCounter {  
  2.     private volatile int value;  
  3.  
  4.     public int getValue() { return value; }  
  5.  
  6.     public int increment() {  
  7.         return value++;  
  8.     }  

value++には、読み取り、修正、書き込みの3つの操作があるため、volatileはこのシーケンスが原子であることを保証できません.valueの変更操作はvalueの最新値に依存します.この問題を解決する方法はincrement法を同期するか,AtomicInteger原子クラスを用いることができる.
2)その他の変数との不変式の構成
典型的な例は、制約lower
  1. public class NumberRange {  
  2.     private volatile int lower, upper;  
  3.  
  4.     public int getLower() { return lower; }  
  5.     public int getUpper() { return upper; }  
  6.  
  7.     public void setLower(int value) {   
  8.         if (value > upper)   
  9.             throw new IllegalArgumentException();  
  10.         lower = value;  
  11.     }  
  12.  
  13.     public void setUpper(int value) {   
  14.         if (value < lower)   
  15.             throw new IllegalArgumentException();  
  16.         upper = value;  
  17.     }  
  18. }  

lowerとupperはvolatileとして宣言されていますが、setLowerとsetUpperはスレッドセキュリティ方法ではありません.初期状態を(0,5)と仮定し,setLower(4)とsetUpper(3)を同時に呼び出し,2つのスレッドが交差して行われ,最終結果は(4,3)であり,制約に違反する可能性がある.この問題を修正する方法はsetLowerとsetUpperを同期することです.

  
  
  
  
  1. public class NumberRange {  
  2.     private volatile int lower, upper;  
  3.  
  4.     public int getLower() { return lower; }  
  5.     public int getUpper() { return upper; }  
  6.  
  7.     public synchronized void setLower(int value) {   
  8.         if (value > upper)   
  9.             throw new IllegalArgumentException();  
  10.         lower = value;  
  11.     }  
  12.  
  13.     public synchronized void setUpper(int value) {   
  14.         if (value < lower)   
  15.             throw new IllegalArgumentException();  
  16.         upper = value;  
  17.     }  
  18. }  :http://developer.51cto.com/art/200906/129435.htm