web-dev-qa-db-ja.com

ArrayListとマルチスレッドin Java

同期されていないコレクション、たとえばArrayListが問題を引き起こすのはどのような状況ですか?私は何も考えられません、誰かがArrayListが問題を引き起こし、Vectorがそれを解決する例を教えてもらえますか? 1つの要素を持つ配列リストを変更する2つのスレッドを持つプログラムを作成しました。一方のスレッドは「bbb」を配列リストに入れ、もう一方のスレッドは「aaa」を配列リストに入れます。文字列が半分変更されているインスタンスは実際には表示されません。ここで正しい方向に進んでいますか?

また、複数のスレッドが実際には同時に実行されておらず、1つのスレッドがしばらく実行され、その後別のスレッドが実行されると言われたことを覚えています(単一のCPUを搭載したコンピューターで)。それが正しければ、2つのスレッドが同じデータに同時にアクセスするにはどうすればよいでしょうか。何かを変更している途中でスレッド1が停止し、スレッド2が開始されるのではないでしょうか。

よろしくお願いします。

23
user433500

適切な同期なしで(たとえば)ArrayListを使用すると、問題が発生する可能性がある3つの側面があります。

最初のシナリオは、2つのスレッドがたまたまArrayListを同時に更新すると、破損する可能性があるということです。たとえば、リストに追加するロジックは次のようになります。

_public void add(T element) {
    if (!haveSpace(size + 1)) {
        expand(size + 1);
    }
    elements[size] = element;
    // HERE
    size++;
}
_

ここで、1つのプロセッサ/コアと2つのスレッドが、同じリストで「同時に」このコードを実行するとします。最初のスレッドがHEREというラベルの付いたポイントに到達し、プリエンプトされたとします。 2番目のスレッドがやって来て、最初のスレッドが更新したばかりのelementsのスロットを独自の要素で上書きしてから、sizeをインクリメントします。最初のスレッドが最終的に制御を取得すると、sizeを更新します。最終結果は、最初のスレッドの要素ではなく、2番目のスレッドの要素を追加し、おそらくリストにnullも追加したことです。 (これは単なる例示です。実際には、ネイティブコードコンパイラがコードを並べ替えた可能性があります。ただし、更新が同時に発生すると、問題が発生する可能性があります。)

2番目のシナリオは、CPUのキャッシュメモリにメインメモリの内容がキャッシュされているために発生します。 2つのスレッドがあり、1つはリストに要素を追加し、もう1つはリストのサイズを読み取ると仮定します。スレッド上で要素を追加すると、リストのsize属性が更新されます。ただし、sizevolatileではないため、sizeの新しい値がすぐにメインメモリに書き出されない場合があります。代わりに、Javaメモリモデルでキャッシュされた書き込みをフラッシュする必要がある同期ポイントまでキャッシュ内にとどまることができます。その間に、2番目のスレッドはsize()を呼び出すことができます。リストを作成し、sizeの古い値を取得します。最悪の場合、2番目のスレッド(たとえばget(int)を呼び出す)では、sizeelements配列の値に一貫性がなく、予期しない例外が発生する可能性があります(その種類に注意してください)。コアが1つだけで、メモリキャッシュがない場合でも、問題が発生する可能性があります。JITコンパイラはCPUレジスタを自由に使用してメモリの内容をキャッシュし、スレッドコンテキストの場合、これらのレジスタはメモリ位置に関してフラッシュ/更新されません。切り替えが発生します。)

番目のシナリオは、ArrayListの操作を同期するときに発生します。例えばSynchronizedListとしてラップします。

_    List list = Collections.synchronizedList(new ArrayList());

    // Thread 1
    List list2 = ...
    for (Object element : list2) {
        list.add(element);
    }

    // Thread 2
    List list3 = ...
    for (Object element : list) {
        list3.add(element);
    }
_

Thread2のリストがArrayListまたはLinkedListであり、2つのスレッドが同時に実行されている場合、スレッド2はConcurrentModificationExceptionで失敗します。それが他の(自家醸造)リストである場合、結果は予測できません。問題は、listを同期リストにするだけでは、異なるスレッドによって実行されるリスト操作のシーケンスに関してスレッドセーフにするのに十分ではないということです。これを取得するには、通常、アプリケーションをより高いレベル/より粗いグレインで同期する必要があります。


また、複数のスレッドが実際には同時に実行されておらず、1つのスレッドがしばらく実行され、その後別のスレッドが実行されると言われたことを覚えています(単一のCPUを搭載したコンピューターで)。

正しい。アプリケーションの実行に使用できるコアが1つしかない場合は、明らかに一度に1つのスレッドしか実行できません。これにより、一部の危険が不可能になり、その他の危険が発生する可能性がはるかに低くなります。ただし、OSは、コード内の任意の時点で、いつでも、あるスレッドから別のスレッドに切り替えることができます。

それが正しければ、2つのスレッドが同じデータに同時にアクセスするにはどうすればよいでしょうか。何かを変更している途中でスレッド1が停止し、スレッド2が開始されるのではないでしょうか。

うん。それは可能です。それが起こる確率は非常に小さいです1 しかし、それはこの種の問題をより陰湿なものにします。


1-これは、ハードウェアクロックサイクルのタイムスケールで測定した場合、スレッドのタイムスライスイベントが非常にまれであるためです。

29
Stephen C

実用的な例。最後のリストには40個のアイテムが含まれているはずですが、私にとっては通常30〜35個のアイテムが表示されます。理由は何でしょうか。

static class ListTester implements Runnable {
    private List<Integer> a;

    public ListTester(List<Integer> a) {
        this.a = a;
    }

    public void run() {
        try {
            for (int i = 0; i < 20; ++i) {
                a.add(i);
                Thread.sleep(10);
            }
        } catch (InterruptedException e) {
        }
    }
}


public static void main(String[] args) throws Exception {
    ArrayList<Integer> a = new ArrayList<Integer>();

    Thread t1 = new Thread(new ListTester(a));
    Thread t2 = new Thread(new ListTester(a));

    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(a.size());
    for (int i = 0; i < a.size(); ++i) {
        System.out.println(i + "  " + a.get(i));
    }
}

編集
より包括的な説明があります(たとえば、Stephen Cの投稿)が、それ以来少しコメントしますmfukarが尋ねました。 (回答を投稿するときに、すぐにそれを行うべきでした)

これは、2つの異なるスレッドから整数をインクリメントするという有名な問題です。 SunのJava並行性に関するチュートリアルには わかりやすい説明 があります。その例でのみ、--i++iがあり、++sizeが2回あります。(++sizeArrayList#add実装の一部です。 )

21
Nikita Rybak

文字列が半分変更されているインスタンスは実際には表示されません。ここで正しい方向に進んでいますか?

それは起こりません。ただし、発生する可能性があるのは、文字列の1つだけが追加されることです。または、addの呼び出し中に例外が発生したこと。

arrayListが問題を引き起こし、Vectorがそれを解決する例を誰かに教えてもらえますか?

複数のスレッドからコレクションにアクセスする場合は、このアクセスを同期する必要があります。ただし、Vectorを使用するだけでは、実際には問題は解決しません。上記の問題は発生しませんが、次のパターンは引き続き機能しません。

 // broken, even though vector is "thread-safe"
 if (vector.isEmpty())
    vector.add(1);

ベクター自体が破損することはありませんが、ビジネスロジックが望まない状態に陥ることができないという意味ではありません。アプリケーションコードで同期する必要があります(その後、Vectorを使用する必要はありません)。

synchronized(list){
   if (list.isEmpty())
     list.add(1);
}

並行性ユーティリティパッケージには、スレッドセーフキューなどに必要なアトミック操作を提供する多数のコレクションもあります。

4
Thilo

いつトラブルが発生しますか?

スレッドがArrayListを読み取り、もう一方が書き込みを行っているとき、または両方が書き込みを行っているとき。 これは非常によく知られている例です。

また、複数のスレッドが実際には同時に実行されておらず、1つのスレッドがしばらく実行され、その後別のスレッドが実行されると言われたことを覚えています(単一のCPUを搭載したコンピューターで)。それが正しければ、2つのスレッドが同じデータに同時にアクセスするにはどうすればよいでしょうか。何かを変更している途中でスレッド1が停止し、スレッド2が開始されるのではないでしょうか。

はい、シングルコアCPUは一度に1つの命令しか実行できません(実際には、 パイプライン はしばらくここにありますが、教授がかつて言ったように、それは「無料の」並列処理です)。コンピュータで実行されている各プロセスは一定期間しか実行されない場合でも、アイドル状態になります。その瞬間、別のプロセスがその実行を開始/継続する可能性があります。そして、アイドル状態になるか、終了します。プロセスの実行はインターリーブされます。

スレッドでも同じことが起こりますが、プロセス内に含まれているだけです。それらの実行方法はオペレーティングシステムによって異なりますが、概念は同じです。それらは、生涯を通じて常にアクティブからアイドルに変化します。

2
Tom

Youeクエリの最初の部分はすでに回答済みです。私は2番目の部分に答えようとします:

また、複数のスレッドが実際には同時に実行されておらず、1つのスレッドがしばらく実行され、その後別のスレッドが実行されると言われたことを覚えています(単一のCPUを搭載したコンピューターで)。それが正しければ、2つのスレッドが同じデータに同時にアクセスするにはどうすればよいでしょうか。何かを変更している途中でスレッド1が停止し、スレッド2が開始されるのではないでしょうか。

wait-notifyフレームワークでは、オブジェクトのロックを取得しているスレッドは、何らかの条件で待機しているときにオブジェクトを解放します。良い例は、生産者/消費者問題です。ここを参照してください: リンクテキスト

1
Amit

1つのスレッドがいつ停止し、もう1つのスレッドが開始するかを制御することはできません。スレッド1は、データの追加が完全に完了するまで待機しません。データを破損する可能性は常にあります。

0
fastcodejava