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

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

ただいまの
回答率

90.22%

ConcurrentModificationExceptionが発生するパターンについて

解決済

回答 3

投稿

  • 評価
  • クリップ 7
  • VIEW 21K+

k499778

score 521

現在Java Silverの勉強をしています。
そこでConcurrentModificationExceptionの問題がありました。
ConcurrentModificationExceptionは、ArrayListなどの非同期クラスのインスタンスが、別スレッドから変更されるとスローされるというのは知っているのですが、
ではなぜ次のような2パターンでは例外がスローされるのとされないので分かれるのでしょうか?

つまり、
下記のコードの前者ではConcurrentModificationExceptionが発生しないのに、
後者では発生するのはなぜでしょうか?

public class Main {
    public static void main(String[] args) {
    ArrayList<String>list=new ArrayList<String>();
    list.add("A");
    list.add("B");
    list.add("C");
    for (String str : list) {
        if ("B".equals(str)) {
        list.remove(str);
        }
    }
    }
}

public class Main {
    public static void main(String[] args) {
    ArrayList<String>list=new ArrayList<String>();
    list.add("A");
    list.add("B");
    list.add("C");
    for (String str : list) {
        if ("C".equals(str)) {
        list.remove(str);
        }
    }
    }
}

もちろん変更点はif文の条件が「B」と「C」とで変わっている点だとわかっているのですが、
それだとなぜ前者と後者で例外がスローされないのとされるので変わるかわかりません。
removeメソッドで要素を削除した場合、要素が繰り上がるのも知っています。
そもそもどこがどうマルチスレッドなのかがわかっていません。
言葉では「要素を取り出している処理」と「要素を削除している処理」の2つが並列処理なのかなとは思っているのですが、
コード上の動きや仕組みとしていまいちピンときていません。

いろいろと具体的に挙げてしまいましたが、
まずはなぜこの両者で例外が発生しないのとするので分かれるのか理解したいと思います。

もしわかる方がいればお願い致します。

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

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

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

    クリップを取り消します

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

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

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

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

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

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

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

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

    質問の評価を下げる

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

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

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

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

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

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

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

    詳細な説明はこちら

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

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

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

回答 3

checkベストアンサー

+6

内部のコードを読むと,ArrayListとIteratorは内部で「変更された回数」を保持しています.
そしてArrayListのiterator()メソッドで生成されるIteratorはforで回す際に使うnext()メソッドなどを起動した際に,変更された回数にミスマッチがないかチェックし,ミスマッチしていればConcurrentModificationExceptionを投げる仕組みになっています.
また,ArrayListのIteratorのhasNext()メソッドは,「現在位置とリストのサイズが一致していない場合にtrueを返す」というコードになっています.これを踏まえて前者・後者のコードを読み解いていきます.

前者:
  1.  "B"が取り出される直前,Iteratorの現在位置は"B"の位置である"1"
  2.  next()によって"B"が取り出され,Iteratorの現在位置は1加算され"2",remove()でリストのサイズは2になる
  3.  次のループでhasNext()はサイズ=現在位置によりfalseを返す.ループを抜けるため変更回数チェックがされず例外は発生しない

後者:
  1.  "C"が取り出される直前,Iteratorの現在位置は"C"の位置である"2"
  2.  next()によって"C"が取り出され,Iteratorの現在位置は1加算され"3",remove()でリストのサイズは2になる
  3.  hasNext()はサイズ≠位置なのでtrue.ループ継続する
  4.  次のnext()でリスト側の変更回数がremove()によって1回多いことが判明,例外発生
こういうことになります.

追記・補足
ArrayListのJavadocに次のような記述があります.
このクラスの iterator および listIterator メソッドによって返されるイテレータは、フェイルファストです。イテレータの作成後に、イテレータ自体の remove または add メソッド以外の方法でリストが構造的に変更されると、イテレータは ConcurrentModificationException をスローします。このように、並行して変更が行われると、イテレータは、将来の予測できない時点において予測できない動作が発生する危険を回避するために、ただちにかつ手際よく例外をスローします。
通常、非同期の並行変更がある場合、確かな保証を行うことは不可能なので、イテレータのフェイルファストの動作を保証することはできません。フェイルファストイテレータは最善努力原則に基づき、ConcurrentModificationException をスローします。したがって、正確を期すためにこの例外に依存するプログラムを書くことは誤りです。「イテレータのフェイルファストの動作はバグを検出するためにのみ使用すべきです」。

投稿

編集

  • 回答の評価を上げる

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

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

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

  • 回答の評価を下げる

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

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

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

  • 2015/09/28 14:17

    swordoneさん
    いつもありがとうございます。

    そういうことなんですね。
    拡張for文は暗黙的にIteratorを使用するため、実際はそういった動きにより例外が発生したりしなかったりしていたのですね。

    理解できました。ありがとうございます。

    ちなみにもしお時間あればで結構なのですが、
    内部のコードというのは何なのでしょうか?実際にどういう動きをしているか。という認識で合っているでしょうか?
    またそれを目で見て確かめる方法はありますか?
    エクリプスでデバッグし、F6で実際にコードを1行1行追っていったのですが、そこまでのことはわかりませんでした。F5でステップインするのかな?

    もしよろしければお答えいただけたらと思います。

    キャンセル

  • 2015/09/28 15:12 編集

    JDKに添付されているソースコードのことです.
    例外発生時にスタックトレースがコンソールに表示されると思います.
    私も実際に例外が発生する方のコードをEclipseで実行しました.
    すると次のようなログが表示されます.

    Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
    at java.util.ArrayList$Itr.next(ArrayList.java:851)
    at teratail.question16901.Main.main(Main.java:12)

    この括弧内はリンクになっており,クリックするとプログラムの該当行にジャンプします.
    違うクラスの場合はそのクラスのソースコードが別タブで開きます.
    ステップインでもその行を通って行くことが確認できるかと思います.
    ちなみに,このログの2行目にあるcheckForComodificationが,上記回答で言った「変更回数にミスマッチがないかチェックする」メソッドです.

    キャンセル

  • 2015/09/28 16:21

    swordoneさん
    追加回答ありがとうございます。
    おっしゃる通りですね。勉強になりました。自分でも色々と試してみて理解を深めたいと思います。ありがとうございました。

    キャンセル

  • 2015/09/28 23:02

    ちなみに,Eclipse上のコードのクラス名やメソッド名にマウスオーバーすると,
    そのクラスまたはメソッドの説明の小さな窓が表示されます.
    その窓をクリックまたはしばらくマウスオーバーして枠が出てきたところで
    左側のマークをクリックするとそのクラス/メソッドに飛べます.

    キャンセル

+6

簡単に言えば:ConcurrentModificationException例外がスローされる/されないについては、何の保証もありません。「コレクション側で問題を検知できたときはできる限り早期にスローすること」という努力目標が設定されているだけです。

Java 7 SE ConcurrentModificationExceptionドキュメントより:
この例外は、オブジェクトが別のスレッドによって並行して更新されていないことを必ずしも示しているわけではありません。単一のスレッドが、オブジェクトの規約に違反する一連のメソッドを発行した場合、オブジェクトはこの例外をスローします。たとえば、フェイルファストイテレータを持つコレクションの繰り返し処理を行いながら、スレッドがコレクションを直接修正する場合、イテレータはこの例外をスローします。
通常、非同期の並行変更がある場合、確かな保証を行うことは不可能なので、フェイルファストの動作を保証することはできません。フェイルファストオペレーションは最善努力原則に基づき、ConcurrentModificationException をスローします。したがって、正確を期すためにこの例外に依存するプログラムを書くことは誤りです。ConcurrentModificationException は、バグを検出するためにのみ使用してください。

やたら難解な表現になっていますが、「フェイルファストオペレーションは最善努力原則に基づき、ConcurrentModificationException をスローします。」が”努力目標”に該当します。また「単一のスレッドが、オブジェクトの規約に違反する一連のメソッドを発行した場合、オブジェクトはこの例外をスローします。」とある通り、その名前(Concurrent)に反して、シングルスレッドでもスローされることがあります。

投稿

  • 回答の評価を上げる

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

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

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

  • 回答の評価を下げる

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

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

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

  • 2015/09/28 14:24

    yohhoyさん
    いつもありがとうございます。

    仕様はそういうことを言っているのですね。いつも読んでみるのですが、なかなか理解できなくて。

    仕様では努力目標だけ設定されていてマルチでもシングルでもConcurrentModificationExceptionはスローされることがあるんですね。

    勉強になります。ありがとうございました。

    キャンセル

+2

Java 7の解説によれば、

この例外は、オブジェクトが「別の」スレッドによって並行して更新されていないことを必ずしも示しているわけではありません。単一のスレッドが、オブジェクトの規約に違反する一連のメソッドを発行した場合、オブジェクトはこの例外をスローします。

とあります。別に、スレッド実行と関係なくこの例外が発生することもあるようです。

おそらく、削除したイテレーターから次のステップへ進もうとして例外になるので、最後の時だけは次に進むことがなく例外にならなかった、ということでしょう。

投稿

  • 回答の評価を上げる

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

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

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

  • 回答の評価を下げる

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

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

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

  • 2015/09/28 11:09 編集

    maisumakunさん
    回答ありがとうございます。
    そうなんですね。別スレッドから変更された場合の例外だと思っていたので、
    なんだかつじつまが合わなかったのですが、そういう理由だと知れてよかったです。

    拡張for文に関しては一見イテレーターを使用していないように見えますが、
    暗黙的にイテレーターを使用するfor文であるため、イテレーターを使用しているということですよね。まず前提の話になってしまっていますが。実はイテレーターにあまり詳しくないので。少し調べて、「コレクションに格納されたすべての要素に対して処理を実施する」ものでIteratorインターフェイスがあり、nextメソッドやhasNextメソッドで要素の検証をすることができると知りました。

    少し逸れましたが、例外が発生している理由が知れてよかったです。ありがとうございました。

    キャンセル

  • 2015/09/28 11:14 編集

    あ、すいません。
    少し考えたのですが。

    真ん中の要素(B)を削除する前者は例外が起きず、
    最後の要素(C)を削除する後者は例外が発生しました。

    maisumakunさんの
    >最後の時だけは次に進むことがなく例外にならなかった
    という部分にそぐわない気がするのですが。最後を削除したときに例外になったので。
    もしお時間ございましたらお答えいただけると嬉しいです。

    キャンセル

  • 2015/09/28 11:58

    すみません、読み間違えていました。例外の発生原因の部分については勘違いでのコメントなので無視してください。

    キャンセル

  • 2015/09/28 12:55

    maisumakunさん
    返答ありがとうございます。
    そうでしたか。ただ回答いただけたこと感謝しています。ありがとうございました。

    キャンセル

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

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