Java の ConcurrentHashMap における同期化 バッドケースとその対処法 2013 年 9 月湊隆行 1. はじめに表 1.1 に示すように Java の Collections Framework には 3 つの世代があります バージョン 1.0 から存在するレガシー API バ

Similar documents
HashMapからConcurrentHashMapへの移行

Javaと マルチスレッド

Microsoft PowerPoint ppt

Microsoft PowerPoint Java基本技術PrintOut.ppt [互換モード]

PowerPoint プレゼンテーション

Prog1_10th

Android Layout SDK プログラミング マニュアル

プログラミング基礎I(再)

ただし 無作為にスレッドを複数実行すると 結果不正やデッドロックが起きる可能性がある 複数のスレッド ( マルチスレッド ) を安全に実行する ( スレッドセーフにする ) ためには 同期処理を用いるこ とが必要になる 同期処理は 予約語 synchronized で行うことができる ここでは sy

Prog1_6th

intra-mart Accel Platform — IM-Repository拡張プログラミングガイド   初版  

Microsoft Word - Android_SQLite講座_画面800×1280

Brekeke PBX - Version 2.1 ARSプラグイン開発ガイド

ファイナライザを理解する ~ ファイナライザに起因するトラブルを避けるために ~ 2013 年 11 月 25 日 橋口雅史 Java アプリケーションでファイナライザ (finalize() メソッド ) を使用したことがあるプログラマーは多いと思います しかし ファイナライザの仕組みや注意点につ

大容量情報検索論

POSIXスレッド

Javaプログラムの実行手順

.NETプログラマー早期育成ドリル ~VB編 付録 文法早見表~

JavaプログラミングⅠ

プログラミング入門1

GEC-Java

Javaの作成の前に

Javaセキュアコーディングセミナー2013東京第1回 演習の解説

コンピュータ中級B ~Javaプログラミング~ 第3回 コンピュータと情報をやりとりするには?

プレポスト【問題】

PowerPoint プレゼンテーション

Microsoft PowerPoint - ruby_instruction.ppt

Microsoft PowerPoint - prog03.ppt

メディプロ1 Javaプログラミング補足資料.ppt

Microsoft認定資格問題集(70-483_demo)

Prog1_15th

た場合クラスを用いて 以下のように書くことが出来る ( 教科書 p.270) プログラム例 2( ソースファイル名 :Chap08/AccountTester.java) // 銀行口座クラスとそれをテストするクラス第 1 版 // 銀行口座クラス class Account String name

(1) プログラムの開始場所はいつでも main( ) メソッドから始まる 順番に実行され add( a,b) が実行される これは メソッドを呼び出す ともいう (2)add( ) メソッドに実行が移る この際 add( ) メソッド呼び出し時の a と b の値がそれぞれ add( ) メソッド

Java 基礎問題ドリル ~ メソッドを理解する ~ 次のプログラムコードに 各設問の条件にあうメソッドを追加しなさい その後 そのメソッドが正しく動作することを検証するためのプログラムコードを main メソッドの中に追加しなさい public class Practice { // ここに各設問

Prog2_12th

プログラミング入門1

プログラミング入門1

JavaプログラミングⅠ

第 3 回 Java 講座 今回の内容 今週の Java 講座はコレクション 拡張 for 文, ガベージコレクションについて扱う. 今週の Java 講座は一番内容が薄いも のになるだろう. コレクション コレクションとは大きさが決まっていない配列だと考えればよい. コレクションには List 先

プログラミング入門1

Javaセキュアコーディングセミナー東京 第3回 入出力(File, Stream)と例外時の動作 演習解説

JAVA とテンプレート

GEC-Java

PowerPoint プレゼンテーション

メソッドのまとめ

第1章 ビジュアルプログラミング入門

第2回講義

Prog2_9th

Prog2_10th

26 editor.putint(pref_count_key, executecount); 27 // 変更した Preference を確定させる 28 editor.commit(); 29 } (c) 実行の様子実装して実行した様子を図 1 と図 2 に示す. 一度実行するごとに, カウン

基本情報STEP UP演習Java対策

JavaプログラミングⅠ

storage-sdk-Java

Java知識テスト問題

Microsoft Word - no11.docx

Java講座

オブジェクト指向プログラミング・同演習 5月21日演習課題

Prog2_10th

GEC-Java

Sort-of-List-Map(A)

Java Scriptプログラミング入門 3.6~ 茨城大学工学部情報工学科 08T4018Y 小幡智裕

JavaプログラミングⅠ

Microsoft Word - VBA基礎(6).docx

プログラミング実習I

解答上の注意 1 解答は 解答 紙の問題番号に対応した解答欄にマークしなさい 2 選択肢は 問ごとに 意されています 問 1の選択肢は 問 2で使 しません 3 選択肢は量が多いため 探しやすさの観点よりグループ分けされています グループ分けに合わせて解答欄が区切られていますが 横 1 列で問題 1

デジタル表現論・第4回

人工知能入門

Prog2_10th

データアダプタ概要

Microsoft PowerPoint - prog09.ppt

1.SqlCtl クラスリファレンス SqlCtl クラスのリファレンスを以下に示します メソッドの実行中にエラーが発生した場合は標準エラー出力にメッセージを出力します (1)Connect() メソッド データベースへ connect 要求を行います boolean Connect(String

Microsoft PowerPoint - prog04.ppt

Microsoft PowerPoint - prog09.ppt

プログラミング入門1

開発・運用時のガイド JDK8への移行に伴う留意点 [UNIX]

PowerPoint プレゼンテーション

PowerPoint プレゼンテーション

Seasar.NET入門

JAVA入門

アクティビティ図・シーケンス図からのコード生成 機能ガイド

JAVA入門

JAVA入門


Make the Future Java FY13 PPT Template

Microsoft PowerPoint - lec10.ppt

Javaプログラマー早期育成ドリル ~コードリーディング編~ 解答

108 頁通過テスト 2. の本文 111 頁紹介文 136 頁練習 5-1 プログラム 136 頁練習 5-1 問 2 末尾に句点追加 158 頁練習問題文 161 頁練習 2-2 コメント文 166 頁練習 3-1 問 1 クラス名を挿入 178 頁通過テスト 3 文字 s を削除 180 頁コ

プログラミング入門1

2006年10月5日(木)実施

DVIOUT-exer

関数の動作 / printhw(); 7 printf(" n"); printhw(); printf("############ n"); 4 printhw(); 5 関数の作り方 ( 関数名 ) 戻り値 ( 後述 ) void である. 関数名 (

(Microsoft PowerPoint - \223\306\217KJAVA\221\346\202R\224\ ppt)

Prog1_12th

Slide 1

プログラミング入門1

Prog2_9th

21 章のお話

教材ドットコムオリジナル教材 0から始めるiアフ リ リファレンス i アプリ簡易リファレンス ver i アプリ Java 独自のメソッド (1)iアプリの命令を使えるようにする import com.nttdocomo.ui.*; (2) 乱数を使う import java.u

Microsoft PowerPoint - prog07.ppt

全商情報処理検定プログラミング部門 サンプル問題1級解説

Transcription:

Java の ConcurrentHashMap における同期化 バッドケースとその対処法 2013 年 9 月湊隆行 1. はじめに表 1.1 に示すように Java の Collections Framework には 3 つの世代があります バージョン 1.0 から存在するレガシー API バージョン 1.2 で追加されたロック機構を使わない API および バージョン 5.0 で追加された同期化コストが低い API があります 世代 API の例 特徴 Java SE 1.0 から存在するもの Hashtable Vector 多くのメソッドに synchronized 修飾子があり スレッドセーフである反面 同期化コストが高く速度性能が悪い Java SE 1.2 で追加されたもの HashMap ArrayList ロック機構を使わないため速度性能が良い反面 スレッドセーフではないためプログラマーは多くの同期化処理を Java SE 5.0 で追加されたもの ConcurrentHashMap CopyOnWriteArrayList 表 1.1 Java Collections Framework の世代と特徴 実装しなければならない 同期化コストを下げて速度性能を向上させたスレッドセーフなクラスであり プログラマーは実装しなければならない同期化処理を軽減している ただし クラスがスレッドセーフであるかどうかに関わらず プログラマーは同期処理の実装から完全に解放されるわけではありません なぜなら 複数のスレッドから同時にアプリケーションの共通データを更新する場合 共通データのクラスの更新メソッドがスレッドセーフであっても データに矛盾が発生する可能性を排除できないからです 特にオブジェクトの状態を更新する処理が 1 つでもあれば そのオブジェクトに対する一連の呼び出しを 1 つのトランザクションとして同期化し 他のスレッドから割り込まれないようにする必要があります また アプリケーションが 2 つ以上のオブジェクトを共通データとして持っていて これらのオブジェクトが相互依存する関係にある場合 複数のオブジェクトに対する一連の呼び出しも 1 つのトランザクションとして同期化しなければなりません 本書では 並列プログラミング初心者を対象とし ConcurrentHashMap を使用するプログラムについて 初心者がおかしやすい 2 つのバッドケースを紹介し どのような対処が必要かを紹介します 2. バッドケース 1 図 2.1 のプログラムを見てください インスタンス変数 countermap は ConcurrentHashMap オブジェクトであり 文字列 (String オブジェクト ) と整数値 (Integer オブジェクト ) を対とした key-value データを保持します incrementcounter() が呼び出されるたびに 引数 key に対応する整数値をインクリメントする つもり のプログラムです class Sample1 { ConcurrentMap<String, Integer> countermap = new ConcurrentHashMap<>(); // インスタンス変数 1

void incrementcounter(string key) { int counter = countermap.get(key); countermap.put(key, ++counter); 図 2.1 同期化を行わないプログラム 1 countermap にはすでに "key1" と整数値 1 の key-value が格納してあるとします そのとき 図 2.2 のように 2 つのスレッドが異なるタイミングで incrementcounter("key1") を 1 回ずつ呼び出した場合 最終的に countermap に整数値 3 を格納します これは期待通りの動作です しかし 図 2.3 のように 2 つのスレッドがほぼ同時に incrementcounter() を呼び出した場合 最終的に格納する整数値は 3 ではなく 2 となるおそれがあります なぜならば 複数のスレッドが同時に動作しているとき どのスレッドがどのメソッドを先に呼び出すかは不定だからです 図 2.2 では 1.1: get("key1") は整数値 1 2.1: get("key1") は整数値 2 を返します しかし 図 2.2 と違って図 2-3 では スレッド 1 が 1.2: put("key1", 2) を呼び出す前に スレッド 2 が 2.1: get("key1") を呼び出している点が根本的な問題です 結果 スレッド 1 とスレッド 2 はともに 整数値 2 を格納してしまうわけです これは read-modify-write のシーケンスであり 読み込んだ現在の値を更新し 更新した値を書き出す という一連の処理を指します countermap.get() が read( 読み込み ) ++counter が modify( 更新 ) で countermap.put() が write( 書き出し ) に該当します ConcurrentHashMap のようなスレッドセーフなクラスを使う場合でも この read-modify-write の処理全体を 1 つのトランザクションとして同期化する必要があります 図 2.2 複数スレッドが異なるタイミングで incrementcounter() を呼び出す図 2

図 2.3 複数スレッドが同時に incrementcounter() を呼び出す図 3. バッドケース 1 の対処法バッドケース 1 の対処案を図 3.1 に示します countermap オブジェクトに対する処理全体を 1 つのトランザクションで行うように synchronized 節で囲んでいます この対処により incrementcounter() を呼び出すたびに正しくインクリメントした値を格納するようになります class Sample1 { ConcurrentMap<String, Integer> countermap = new ConcurrentHashMap<>(); // インスタンス変数 void incrementcounter(string key) { synchronized(countermap) { int counter = countermap.get(key); countermap.put(key, ++counter); 図 3.1 バッドケース 1 の対処案 ( その 1) しかし synchronized や ReentrantLock などを使ったロック機構はコストが高く 並行率 が大きく低下します 並行率とは プログラムの総実行時間のうち 複数のスレッドが同時に動作した時間の割合のことです 言い換えると スレッドがブロック状態または待機状態になった時間が長ければ長いほど 並行率が低下します そこで別の対処案を図 3.2 に示します class Sample1 { ConcurrentMap<String, AtomicInteger> countermap = new ConcurrentHashMap<>(); // インスタンス変数 void incrementcounter(string key) { countermap.get(key).incrementandget(); 図 3.2 バッドケース 1 の対処案 ( その 2) 3

この対処案は図 2.1 のバッドケースのプログラムに対して 次の 3 点を修正したものです 1) countermap の値を java.lang.integer クラスから java.util.concurrent.atomic.atomicinteger クラスに変更 2) AtomicInteger クラスの incrementandget() を使って 整数値をインクリメント 3) countermap の put() 呼び出しを削除 1) の ConcurrentHashMap の値を Integer から AtomicInteger に変更したことが大きなポイントです AtomicInteger は CPU のアトミック命令を使って値を取得 / 更新するクラスです アトミック命令を使えば ロック機構を使わなくてもスレッドセーフに整数値の取得と更新ができます これにより 図 3-1 の対処案より並行率が向上します また Integer オブジェクトは immutable( 不変 ) なため 整数値を更新するたびに新しい Integer オブジェクトを生成し put() で格納し直す必要がありました 対して AtomicInteger オブジェクトは immutable ではないため put() を実行する必要がなくなりました 4. バッドケース 2 2 つ目のバッドケースを紹介します 図 4.1 のプログラムを見てください インスタンス変数 personmap は ConcurrentHashMap オブジェクトであり 氏名と個人情報 (Person クラス ) の対を格納します updateaddress() では 引数 updatetime が Person オブジェクトの lastupdatetime より大きな値である場合に限り 郵便番号と住所を更新しています また personmap.get(name) は null を返さないものと仮定し null チェックは省略しています なお getaddress() は 引数 zipcode( 郵便番号 ) に対応する住所を 他の Web サービスやローカルファイル等にアクセスして取得するメソッドです class Sample2 { ConcurrentMap<String, Person> personmap = new ConcurrentHashMap<>(); // インスタンス変数 void updateaddress(long updatetime, String name, String zipcode) { Person p = personmap.get(name); if(updatetime > p.lastupdatetime) { p.lastupdatetime = updatetime; p.zipcode = zipcode; p.address = getaddress(zipcode); // 郵便番号に対応する住所を取得 String getaddress(string zipcode) { : return address; class Person { long lastupdatetime; // 最終更新日時 String name; // 氏名 String zipcode; // 郵便番号 String address; // 住所 図 4.1 同期化を行わないプログラム 2 4

このプログラムもバッドケース 1 と同様 複数のスレッドから updateaddress() を呼び出した場合に問題が起きるおそれがあります あるスレッドが Person オブジェクトの lastupdatetime zipcode と address の各フィールドを更新している間に 別のスレッドが同時にこれらのフィールドを更新すると 各フィールドの値が矛盾します 図 4.2 は スレッド 1 とスレッド 2 がほぼ同時に updateaddress() を呼び出したときの処理の流れです 1.x がスレッド 1 による呼び出しで 2.x がスレッド 2 による呼び出しです 1.1 と 1.2 の get(" 富士通太郎 ") が同じ Person オブジェクトを返すため 1.2~1.4 および 2.2~2.4 は同じ Person オブジェクトに対するアクセスになります 郵便番号 222-0033 の本来の住所は 神奈川県横浜市港北区新横浜 です しかし 図 4.2 では 1.4 と 2.4 の呼び出し順序が逆になったために Person オブジェクトの address に 神奈川県川崎市中原区上小田中 を代入してしまっています このプログラムでは check-then-act と呼ばれるシーケンス つまり 値をチェックし その結果に基づいて何らかの処理を行う という一連の処理を行っています しかし Person オブジェクトの lastupdatetime フィールドのチェック (check) と 各フィールドに対する更新 (act) を同期化していないために 郵便番号と住所に矛盾が起きたわけです 処理対象のオブジェクトがスレッドセーフであるかどうかに関係なく check-then-act の処理全体を 1 つのトランザクションとして同期化する必要があります 図 4.2 複数スレッドが同時に Person のフィールドを更新する図 5

5. バッドケース 2 の対処法 バッドケース 2 の updateaddress() の対処案を図 5.1 に示します void updateaddress(long updatetime, String name, String zipcode){ Person p = personmap.get(name); if(updatetime <= p.updatetime){ return; String address = getaddress(zipcode); synchronized(p) { if(updatetime > p.updatetime){ p.updatetime = updatetime; p.zipcode = zipcode; p.address = address; 図 5.1 バッドケース 2 の対処案 この対処案のポイントは 2 つです 1 つめは if 節全体を synchronized 節で囲み Person オブジェクトに対する操作を同期化したことです この対処により check-then-act 全体の処理を 1 つのトランザクションで行うようになります 2 つめは getaddress() の処理コストが高いことを想定して getaddress() の呼び出しを synchronized 節の外側で行うようにした点です なぜなら synchronized 節の内側でコストが高い処理を行うと 他のスレッドを長時間待たせることになり 並行率が大幅に悪化するためです 6. まとめ本書では スレッド間で共有しているオブジェクトの中身を更新する場合 そのオブジェクトがスレッドセーフであってもなくても同期化しなければならないことを ConcurrentHashMap を題材にして紹介しました その対処法として java.util.concurrent.atomic パッケージを使った低コストな同期化と synchronized を使った同期化を紹介しました また 同期化する範囲を可能な限り狭めて 並行率を高めることも紹介しました 参考文献 Java 並行処理プログラミング その 基盤 と 最新 API を究める Brian Goetz( 著 ) Joshua Bloch( 著 ) Doug Lea( 著 ) 岩谷宏 ( 訳 ) ソフトバンククリエイティブ ( 出版社 ) 商標について Java は Oracle Corporation およびその子会社 関連会社の米国およびその他の国における登録商標です その他の会社名および製品名は それぞれの会社の登録商標もしくは商標です - 以上 - 6