ホームページ >Java >&#&チュートリアル >Java でマルチスレッド プログラミングを実装する方法
1. コンストラクターでスレッドを開始する
この問題は多くのコードで見られましたが、コンストラクターで次のようなスレッドを開始します:
public class A{ public A(){ this.x=1; this.y=2; this.thread=new MyThread(); this.thread.start(); } }
これによりどのような問題が発生するのでしょうか?クラス A を継承するクラス B がある場合、Java クラスの初期化の順序に従って、B のコンストラクターが呼び出される前に必ず A のコンストラクターが呼び出され、B が完全に初期化される前にスレッド thread も開始されます。クラス A でいくつかの変数を使用する場合、B のコンストラクターでこれらの変数に新しい値を割り当てる可能性があるため、期待した値が使用できない可能性があります。つまり、現時点ではこれらの変数を使用するスレッドが 2 つありますが、これらの変数は同期されていません。
この問題を解決するには 2 つの方法があります: A を Final として継承不可に設定するか、コンストラクターにスレッドを配置する代わりに別の start メソッドを提供してスレッドを開始します。
2. 不完全な同期
変数を同期する効果的な方法は、変数を synchronized で保護することであることは誰もが知っています。同期とは、オブジェクト ロックまたはクラス ロックの場合があります。 . 、それはクラス メソッドであるかインスタンス メソッドであるかによって異なります。ただし、メソッド A で変数を同期する場合、弱い可視性を許可したり、エラー値を生成したりしない限り、変数が表示される他の場所でも変数を同期する必要があります。これに似たコード:
class A{ int x; public int getX(){ return x; } public synchronized void setX(int x) { this.x=x; } }
x の setter メソッドは同期されていますが、getter メソッドは同期されていないため、getX を通じて他のスレッドによって取得された x が *** 値であるという保証はありません。実際、ここでの setX の同期は必要ありません。int の書き込みはアトミックであり、JVM 仕様で保証されており、複数の同期には意味がありません。もちろん、これが int ではなく double または long の場合は、その場合、getX と setX の両方を同期する必要があります。double と long は両方とも 64 ビットであり、書き込みと読み取りは 2 つの 32 ビットに分割されます (これは jvm の実装によって異なります。一部の jvm 実装では、long と long Double の読み取りが保証される場合があります)。および書き込みはアトミックです)、アトミック性は保証されません。上記のようなコードは、変数を volatile として宣言することで実際に解決できます。
3. オブジェクトをロックとして使用すると、オブジェクトの参照が変更され、同期が失敗します。
これも非常によくある間違いで、次のコードに似ています:
synchronized(array[0]) { ...... array[0]=new A(); ...... }
同期ブロックはロックとして配列[0]を使用しますが、配列[0]は変更されます。同期されたブロックが指す参照。このシナリオを分析すると、最初のスレッドが配列 [0] のロックを取得し、2 番目のスレッドが配列 [0] を取得できないため待機し、配列 [0] の参照を変更した後、3 番目のスレッドが新しい配列のロックを取得します。 [0]では、1番目と3番目のスレッドが保持するロックが異なり、同期と相互排除の目的はまったく達成されていません。このようなコードの変更には通常、ロックを最終変数として宣言するか、ビジネスに依存しないロック オブジェクトを導入して、同期ブロック内で参照が変更されないようにします。
4. Wait() はループ内では呼び出されません。
wait と Notice は、条件変数の実装に使用されます。条件の変更がアトミックで表示されるようにするには、wait と Notice を同期ブロックで呼び出す必要があることはご存知かもしれません。同期されているものの、ループ内で wait を呼び出していないコードをよく見かけます。代わりに、if を使用するか、条件判断をまったく使用しません:
synchronized(lock) { if(isEmpty() lock.wait(); }
条件判断は if を使用します。これによりどのような問題が発生しますか?原因?条件を判断する前に、notify または NoticeAll を呼び出しても、条件は満たされており、待ち時間は発生しません。条件が満たされない場合は、wait() メソッドが呼び出され、ロックが解放され、待機スリープ状態になります。通常の状況、つまり条件が変更された後にスレッドが起動される場合には問題はなく、条件が満たされていれば次の論理演算が実行され続けます。問題は、スレッドが誤って、または悪意によって呼び起こされる可能性があることであり、条件が再度判断されないため、条件が満たされない場合、スレッドは次の操作を実行します。予期しないウェイクアップは、notifyAll の呼び出しによって引き起こされる場合、誰かが悪意を持ってウェイクアップする場合、またはまれに自動ウェイクアップである場合があります (「疑似ウェイクアップ」と呼ばれます)。そのため、条件を満たさない場合には以降の操作が行われないようにするために、覚醒後に再度条件を判断し、条件を満たさない場合には待機状態を継続し、その後の操作を行う必要があります。条件が満たされた場合。
synchronized(lock) { while(isEmpty() lock.wait(); }
条件判断なしで wait を呼び出す状況はさらに深刻です。notify が待機前に呼び出された可能性があるため、wait を呼び出して待機スリープ状態に入った後、スレッドがウェイクアップする保証はありません。
5. 同期範囲が小さすぎるか、または大きすぎます。
同期の範囲が小さすぎると、同期の目的がまったく達成されない可能性があり、同期の範囲が大きすぎると、パフォーマンスに影響を与える可能性があります。同期スコープが小さすぎる一般的な例としては、2 つの同期メソッドを一緒に呼び出すと同期されるという誤った認識が挙げられます。
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; } }以上がJava でマルチスレッド プログラミングを実装する方法の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。