これは、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;
}
}
StopがvolatileまたはsetStop
およびrun
がsynchronizedでない限り、これが機能することは保証されません。この間違いは特に悪魔的なものです。99.999%では、読者スレッドが最終的に変更を確認するため、実際には問題になりませんが、彼がそれをどのくらい早く確認したかはわかりません。
私の#1最も痛い2つの異なるオープンソースライブラリが次のようなことをしたときにこれまでに並行性の問題が発生しました。
private static final String LOCK = "LOCK"; // use matching strings
// in two different libraries
public doSomestuff() {
synchronized(LOCK) {
this.work();
}
}
一見、これは非常に簡単な同期の例のように見えます。しかしながら; Javaでは文字列がinternedであるため、リテラル文字列"LOCK"
はJava.lang.String
の同じインスタンスであることがわかります(互いに完全に異なる宣言されている場合でも)。明らかに悪いです。
古典的な問題の1つは、同期中に同期中のオブジェクトを変更することです。
synchronized(foo) {
foo = ...
}
他の同時スレッドは別のオブジェクトで同期しており、このブロックは期待する相互排除を提供しません。
一般的な問題は、複数のスレッドからCalendarやSimpleDateFormatなどのクラスを使用することです(多くの場合、それらを静的変数にキャッシュすることにより)。これらのクラスはスレッドセーフではないため、マルチスレッドアクセスは最終的に、一貫性のない状態で奇妙な問題を引き起こします。
ダブルチェックロック。概して。
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 Memory Modelではサポートされていないため、これも機能しません。 s_instanceをvolatileとして宣言して動作させる必要がありますが、それでもJava 5でのみ動作します。
Java Memory Modelの複雑さに詳しくない人は、これを台無しにしています常時。
正しくない同期Collections.synchronizedXXX()
によって返されるオブジェクト、特に反復または複数の操作中:
Map<String, String> map = Collections.synchronizedMap(new HashMap<String, String>());
...
if(!map.containsKey("foo"))
map.put("foo", "bar");
それはwrongです。単一の操作は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); }
私が作業している場所で見られる最も一般的なバグは、プログラマーがEDT上でサーバー呼び出しなどの長い操作を実行し、GUIを数秒間ロックしてアプリが応答しなくなることです。
同期されたコレクションは、実際に行うよりも多くの保護を与えると考えやすく、呼び出し間でロックを保持することを忘れることがあります。私は何度かこの間違いを見てきました。
List<String> l = Collections.synchronizedList(new ArrayList<String>());
String[] s = l.toArray(new String[l.size()]);
たとえば、上の2行目では、toArray()
メソッドとsize()
メソッドは両方ともスレッドセーフですが、size()
はtoArray()
とは別に評価されます。リストのロックは、これら2つの呼び出しの間では保持されません。
このコードを別のスレッドで実行するとconcurrentlyリストから項目を削除すると、遅かれ早かれ、新しいString[]
が返され、すべての要素を保持するのに必要なサイズよりも大きくなりますリスト、および末尾にヌル値があります。 Listへの2つのメソッド呼び出しは1行のコードで発生するため、これはなんとなくアトミックな操作であると考えるのは簡単ですが、そうではありません。
ループ内でwait()(またはCondition.await())を忘れ、実際に待機条件が真であることを確認します。これがないと、偽のwait()ウェイクアップによるバグに遭遇します。正規の使用法は次のとおりです。
synchronized (obj) {
while (<condition does not hold>) {
obj.wait();
}
// do stuff based on condition being true
}
もう1つの一般的なバグは、例外処理の低さです。バックグラウンドスレッドが例外をスローするときに、適切に処理しないと、スタックトレースがまったく表示されない場合があります。または、例外の処理に失敗したため、バックグラウンドタスクの実行が停止し、再び開始することはありません。
Brian Goetzのクラスを受講するまで、プライベートフィールドの非同期getter
が同期setter
によって変更されたことはnever更新された値を返すことが保証されています。 読み取りと書き込みの両方の同期ブロックによって変数が保護されている場合のみ、変数の最新値の保証が得られます。
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;
}
}
あなたはシングルスレッドのコードを書いていると思っていますが、可変の静的(シングルトンを含む)を使用しています。明らかに、それらはスレッド間で共有されます。これは驚くほど頻繁に起こります。
同期ブロック内から任意のメソッド呼び出しを行うべきではありません。
Dave Rayは彼の最初の答えでこれに触れました。実際、同期メソッド内からリスナーのメソッドを呼び出すことに関係するデッドロックにも遭遇しました。より一般的な教訓は、メソッドの呼び出しを同期ブロック内から「ワイルドに」しないことだと思います-呼び出しが長時間実行されるか、デッドロックになるかなどはわかりません。
この場合、通常は一般的に、ソリューションはコードの重要なprivateセクションを保護するために同期ブロックのスコープを縮小することでした。
また、同期ブロック外のリスナーのコレクションにアクセスするようになったため、コピーオンライトコレクションに変更しました。または、単にコレクションの防御的なコピーを作成することもできます。ポイントは、通常、不明なオブジェクトのコレクションに安全にアクセスするための代替手段があります。
私が遭遇した最新の並行性関連のバグは、コンストラクターでExecutorServiceを作成したオブジェクトでしたが、オブジェクトが参照されなくなると、ExecutorServiceをシャットダウンすることはありませんでした。したがって、数週間にわたって千のスレッドがリークし、最終的にシステムがクラッシュしました。 (技術的には、クラッシュしませんでしたが、実行を続けている間は正常に機能しなくなりました。)
技術的には、これは同時性の問題ではないと思いますが、Java.util.concurrencyライブラリの使用に関連する問題です。
特にMapsに対する不均衡な同期は、かなり一般的な問題のようです。多くの人は、同期をマップ(ConcurrentMapではなく、HashMapと言います)に置き、取得を同期しないことで十分であると考えています。ただし、これにより、再ハッシュ中に無限ループが発生する可能性があります。
ただし、読み取りと書き込みで状態を共有している場所であればどこでも同じ問題(部分的な同期)が発生する可能性があります。
各リクエストによって設定される変更可能なフィールドがある場合、サーブレットで並行性の問題が発生しました。しかし、すべてのリクエストに対してサーブレットインスタンスは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");
これが発生すると、コードはこの単純化された例よりもはるかに複雑になります。バグの複製、発見、修正は困難です。特定のクラスを不変として、特定のデータ構造を不変オブジェクトのみを保持するものとしてマークすることができれば、おそらく回避できます。
将来、Javaの主な問題は、コンストラクターの(保証されない)可視性の保証になると思います。たとえば、次のクラスを作成する場合
class MyClass {
public int a = 1;
}
次に、MyClassのプロパティaを別のスレッドから読み取ります。JavaVMの実装とムードに応じて、MyClass.aは0または1になります。今日、「a」が1になる可能性は非常に高くなっています。しかし、将来のNUMAマシンではこれが異なる場合があります。多くの人はこれを知らず、初期化段階でマルチスレッドを気にする必要がないと考えています。
文字列リテラルまたは文字列リテラルによって定義された定数の同期は、(潜在的に)文字列リテラルがインターンされ、同じ文字列リテラルを使用してJVM内の他のユーザーと共有されるため、問題になります。この問題は、アプリケーションサーバーやその他の「コンテナ」シナリオで発生していることを知っています。
例:
private static final String SOMETHING = "foo";
synchronized(SOMETHING) {
//
}
この場合、文字列「foo」を使用してロックする人は誰でも同じロックを共有しています。
もう1つの一般的な「同時実行性」の問題は、同期コードがまったく必要ないときに使用することです。たとえば、StringBuffer
またはJava.util.Vector
(メソッドのローカル変数として)を使用しているプログラマーはまだいます。
よくある間違いは、オブジェクトでnotify()またはwait()を呼び出す前に同期するのを忘れることです。
ローカルの「new Object()」をミューテックスとして使用します。
synchronized (new Object())
{
System.out.println("sdfs");
}
これは無意味です。
ロック保護されているが、通常は連続してアクセスされる複数のオブジェクト。ロックが異なるコードによって異なる順序で取得され、デッドロックが発生する場合がいくつかあります。
内部クラスのthis
が外部クラスのthis
ではないことに気づかない。通常、Runnable
を実装する匿名内部クラス内。根本的な問題は、同期がすべてのObject
sの一部であるため、静的型チェックが事実上存在しないことです。これをusenetで少なくとも2回見たことがありますが、Brian Goetz'z Java Concurrency in Practiceにも表示されます。
BGGAクロージャーは、クロージャーにthis
がないので、この問題の影響を受けません(this
は外部クラスを参照します)。 this
以外のオブジェクトをロックとして使用すると、この問題やその他の問題を回避できます。
Java.awt.EventQueue.invokeAndWait
を認識しない ロックを保持しているかのように動作する (イベントディスパッチスレッド、EDTへの排他的アクセス)。デッドロックの素晴らしいところは、それがめったに起こらない場合でも、jstackなどでスタックトレースを取得できることです。広く使用されている多くのプログラムでこれを確認しました(Netbeansで一度しか発生していない問題の修正は、次のリリースに含める必要があります)。
1)私が遭遇したよくある間違いには、同期されたCollectionクラスの繰り返しが含まれます。反復子を取得する前および反復中に手動で同期する必要があります。
2)もう1つの間違いは、ほとんどの教科書は、クラススレッドを安全にすることは、すべてのメソッドにsynchronizedを追加するだけだという印象を与えることです。それ自体は保証ではありません-特定のクラスの整合性を保護するだけですが、結果は依然として不確定になる可能性があります。
3)時間のかかる操作を同期ブロックに入れすぎると、パフォーマンスが非常に低下することがよくあります。幸いなことに、同時実行パッケージのFutureパターンはその日を安全にします。
4)可変オブジェクトをキャッシュしてパフォーマンスを向上させると、多くの場合マルチスレッドの問題も発生します(そして、あなたが唯一のユーザーであると想定するため、追跡が非常に困難な場合があります)。
5)複数の同期オブジェクトの使用は慎重に処理する必要があります。
このコードを試してください。
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 ...
}
}
ロックのための静的変数などのグローバルオブジェクトの使用。
これは、競合のために非常に悪いパフォーマンスにつながります。
正直? Java.util.concurrent
の出現前、私が日常的に遭遇した最も一般的な問題は、「スレッドスラッシング」と呼ばれるものでした。
Java RMIを開始すると、バックグラウンドタスクが実行され、ガベージコレクターが60秒ごとに実行されます。それ自体は良いことかもしれませんが、RMIサーバーが直接起動されたのではなく、使用するフレームワーク/ツール(JRunなど)によって起動された可能性があります。また、RMIは実際には何にも使用されていない可能性があります。
最終結果は、1分間に1回System.gc()呼び出しです。負荷の高いシステムでは、ログに次の出力が表示されます。60秒のアクティビティに続いて長いgcポーズが続き、60秒のアクティビティに続いて長いgcポーズが続きます。これはスループットにとって致命的です。
解決策は、-XX:+ DisableExplicitGCを使用して明示的なgcをオフにすることです
長時間実行されるスレッドを管理するオブジェクトに明確に定義されたライフサイクルメソッドを提供できない。 init()とdestroy()という名前のメソッドのペアを作成するのが好きです。アプリが正常に終了できるように、実際にdestroy()を呼び出すことも重要です。
同時に呼び出すことができる別のメソッドが独自の目的で同じインスタンス変数を使用する場合、データをヘルパーメソッドに渡す「労力を節約する」ためにインスタンス変数にデータを保存するメソッド。
代わりに、データは、同期された呼び出しの間、メソッドパラメーターとして渡される必要があります。これは私の最悪の記憶をほんの少し単純化したものです:
public class UserService {
private String userName;
public String getUserName() {
return userName;
}
public void login(String name) {
this.userName = name;
doLogin();
}
private void doLogin() {
userDao.login(getUserName());
}
public void delete(String name) {
this.userName = name;
doDelete();
}
private void doDelete() {
userDao.delete(getUserName());
}
}
論理的に言えば、ログインとログアウトのメソッドを同期する必要はありません。しかし、現状のまま書かれていると、あらゆる種類の楽しいカスタマーサービスコールを体験できます。
待機と通知で異なるロックオブジェクトを使用する同時実行性の問題。
私はwait()とnotifyAll()メソッドを使用しようとしていましたが、ここに私がどのように使用して地獄に落ちたかがあります。
スレッド1
Object o1 = new Object();
synchronized(o1) {
o1.wait();
}
そして、他のスレッドで。スレッド-2
Object o2 = new Object();
synchronized(o2) {
o2.notifyAll();
}
Thread1はo1で待機し、o1.notifyAll()を呼び出すべきであるThread2はo2.notifyAll()を呼び出しています。スレッド1は決して起動しません。
そして、同期ブロック内でwait()またはnotifyAll()を呼び出さず、ブロックのシンクロナイズに使用されるのと同じオブジェクトを使用してそれらを呼び出さないという一般的な問題を回避します。
Object o2 = new Object();
synchronized(o2) {
notifyAll();
}
NotifyAll()を呼び出したスレッドはこのオブジェクトを使用してnotifyAll()を呼び出しましたが、このロックオブジェクトの所有者ではないため、これによりIllegalMonitorStateExceptionが発生します。ただし、現在のスレッドはo2ロックオブジェクトの所有者です。
すべてのスレッドをビジー状態に保ちます。
これは、ロック構造を悪用した他の人のコードの問題を修正する必要がある場合に最も頻繁に発生します。最近、私の同僚は、リーダー/ライターのロックを振りかけるのが非常に楽しいことに気付いているようですが、ちょっとした考えではその必要性が完全になくなります。
私のコードでは、スレッドをビジー状態に保つことはそれほど明白ではありませんが、挑戦的です。新しいデータ構造の作成や、ロックが使用されたときに競合しないように慎重にシステムを設計するなど、アルゴリズムをより深く考える必要があります。
同時実行エラーを解決するのは簡単です-ロックの競合を回避する方法を見つけ出すのは難しい場合があります。
オブジェクトのfinalize/release/shutdown/destructorメソッドおよび通常の呼び出し中の競合状態。
Javaから、COMオブジェクトやFlashプレーヤーなど、閉じる必要のあるリソースと多くの統合を行います。開発者は常にこれを適切に行うことを忘れ、スレッドがシャットダウンされたオブジェクトを呼び出すことになります。
Java 5以降はThread.getUncaughtExceptionHandlerがありますが、ExecutorService/ThreadPoolが使用されている場合、このUncaughtExceptionHandlerは呼び出されません。
少なくとも、ExcutorServiceが機能するUncaughtExceptionHandlerを取得できませんでした。
SwingスレッドではなくワーカースレッドでSwing UIコンポーネント(通常はプログレスバー)を更新します(もちろんSwingUtilities.invokeLater(Runnable)
を使用する必要がありますが、これを忘れるとバグが表面化するのに時間がかかる可能性があります) )
カウントダウンラッチを作成したI/Oスレッドから疑似デッドロックに遭遇しました。問題の非常に単純化されたバージョンは次のようなものです。
パブリッククラスMyReaderはRunnableを実装します{ private final CountDownLatch done = new CountDownLatch(1); private volatile isOkToRun = true; public void run(){ while(isOkToRun){ sendMessage(getMessaage()); } done.countDown(); } public void stop(){ isOkToRun = false; done.await(); } }
Stop()の考え方は、スレッドが終了するまで戻りませんでした。したがって、システムが戻ったとき、システムは既知の状態にありました。 sendMessage()がstop()を呼び出し、永久に待機しない限り、これは問題ありません。 stop()がRunnableから呼び出されない限り、すべてが期待どおりに機能します。ただし、大規模なアプリケーションでは、Runnableのスレッドのアクティビティが明らかでない場合があります。
解決策は、数秒のタイムアウトでawait()を呼び出し、タイムアウトが発生したときにスタックダンプと苦情をログに記録することでした。これにより、可能な場合は望ましい動作が維持され、発生したコーディングの問題が明らかになりました。
最初から同期の問題を回避しようとする私の2つのセント—次の問題/臭いに注意してください。
new Thread()
の呼び出しは匂いです。代わりに、専用のExecutorServicesを使用します。これにより、アプリケーションの全体的なスレッド化の概念(1を参照)について考え、他の人にそれに従うように促します。AtomicBoolean
et al、同期されたコレクションなど)繰り返しますが、スレッドセーフが特定のコンテキストで重要であるかどうかを意識的に決定し、盲目的に使用しないでください。私が遭遇した最大の問題は、マルチスレッドサポートを後から追加する開発者です。
public class ThreadA implements Runnable {
private volatile SharedObject obj;
public void run() {
while (true) {
obj = new SharedObject();
obj.setValue("Hallo");
}
}
public SharedObject getObj() {
return obj;
}
}
ここで(特に)指摘しようとしている問題は、値 "Hallo"を設定する前にSharedObject objのフラッシュが発生することです。これは、getObj()のコンシューマーがgetValue()がnullを返すインスタンスを取得する可能性があることを意味します。
public class ThreadB implements Runnable {
ThreadA a = null;
public ThreadB(ThreadA a) {
this.a = a;
}
public void run() {
while (true) {
try {
System.out.println("SharedObject: " + a.getObj().getVal());
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class SharedObject {
private String val = null;
public SharedObject() {
}
public String getVal() {
return val;
}
public void setVal(String val) {
this.val = val;
}
}
Javaで見つけた厄介な落とし穴は、複数のスレッドが同期なしでHashMapにアクセスすることです。読み取り中と書き込み中の場合、リーダーが無限ループに陥る可能性が高くなります(バケットノードリスト構造がループリストに破損します)。
明らかに、最初にこれを行うべきではありません(ConcurrentHashMapまたはCollections.synch ...ラッパーを使用)そのようなマップを含むユーティリティクラスは、スタックの数レベル下にあり、誰もそれを考えていません。
Functional Javaでのアクターの実装を支援し、マルチコアマシンで数百万のスレッドをベンチマークします。
Javaで最も頻繁に発生する同時実行性の問題は、実際にはスレッドセーフではありませんが、これまでのところ実際に動作しているように見えるコードです。小さなエラーのおかげで、それは時限爆弾になり、ほとんどすべての場合、それはあなたには明らかではないので、事前にそれを知りません。テスト中に通常の障害のあるコードが失敗することを期待していますが、並行コードは多くの場合、最終的にしか再現できません。
while(true)
{
if (...)
break
doStuff()
}
開発者がwhileループを書くときは常に、独自のコードの「リソースコミット」を見逃します。
つまり、そのブロックが終了しない場合、アプリケーション、さらにはシステムがロックアップして死ぬ可能性があります。単純なwhile(fantasy_land)...if(...) break
のためです。