これは、Javaにおける一般的な同時実行性の問題に関する一種の投票です。例としては、古典的なデッドロックや競合状態、あるいはSwingのEDTスレッド化バグが考えられます。考えられる問題の範囲だけでなく、最も一般的な問題にも興味があります。したがって、Javaの同時実行性バグの特定の回答をコメントごとに1つ残し、遭遇したものを見つけたら投票してください。
これは、Javaにおける一般的な同時実行性の問題に関する一種の投票です。例としては、古典的なデッドロックや競合状態、あるいはSwingのEDTスレッド化バグが考えられます。考えられる問題の範囲だけでなく、最も一般的な問題にも興味があります。したがって、Javaの同時実行性バグの特定の回答をコメントごとに1つ残し、遭遇したものを見つけたら投票してください。
回答:
私が見た最も一般的な同時実行の問題は、あるスレッドによって書き込まれたフィールドが別のスレッドによって表示されることが保証されていないことを認識していないことです。これの一般的なアプリケーション:
class MyThread extends Thread {
private boolean stop = false;
public void run() {
while(!stop) {
doSomeWork();
}
}
public void setStop() {
this.stop = true;
}
}
停止が揮発性でないかsetStop
、同期されrun
ていない限り、これは機能するとは限りません。この間違いは特に悪魔的です。99.999%の場合、リーダースレッドが最終的に変更を確認するため、実際には問題になりません。
2つの異なるオープンソースライブラリが次のようなことをしたときに、私の最大の同時実行性の問題の1番目が発生しました。
private static final String LOCK = "LOCK"; // use matching strings
// in two different libraries
public doSomestuff() {
synchronized(LOCK) {
this.work();
}
}
一見すると、これはかなり簡単な同期の例のように見えます。しかしながら; 文字列はJavaでインターンされるため、リテラル文字列"LOCK"
は同じインスタンスであるjava.lang.String
ことがわかります(たとえそれらが互いに完全に異なるように宣言されていても)。結果は明らかに悪いです。
古典的な問題の1つは、同期しているオブジェクトを同期中に変更することです。
synchronized(foo) {
foo = ...
}
その後、他の並行スレッドが別のオブジェクトで同期しており、このブロックは期待する相互排除を提供しません。
ダブルチェックされたロック。概して。
私がBEAで働いていたときに問題を学び始めたパラダイムは、人々がシングルトンを次のようにチェックするというものです。
public Class MySingleton {
private static MySingleton s_instance;
public static MySingleton getInstance() {
if(s_instance == null) {
synchronized(MySingleton.class) { s_instance = new MySingleton(); }
}
return s_instance;
}
}
別のスレッドが同期ブロックに入り、s_instanceがnullでなくなるため、これは機能しません。したがって、自然な変化はそれを作ることです:
public static MySingleton getInstance() {
if(s_instance == null) {
synchronized(MySingleton.class) {
if(s_instance == null) s_instance = new MySingleton();
}
}
return s_instance;
}
Javaメモリモデルがサポートしていないため、これも機能しません。これを機能させるには、s_instanceをvolatileとして宣言する必要があり、それでもJava 5でのみ機能します。
Javaメモリモデルの複雑さに精通していない人は、これを常に混乱させます。
特に反復または複数の操作中に、によって返されたオブジェクトを適切に同期しないCollections.synchronizedXXX()
:
Map<String, String> map = Collections.synchronizedMap(new HashMap<String, String>());
...
if(!map.containsKey("foo"))
map.put("foo", "bar");
それは間違っています。単一の操作であるにもかかわらずsynchronized
、呼び出しcontains
との間のマップの状態はput
別のスレッドによって変更できます。そのはず:
synchronized(map) {
if(!map.containsKey("foo"))
map.put("foo", "bar");
}
またはConcurrentMap
実装あり:
map.putIfAbsent("foo", "bar");
おそらく正確にあなたが求めているものではないかもしれませんが、私が遭遇した最も頻繁な同時実行関連の問題は(おそらくそれが通常のシングルスレッドコードで発生するため)
java.util.ConcurrentModificationException
次のようなことが原因で発生します:
List<String> list = new ArrayList<String>(Arrays.asList("a", "b", "c"));
for (String string : list) { list.remove(string); }
同期されたコレクションは、実際よりも多くの保護を提供し、呼び出し間でロックを保持することを忘れると考えるのは簡単です。私はこの間違いを数回見ました:
List<String> l = Collections.synchronizedList(new ArrayList<String>());
String[] s = l.toArray(new String[l.size()]);
たとえば、上記の2行目ではtoArray()
、size()
メソッドとメソッドはどちらもそれ自体でスレッドセーフですが、はとsize()
は別に評価さtoArray()
れ、リストのロックはこれら2つの呼び出し間で保持されません。
リストからアイテムを同時に削除する別のスレッドでこのコードを実行すると、遅かれ早かれString[]
、リスト内のすべての要素を保持するために必要なサイズよりも大きく、末尾にnull値を持つ新しい戻り値が返されます。Listへの2つのメソッド呼び出しが1行のコードで発生するため、これはどういうわけかアトミック操作であると考えられがちですが、そうではありません。
別の一般的なバグは、貧弱な例外処理です。バックグラウンドスレッドが例外をスローするときに、それを適切に処理しないと、スタックトレースがまったく表示されない場合があります。または、バックグラウンドタスクが実行を停止し、例外の処理に失敗したために再び開始しない場合もあります。
Brian Goetzでクラスを受講するまでは、同期getter
によって変更されたプライベートフィールドの非同期setter
が更新された値を返すことが保証されていないことに気づきませんでした。変数が読み取りと書き込みの両方で同期ブロックによって保護されている場合のみ、変数の最新の値が保証されます。
public class SomeClass{
private Integer thing = 1;
public synchronized void setThing(Integer thing)
this.thing = thing;
}
/**
* This may return 1 forever and ever no matter what is set
* because the read is not synched
*/
public Integer getThing(){
return thing;
}
}
シングルスレッドのコードを書いていると思いますが、変更可能な静的(シングルトンを含む)を使用しています。明らかに、それらはスレッド間で共有されます。これは驚くほど頻繁に発生します。
同期されたブロック内から任意のメソッド呼び出しを行うことはできません。
デイブ・レイは彼の最初の答えでこれに触れました、そして実際に私はまた、同期化されたメソッド内からリスナーのメソッドを呼び出すことと関係があるデッドロックに遭遇しました。より一般的な教訓は、同期されたブロック内からメソッド呼び出しを「実際に」行うべきではないということです-呼び出しが長時間実行されるか、デッドロックになるか、または何でもわかりません。
この場合、通常は一般的に、解決策は同期ブロックのスコープを減らして、コードの重要なプライベートセクションのみを保護することでした。
また、同期ブロックの外側でリスナーのコレクションにアクセスしていたため、コピーオンライトコレクションに変更しました。または、単にコレクションの防御的なコピーを作成することもできます。つまり、通常、未知のオブジェクトのコレクションに安全にアクセスするための代替手段があります。
リクエストごとに設定される可変フィールドがある場合、サーブレットで同時実行の問題が発生しました。ただし、すべてのリクエストに対してサーブレットインスタンスは1つしかないため、これはシングルユーザー環境で完全に機能しましたが、複数のユーザーがサーブレットをリクエストすると、予期しない結果が発生しました。
public class MyServlet implements Servlet{
private Object something;
public void service(ServletRequest request, ServletResponse response)
throws ServletException, IOException{
this.something = request.getAttribute("something");
doSomething();
}
private void doSomething(){
this.something ...
}
}
バグではありませんが、最悪の罪は、他のユーザーが使用するライブラリを提供することですが、どのクラス/メソッドがスレッドセーフであり、どのクラス/メソッドが単一スレッドからのみ呼び出されなければならないかなどを述べていません。
Goetzの本に記載されている同時実行アノテーション(@ ThreadSafe、@ GuardedByなど)をより多くの人が利用する必要があります。
私の最大の問題は常にデッドロックであり、特にロックを保持したままリスナーが起動されることが原因でした。これらの場合、2つのスレッド間で逆ロックを取得するのは非常に簡単です。私の場合、1つのスレッドで実行されているシミュレーションと、UIスレッドで実行されているシミュレーションの視覚化との間。
編集:2番目の部分を別の答えに移動しました。
共有データ構造の可変クラス
Thread1:
Person p = new Person("John");
sharedMap.put("Key", p);
assert(p.getName().equals("John"); // sometimes passes, sometimes fails
Thread2:
Person p = sharedMap.get("Key");
p.setName("Alfonso");
これが発生すると、コードはこの簡略化された例よりもはるかに複雑になります。バグの複製、発見、修正は困難です。おそらく、特定のクラスを不変としてマークし、特定のデータ構造を不変オブジェクトのみを保持するものとしてマークできれば、回避することができます。
文字列リテラルがインターンされ、同じ文字列リテラルを使用してJVM内の他のユーザーと共有されるため、文字列リテラルまたは文字列リテラルによって定義された定数での同期は(潜在的に)問題です。この問題は、アプリケーションサーバーやその他の「コンテナ」シナリオで発生していることを知っています。
例:
private static final String SOMETHING = "foo";
synchronized(SOMETHING) {
//
}
この場合、ロックに文字列「foo」を使用している誰もが同じロックを共有しています。
将来的には、Javaの主な問題は、コンストラクターの可視性保証(の欠如)になると思います。たとえば、次のクラスを作成した場合
class MyClass {
public int a = 1;
}
次に、MyClassのプロパティaを別のスレッドから読み取るだけです。JavaVMの実装とムードに応じて、MyClass.aは0または1になります。今日、「a」が1になる可能性は非常に高いです。しかし、将来のNUMAマシンでは、これは異なる場合があります。多くの人々はこれに気づいておらず、初期化段階でマルチスレッドを気にする必要がないと信じています。
オブジェクトでnotify()またはwait()を呼び出す前に同期を忘れることは、私がよくする最もおかしな間違いです。
ローカルの「new Object()」をミューテックスとして使用します。
synchronized (new Object())
{
System.out.println("sdfs");
}
これは役に立たない。
もう1つの一般的な「同時実行性」の問題は、まったく必要ない場合に同期コードを使用することです。たとえば、プログラマーは(StringBuffer
またはjava.util.Vector
メソッドのローカル変数として)を使用しているのを見ています。
ロック保護されているが、通常は連続してアクセスされる複数のオブジェクト。ロックが異なるコードで異なる順序で取得され、デッドロックが発生する場合がいくつかあります。
this
内部クラスのがthis
外部クラスのでないことを認識していません。通常、を実装する匿名の内部クラスですRunnable
。根本的な問題は、同期はすべてObject
のの一部であるため、静的な型チェックが事実上存在しないことです。これはusenetで少なくとも2回見たことがあります。また、Brian Goetz'z Java Concurrency in Practiceにも表示されます。
BGGAクロージャーはthis
、クロージャーが存在しないため(this
外部クラスを参照)、この問題の影響を受けません。非this
オブジェクトをロックとして使用すると、この問題などが回避されます。
ロック用の静的変数などのグローバルオブジェクトの使用。
これは、競合のために非常に悪いパフォーマンスにつながります。
正直?が登場する前、java.util.concurrent
私が日常的に遭遇した最も一般的な問題は、「スレッドスラッシング」と呼ばれるものでした。並行性のためにスレッドを使用しているが、生成するスレッドが多すぎて、スラッシングが発生するアプリケーションです。