質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

90.34%

Java ArrayList スレッドセーフ removeメソッド

解決済

回答 4

投稿 編集

  • 評価
  • クリップ 1
  • VIEW 2,843

flan

score 132

JavaのArrayLIstのスレッドセーフについて質問です。

Java SIlver 黒本より

ソース1

Arraylist<String> list = new ArrayList<>();

list.add("A");
list.add("B");
list.add("C");

for (String str : list){

    if("B".equals(str)){
        list.remove(str);
}else{
System.out.println(str);
}   

}

ソース2

Arraylist<String> list = new ArrayList<>();

list.add("A");
list.add("B");
list.add("C");
list.add("D");
list.add("E");

for (String str : list){

    if("C".equals(str)){
        list.remove(str);
}

}

for (String str : list){
System.out.println(str);
}


*クラスの宣言とmainメソッドは省略しました。


このソース1の実行した結果として
A
が出力。


ソース2は実行時に例外がスローされるとあります。
ConcurrentModificationException←これ

その理由として、ループで読みだしている最中にremoveメソッドを呼び出しているからとあります。
しかし、ソース1でもremoveメソッドを呼び出しているのにソース1では例外がスローされません。これはなぜでしょうか?

私なりの考察。

ソース1では拡張for文2回目のループでremoveメソッドが処理され3買い目のループは要素がなくなり2回目で終わり、残りの要素に影響されない。

ソース2では拡張for文3回目のループでremoveメソッドが処理され4回目ループの際の"E"がまだ残っているため要素数の順番に影響があるから?


よろしくお願いします。

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 4

checkベストアンサー

+3

大まかに言うと考察はあってます。
要素数の順番ではなく、ループ中に要素数が変わることでエラーになります。

またソース1を実行した際も、
直感的にはAとCが出力されることが期待値になると思うのでエラーですよね。

ということで、普通ループ中に要素追加削除をするのはNGになっています。

stacktraceとclassファイル見ると多少詳しくと思うんですが、

java.util.ConcurrentModificationException
    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
    at java.util.ArrayList$Itr.next(ArrayList.java:851)
Iterator arg2 = e.iterator();
while(arg2.hasNext()) {
   String str = (String)arg2.next();
   if("B".equals(str)) {
      e.remove(str);
   } else {
      System.out.println(str);
   }
}

ArrayList$Itr.checkForComodificationで要素数のチェック・エラー判定を行っているので、
ソース1の場合はremoveした後に次のhasNextでループを抜けるため、
nextが呼ばれずエラーになりません。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

+2

他の方の回答に似てますが微妙に違うコメントをしてみます。

ConcurrentModificationException(長いので以下CMEとよびます)が発生する・しないことの厳密な意味を理解しておくことは重要だと思います。

以下ArrayListのリファレンス:

通常、非同期の並行変更がある場合、確かな保証を行うことは不可能なので、イテレータのフェイルファストの動作を保証することはできません。フェイルファスト・イテレータは、ベスト・エフォート・ベースでConcurrentModificationExceptionをスローします。したがって、正確を期すためにこの例外に依存するプログラムを書くことは誤りです。イテレータのフェイルファストの動作はバグを検出するためにのみ使用すべきです。

つまりArrayListは多重処理による処理間違いを確実に検出することは狙っておらず、性能を劣化させない程度の手間をかけることで「うまくいけば例外を発することができることがある」となっているわけです。このためCMEが発生すれば「バグであることは明らか」ですが、CMEが発生しなかった場合は正しいかといえばその保証はないということになってしまいます。

コレクションに限らず一般にオブジェクトの状態を変更するスレッドが複数存在するなら一貫性を保つに必要な処理範囲は他スレッドからの操作をガードする(排他する)ことが必要です。ゆえにご質問のような処理をするなら例えば以下のようにしなければならない場合も多いでしょう。

synchronized (同期用オブジェクト) {
  for (String str : list){
    if("C".equals(str)){
      list.remove(str);
  }
}

補足:masaya_ohashiさんが挙げておられるIterator#removeは、こと多重処理についてはList#removeに比べ「より安全であるとはいえない」と思います。

masaya_ohashiさんご自身もIterator#removeが安全とは限らない点を「イテレータを自分で用意して、イテレータのremoveを使いましょう。 」という表現に込められているのだと思いました。ただ、読み手によっては「Iterator#removeを使えば安全なのかな?」とハヤトチリする可能性もあると思います。また実はそのような特別なIteratorを自分で定義するのは手間がかかる上に「それなりに処理コストがかかる」ものになるでしょう。ちなみにCollections.synchronizedListで同期リストを作ったとしてもそのiterator()メソッドで返されるイテレーターはhasNext(), next(), remove()などの操作を行う場合に利用者自身が(synchronized文を使って)同期した上で使わないといけないことにも注意すべきでしょう。こうしたことから、

複数のスレッドで同一のコレクションを並行して走査(かつ構造変更)をするというのは実現は可能ですが、個人的には余程の理由がない限りそういうことは避けて、走査処理全体をガードした方が簡単確実であることが多い気がします。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

+1

ConcurrentModificationExceptionが発生するかしないかは「時の運」がからんでいます。監視のタイミングの気まぐれによって起こるといっても過言ではありません。ちなみに、ソース1もソース2も、どちらも「スレッドセーフでないListの、イテレータ操作中のremove」という点においてConcurrentModificationExceptionが発生する可能性をはらんでいます。

Listからイテレータ操作中に安全に要素をremoveする場合、このような書き方が正しいです。

Iterator<String> itr = list.iterator();
while(itr.hasNext()){
    str = itr.next();
    if("C".equals(str)){
        itr.remove();
    }
}


拡張for文において、「ConcurrentModificationExceptionの起こらない安全なremove方法」は存在しません。イテレータを自分で用意して、イテレータのremoveを使いましょう。

追記

存在しないとは言いましたが、Collections.synchronizedListを使えば拡張for内で安全にremove可能な同期Listに置き換えることができます。ただし、あまり理解せずにこれに頼るのはよくないです。速度も遅くなりますし、デッドロックの発生の危険性もあります。
http://qiita.com/mountcedar/items/3a4d147d12e8b38cf6d0

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

0

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 90.34%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

同じタグがついた質問を見る