The King's Museum

ソフトウェアエンジニアのブログ。

Java の同期化とは何か(2)(【EffectiveJava】項目66:共有された可変データへのアクセスを同期する-後半-)

前回の記事では Java の同期化(synchronized)について説明してきました。

hjm333.hatenablog.com

今回の記事では「volatile」と「同期化が必要ない場合」について説明します。

volatile

Java の同期化(synchronized)は次の性質を保証するものでした。

  • アトミック性
  • メモリ可視性

Java にはメモリの可視性だけを保証するための仕組みがあります。 それが volatile です。

volatile キーワードが付けられた変数はメモリの可視性が保証されます。 volatile では処理をアトミックにすることはできませんが、各スレッド間でフラグを共有する際に便利です。

例えば、前回、例に挙げたスレッドを停止させるコードでは synchronized を使いました。

public class StopThread {
    private static boolean stop;
    private static synchronized void requestStop() {
        stop = true;
    }
    private static  synchronized boolean isStop() {
        return stop;
    }
    public static void main(String[] args) throws InterruptedException {
        Thread background = new Thread(new Runnable() {
            @Override public void run() {
                int i = 0;
                while (isStop()) {
                    i++;
                }
            }
        });
        background.start();

        TimeUnit.SECONDS.sleep(1);
        requestStop();
    }
}public class StopThread {
    private static boolean stop;
    private static synchronized void requestStop() {
        stop = true;
    }
    private static  synchronized boolean isStop() {
        return stop;
    }
    public static void main(String[] args) throws InterruptedException {
        Thread background = new Thread(new Runnable() {
            @Override public void run() {
                int i = 0;
                while (isStop()) {
                    i++;
                }
            }
        });
        background.start();

        TimeUnit.SECONDS.sleep(1);
        requestStop();
    }
}public class StopThread {
    private static boolean stop;
    private static synchronized void requestStop() {
        stop = true;
    }
    private static  synchronized boolean isStop() {
        return stop;
    }
    public static void main(String[] args) throws InterruptedException {
        Thread background = new Thread(new Runnable() {
            @Override public void run() {
                int i = 0;
                while (isStop()) {
                    i++;
                }
            }
        });
        background.start();

        TimeUnit.SECONDS.sleep(1);
        requestStop();
    }
}

しかし、この例では処理のアトミック性は必要ありません。 boolean の読み書き自体がアトミックになっていますので単にメモリの可視性を保証すれば問題ありません。 そのため volatile を使うことができます。

volatile を使って書き換えたのが次のコードです。

public class StopThread {
    private static volatile boolean stop;

    public static void main(String[] args) throws InterruptedException {
        Thread background = new Thread(new Runnable() {
            @Override public void run() {
                int i = 0;
                while (stop) {
                    i++;
                }
            }
        });
        background.start();

        TimeUnit.SECONDS.sleep(1);
        stop = true;
    }
}

このコードは synchroznied を使ったときと同じように正しく動作します。

インクリメントとデクリメント

volatile はメモリの可視性を保証しますが、アトミック性は保証しません。 そのため「アトミックのように見えて実はアトミックではない」という操作に対して volatile を使わないように注意が必要です。

インクリメントとデクリメントは「アトミックに見えるが実はアトミックでない操作」の代表です。 インクリメントは実際には「値の読み出し」、「値の加算」、「値の書き込み」という複数の操作で構成される処理です。 インクリメント・デクリメントに関わる操作を volatile で実装するのは間違いです。

次のコードは複数のスレッドから一意の番号を生成するためのメソッドを実装したものです。

private static volatile int serialNumber = 0;

public static int generateSerialNumber() {
    return serialNumber++;
}

serialNumber のインクリメントは volatile によってメモリの可視性が保証されています。 ただし、アトミックでないため複数のスレッドから呼び出された際に、それぞれのスレッドで同じ値を生成してしまう可能性があります。

正しくは次のように syncronized を使い、メモリの可視性に加えてアトミック性も保証する必要があります。

private static int serialNumber = 0;

public static synchronized int generateSerialNumber() {
    return serialNumber++;
}

同期を回避する

ここまで同期について説明してきましたが、正しく同期を実装すると複雑になるケースが多いです。 一方、同期がなければコードの複雑度は下がります。

ここでは同期を回避するためのテクニックを紹介します。

不変クラスの利用

不変クラスを利用すれば同期は必要はありません。 不変クラスはインスタンスが生成されてから状態が変わることがないからです。

状態を変える処理が存在しないため、アトミック性について気にすることはありません。 加えて、状態が変わらないので常にキャッシュの値を参照したとしても問題ありません。 すなわち、メモリの可視性について考えなくてよいのです。

可変データをスレッドに閉じ込める

たとえ可変なデータであっても、単一のスレッドからしかアクセスできないようにすれば同期は不要です。

単一スレッドからしか読み書きしませんので、アトミック性もメモリの可視性も考える必要はありません。

スレッドセーフクラス

スレッドセーフ性が保証されているクラスを使えば同期は必要ありません。

同期はそのクラス自体が適切に行ってくれます。 また、同期を使わずにスレッドセーフ性を実現するクラスもあります。

たとえばさきほどの serialNumber の例では java.concurrent の AtomicLong というクラスが利用できます。

private static final AtomicLong serialNumber = new AtomicLong();

public static long generateSerialNumber() {
    // getAndIncrement メソッドは値の読取りとインクリメントをスレッドセーフで行うメソッド
    return serialNumber.getAndIncrement();
}

java.concurrent ライブラリには高度に最適化されたスレッドセーフなクラスが多数あります。 自分でスレッドセーフを実装する前にこれらのライブラリの利用を検討するべきです。

その他

その他のテクニックとして次のような手法がありますが、ここでは説明を省きます。

  • オブジェクトを事実上不変にする
  • オブジェクトを安全に公開する

これらのテクニックに関しては次の書籍に詳しく記載されています。

Java並行処理プログラミング ―その「基盤」と「最新API」を究める―

Java並行処理プログラミング ―その「基盤」と「最新API」を究める―

感想

ついに concurrency の章か。。。

concurrency についてはもう一度『Java 並行処理プログラミング』を読み直そうかと思ったり、思わなかったり。。。

Java の同期化とは何か(1)(【EffectiveJava】項目66:共有された可変データへのアクセスを同期する-前半-)

複数のスレッドで可変データを共有する場合にはそれぞれのスレッドを同期化(synchronized)する必要があります。

Java の同期化は次の二つの性質を保証します。

  • アトミック性(処理の排他制御
  • メモリの可視性

アトミック性

異なるスレッドが共通のデータにアクセスする場合、処理途中の不整合な状態を外部に見せたくない場合があります。 そのためには、処理をアトミックにして同時に一つのスレッドだけが処理することを保証する必要があります。

Java では、処理をアトミックにしたい場合は synchronized キーワードを利用します。 synchronized キーワードを使うとメソッドや処理をアトミックにすることができます。

次の atomic メソッドは常に1つのスレッドでのみ処理されることが保証されます。

public synchronized void atomic() {
    ...(アトミックな処理)...
}

もし、あるスレッド A で atomic() を呼び出していた場合、他のスレッド B で atomic() を呼び出すとスレッド A の atomic() の処理が終わるまでスレッド B は待機します。

変数の読み書き処理はアトミックであることが Java の仕様によって保証されています。 そのため、単純な変数の読み書きに synchronized を使う必要はありません。

// 変数の書きこみはアトミック
int value = 1;

ただし、long と double についてはアトミック性は保証されていないので注意が必要です。

// 次の命令はアトミックではない。二つのバイト命令に変換される可能性がある。
long value = 1L;

メモリの可視性

スレッドが複数ある場合、それぞれのスレッドは独自のキャッシュメモリを持っている可能性があります。

あるスレッド Aで書き込んだ値はスレッド A のキャッシュメモリにのみ反映されていてメインメモリに反映されていません。 この状態では他のスレッド B から値を読み込もうとしても、メインメモリに反映されていないため正しい値を読み込めません。

このように、メモリ上の正しい値が観測できるかどうかの性質を「メモリの可視性」と呼んでいます。

例えば、メインスレッドから他のスレッド A を停止させるコードを考えてみます。 この場合、単に次のようなコードを書くかもしれません。

public class StopThread {
    // boolean は操作がアトミックだから synchronized を利用しない(大きな間違い!)
    private static boolean stop;

    public static void main(String[] args) throws InterruptedException {
        // スレッドの生成
        Thread threadA = new Thread(new Runnable() {
            @Override public void run() {
                int i = 0;
                while (!stop) {
                    i++;
                }
            }
        });
        // スレッド A の開始
        threadA.start();

        TimeUnit.SECONDS.sleep(1);
        // スレッド A を停止!
        stop = true;
    }
}

実際、このプログラムを動作させると永遠に threadA が終わらないことが判明します。

これはまさにメモリの可視性の問題です。 stop = true; という書込みはメインスレッドで行われていますが、この書込み結果が threadA からは見えていないのです。

threadA が書き込んだ値を見られるようにするには、同期化が必要です。 Java の同期化はアトミック性に加えてメモリの可視性も保証します。

同期化を使って次のようにコードを変更すると、先ほどのプログラムは正しく動作します。 同期化によってメモリの可視性が保証されるからです。

public class StopThread {
    private static boolean stop;
    private static synchronized void requestStop() {
        stop = true;
    }
    private static  synchronized boolean isStop() {
        return stop;
    }
    public static void main(String[] args) throws InterruptedException {
        Thread background = new Thread(new Runnable() {
            @Override public void run() {
                int i = 0;
                while (isStop()) {
                    i++;
                }
            }
        });
        background.start();

        TimeUnit.SECONDS.sleep(1);
        requestStop();
    }
}

同期化は次の動作を保証します。(『Javaの理論と実践: Javaメモリ・モデルを修正する 第2回』を読んで - The King's Museum)

  • synchronized ブロックに入る時、すべてメインメモリを見るようになる
  • synchronized ブロックを出る時、すべてメインメモリに書き込む

この事実から分かるように、書き込み側にだけ synchronized を指定しても意味がありません。

書き込み側の synchronized によってメインメモリに値が反映されることは保証されます。 一方、読み込み側に synchronized が指定されていないと自分のキャッシュの値を見てしまい、正しくない値を見てしまう可能性があります。

次回

まとめきれなかったので volatile とかは次回に…。

【Effective Java】項目65:例外を無視しない

空の catch ブロックを使って例外を無視してはいけません。

// 空の catch ブロック
try {
  ...
} catch (Exception e) {
}

例外を無視するとエラーがあるという事実が隠蔽されてしまいます。 それによって、他の場所で別の形でエラーが再現する可能性もあります。

最低でも例外を無視する意図を書くべきです。 もし、可能であれば例外をログなどに記録するべきです。

例えば FileInputStream をクローズする時、すでにデータの読取りに成功していれば例外は無視してもかまわないでしょう。

FileInputStream stream;
... 読み出し処理 ...
try {
    stream.close();
} catch (Exception e) {
    // データの読み取りは成功しているので例外は無視し、ログだけ記録する
    Log.warn("stream.close() is failed: ", e);
}

このアドバイスはチェックされる例外にもチェックされない例外にもあてはまります。

チェックされる例外の場合は、一般的に回復可能な処理があるはずです。 そのため例外を無視せずに適切な処理を行うべきです。

チェックされない例外は一般的にはプログラミングエラーを示しています。 空の catch ブロックで例外を無視するのではなく、例外を外側に伝播させてシステム全体のエラーとしてしまったほうが適切です。

【Effective Java】項目64:エラーアトミック性につとめる

オブジェクトのメソッドが例外をスローしたあとは、そのメソッドを呼び出す前の状態になっているべきです。 この性質を「エラーアトミックである」といいます。

「エラーアトミック」を達成する方法はいくつかあります。

不変オブジェクト

エラーアトミックを達成するいちばん簡単な方法は不変オブジェクトを使うことです。

不変オブジェクトはオブジェクト作成後に状態が変化しませんので、メソッドで例外がスローしたとしても状態は変化しません。

パラメータチェック

次に簡単な方法はメソッド内でオブジェクトの状態を変更する前に、各種パラメータチェックを行うことです。

簡単なスタック実装の pop() メソッドを例にとります。

public Object pop() {
    Object result = elements[--size];
    elements[size] = null;
    return result;
}

この pop() 実装では、size が 0 よりも下回ると IndexOutOfBoundsExeption が発生して、例外がスローされます。 このとき、size の値は 0 を下回ったままですので不整合な状態になってしまいます。

そこで、次のように事前にパラメータチェックを行います。

public Object pop() {
    if (size == 0) {
        throw new EmptyStackException();   
    }
    Object result = elements[--size];
    elements[size] = null;
    return result;
}

このように実装すれば、例外がスローされても状態は変化しませんし、抽象概念の正しい例外を発生させることもできます(項目61)。

状態変更前の事前計算

オブジェクトを変更する前に必要な計算を行ってしまって、不整合を起こす状態になったら例外を発生させてしまう方法です。

この方法でエラーアトミック性を実現しているのは TreeMap です。

TreeMap に要素を追加するためには、オブジェクトに「順序付け可能」という性質が必要です。 TreeMap にオブジェクトを要素に追加しようとすると、まずオブジェクトの検索が行われます。

この検索は木構造にオブジェクトが追加される前に行われ、もし順序づけが不可能であればこの時点で例外が発生します。 木構造にオブジェクトが追加される前に例外がスローされるため、木構造は不整合な状態になりません。

回復コードを書く

例外が発生した際に、状態を復元する回復コードを書くという方法です。 例外を発生させる処理をする前の状態を保存しておく必要があるでしょう。

コピーを作る

例外が発生する処理を行う前にコピーオブジェクトを作成し、それを利用する方法です。 操作はすべてこのコピーオブジェクトに対して行い、すべての操作が成功した段階で本当のオブジェクトに付け替えるという方法です。

エラーアトミックの限界

エラーアトミック性は望ましいことですが、複雑性・コストを増大させることが多いので、必ずしも望ましいとは限りません。

もし、エラーアトミック性が保てない場合は、かならず仕様の一部としてドキュメント化するべきです。

【Effective Java】項目63:詳細メッセージにエラー記録情報を含める

システムがキャッチされない例外で終了すると次の情報が出力されます。

最後の「例外の詳細メッセージ」はエラーの原因を調査するための唯一の手がかりであることが多いです。 そのため、例外の詳細メッセージにはエラー時の記録情報を必ず含めるべきです。

たとえば配列の範囲外にアクセスしているという IndexOutOfBoundsException 例外なら詳細メッセージには次の値を含んでいるべきです。

  • 実際のインデックス
  • 下限範囲
  • 上限範囲

このとき、不必要なメッセージを含める必要はありません。

例外のメッセージとエンドユーザーに表示するメッセージは異なります。 エラーを解析するプログラマやサポートエンジニアにとって分かりやすい、端的なメッセージにするべきです。

コンストラクタイディオム

例外に必要なエラー情報を必ず含めることを保証する方法の一つは、コンストラクタでその情報を要求することです。

例えば、IndexOutOfBoundsException をこの手法で実装すると次のようになります。

/**
 * IndexOutOfBoundsException を生成する
 *
 * @param lowerBound 最も小さな正当インデックス値
 * @param upperBound 最も大きな正当インデックス値に 1 を足した値
 * @param index 実際のインデックス値
 */
public IndexOutOfBoundsExcetion(int lowerBound, int upperBound, int index) {
    super("Lower bound: " + lowerBound + 
        ", Upper Bound: " + upperBound + 
        ", Index: " + index);
    this.lowerBound = lowerBound;
    this.upperBound = upperBound;
    this.index = index;
}

この方法は Java ライブラリで使われていませんが、かなり有用なイディオムです。

このイディオムを利用すれば、プログラマがエラーの記録をスキップすることはできません。 また、例外スローのたびにメッセージを生成する処理を書く必要もありません。

加えて、これらの情報へのアクセッサを提供することが適切かもしれません 特にチェックされる例外では、エラーから回復する時に有用な情報かもしれません。この場合は積極的にアクセッサを提供するべきです。

チェックされない例外でも、toString() 文字列をパースされてしまうことを防ぐためアクセッサを提供してもいいかもしれません。

Productivity Hack 2

朝一に1日のタスクリストを作るのではなく、お昼にその日の午後と次の日の午前のタスクリストを作ることで Productivity を改善しましたよ、というお話。

(僕が今使ってる Productivity 系の話は次の記事からどうぞ。)

hjm333.hatenablog.com

詳細

『エンジニアの時間管理術』の中で「自分のリズムを知れ」というものがあって、これは「自分の集中できる時間帯や、体調のリズムを把握して、もっとも調子のいい時に重要なタスクをしろ」というものだ。

最近、自分は午前中の集中力がかなり高いことに気づいた。お昼に一度下がって、午後にもう一度上がって、17時近くなるとだいぶ下がるという感じだ。きっと一般的にもこんな感じだろう。

今まで朝一の20分くらいは比較的雑務的なことをこなしていた。その日のタスク一覧を作成し、優先順位を決め、見積もって、スケジュリーング。勤怠を記録し、担当しているアプリのレビューをチェックし、クラッシュログを確認し、スラックとメールのチェック。

実はこのタスクはそれほど集中力を必要としないし、けっこう惰性でできてしまう。(タスク決めと優先順位付け、見積もりは重要といえば重要だが)。

午前の集中力が高いなら朝一からコード書いた方がいいんじゃね?とふと思った。そこで、朝一からコーディングをすることにして朝一にやってたタスクはお昼ご飯を食べた後にやることにした。そのときにその日の午後と次の日の午前中までのタスクを埋めておくことにした。

感想

これが結構いい感じである。

まず、自分にとって午前中は集中できるのだから、たとえ20分だとしても午前中をコーディングの時間として使えるのはすごく大きい。

二つ目に、一日ごとにタスクをきるとタスクが終わらない場合にちょっと不完全燃焼感が残ってしまう。今は子供の世話があるので一切残業していないけど、もしそういう状況でなければ、タスクが終わるまで残業してしまうだろう。

三つ目、これは意図しない効果だったのだが、仕事を終えてから次の日に仕事を始めるまでにタスクの連続性が生まれた。その結果、家でも無意識にタスクの解決策を考えるようになり、次の日に出社した時には試してみたいことや実装したいことで頭が満たされていて、いち早く仕事に取り組めるようになった。

最後のやつは家でも仕事のことを考えてるので、どうなんだろう、というところもあると思うけれども、結果的に出力が上がるので悪いとは思ってない。

始めて2週間だからまだまだ観測が足らない感じもするけど、うまくいってるような感触を持っている。今のところデメリットを感じるところもないし。

あとはこうやって自分の資質などを客観的に評価して、よりよい方法を見つけられるようになったことはすごくいいことだとおもいました。まる。

【Effective Java】項目62:各メソッドがスローするすべての例外を文書化する

チェックされる例外は個別に宣言するべきです。 そして、それぞれの例外の発生条件を Javadoc@throws に必ず文書化するべきです。

throws Exceptionthrows Throwable というように、複数の例外をまとめて throws 宣言してはいけません。 扱うべき例外が不明瞭になってしまうためです。

チェックされない例外は throws 宣言をするべきではありません。 その代わりに Javadoc@throws に文書化するべきです。 これはメソッドの事前条件を示すものとして機能します。

ただし、すべてのチェックされない例外を文書化することはほぼ不可能であることも覚えておいてください。 例えば自分が作成したメソッドの実装を変えていなくても、下位レイヤでスローされる例外は変更されているかもしれないからです。

まとめると以下のようになります。

throws Javadoc
チェックされる例外 個別に宣言する 必ず文書化する
チェックされない例外 宣言しない 文書化するべきだが、すべては不可能

クラスの各メソッドが同じ例外を発生させる場合、各メソッドではなくクラス自体の Javadoc にまとめて書く場合もあります。

(c) The King's Museum