Skip to content
This repository was archived by the owner on Jun 20, 2023. It is now read-only.

A.04. Java の活用

Keishin Yokomaku edited this page Feb 15, 2014 · 38 revisions

この章では、より発展的な Java の活用と実践について簡単に解説します。
数々の API の使い方と合わせて、様々なプラクティスやイディオムについても含まれます。

参考:Effective Java

目次

  • マルチスレッド
    • スレッドプール
    • 原子性と可視性
    • スレッドセーフ
    • 遅延初期化
      • スレッドセーフでない実装
      • Double Checked Locking
      • Initialization-on-demand Holder
    • 同期化を支援する仕組み
      • CountDownLatch
      • Semaphore
  • データ構造
    • ミュータブルとイミュータブル
    • Defensive Copying
    • Builder パターン
  • 参照の管理
    • WeakReference
    • WeakHashMap
  • 列挙型の活用
    • Singleton パターン
    • Strategy パターン
    • Enum Factory パターン
    • 列挙型とコレクションフレームワーク
  • アノテーション
  • [New I/O](#New I/O)
    • バッファ
    • チャネル
  • [New I/O2](#New I/O2)
    • 非同期チャネル

マルチスレッド

スレッドプール

Android を始めとして、各種の GUI を構築するアプリケーションでは、UI Thread(Main Thread) をブロックする各種の処理のためのスレッド(Worker Thread)を用いた非同期処理を実装する。

UI のイベントに応じてネットワークやデータベース等へアクセスする頻度が高いアプリケーションの場合、イベントが発生するごとに新規にThreadを立ち上げて動作させると、パフォーマンスの問題が発生する。

そこで、ある程度Threadのインスタンスをプールしておき、適宜インスタンスを使いまわしていく仕組みとして、ThreadPoolExecutorを用いる。

この仕組は Android の標準の非同期処理フレームワークであるAsyncTaskでも用いられている。

TODO: Sample code here

原子性と可視性

原子性(アトミック性)とは、あるスレッド上での、あるデータへの複数の操作が、他のスレッドからみて単一の操作に見えること。データの状態遷移の過渡的な不整合な状態が見えない性質とも言う。

Java のプリミティブ型のうち、long と double 以外の型の操作は原子性が保証されている。long と double は 64bit のデータで、その読み書きの際に複数の操作が発生するため原子性が保証されない。

一方で、int 等の整数のインクリメント操作やデクリメント操作の記法(count++;count--;)は、その操作の中に複数の操作(読み取り、加算or減算、書き込み)を含むため、原子性が保証されない。

64 bit データの操作やインクリメント・デクリメント操作の原子性を保証するには、synchronized による同期化をするか、AtomicIntegerクラスなどの原子性を保証する操作を実現するラッパークラスを使用する。

可視性とは、どのスレッドからでも同じ値が見えること。

通常、スレッドを複数立ちあげると、変数の値はスレッドごとにキャッシュされる仕組みになっている。このため、スレッドごと値の更新と参照に不整合が起こることがある。

同期化では、この原子性と可視性の両方を保証する必要がある。

volatile修飾子は可視性を保証し、どのスレッドからでも同じ値を見えるように、スレッドごとのキャッシュを使わないようにする。

スレッドセーフ

スレッドセーフであるとは、以下の条件を満たすこと。

  • インスタンスに対する操作をどんな順番で実行しても正しく振る舞う。
  • 複数のスレッドからの操作も同様に、どんな順番で実行しても正しく振る舞う。

順番が入れ替わると破綻したり、複数スレッドから操作を行う際、順番が狂うと破綻する操作をスレッドセーフでない操作という。

スレッドセーフにはレベルが有り、クラスの性質から幾つかのレベルに分類され、レベルによって使う側の同期の必要性の有無を判断する。

  • 不変 状態を持たないもの。イミュータブルなオブジェクトは使う側で同期化する必要がない。
  • 無条件スレッドセーフ 使う側で特別同期化をしなくてもよいもの。
  • 条件付きスレッドセーフ 一部に、使う側で同期化が必要な操作を含むもの。
  • スレッドセーフでない 同期化をしていないもの。
  • 敵対 マルチスレッドで使えないもの。

遅延初期化

通常、メンバ変数の初期化はコンストラクタで行う。しかし、コンストラクタでの処理がパフォーマンスに影響をおよぼす場合、メンバ変数を、それが必要になった時に初めて初期化をするようにすることで、コンストラクタのパフォーマンスを向上させることが出来る。このようなチューニングのノウハウを遅延初期化と言う。

遅延初期化は、シングルトンパターンの実装にも見られる。

このイディオムは、マルチスレッドで正しく動作させるために工夫が必要になるため、特に理由のない限り、必要なければ使わないことが推奨されている。

スレッドセーフでない実装

メンバ変数を、必要になったタイミングで初期化する単純な実装は以下のとおり。

public class LazyInitializationSample {
    private Map<String, Object> mMap;

    public LazyInitializationSample() {} // コンストラクタで初期化しない

    public void add(String key, Object data) {
        if (mMap == null) { // mMap を使う直前で初期化する
            mMap = new HashMap<String, Object>();
        }
        mMap.put(key, data);
    }
}

この実装は、単一のスレッドで使用する場合には問題ないが、複数のスレッドからLazyInitializationSampleのインスタンスを操作しようとするときに問題を発生させる可能性がある。

ひとつには、HashMapの中のデータの操作が同期化されないため、結果が不定となること。
もうひとつは、mMapの初期化処理が同期化されないため、これも結果が不定となること。

いずれにしても、スレッドセーフでない操作が含まれるため、予期せぬ動作を招くことがある。

HashMap の中のデータの操作を同期化するには、HashMapではなくConcurrentHashMapを使用することで解決できる。
スレッドセーフなコレクションはjava.util.concurrentパッケージにいくつかの実装があるほか、Collectionsクラスのユーティリティメソッドを用いてスレッドセーフなコレクションを生成することも出来る。

public class LazyInitializationSample {
    private Map<String, Object> mMap;

    public LazyInitializationSample() {} // コンストラクタで初期化しない

    public void add(String key, Object data) {
        if (mMap == null) { // mMap を使う直前で初期化する
            mMap = new ConcurrentHashMap<String, Object>();
        }
        mMap.put(key, data);
    }
}

上記の場合も、mMapの初期化がスレッドセーフではない。

よって、以下のようなイディオムを使用して、スレッドセーフな実装とする。

Double Checked Locking

null チェックをsynchronizedブロックの外と内で二度行うことから、Double Checked と呼ばれる。

public class LazyInitializationSample {
    private volatile Map<String, Object> mMap; // どのスレッドからも常に同じ値を見る(可視性)ことを保証

    public LazyInitializationSample() {}

    public void add(String key, Object data) {
        if (mMap == null) { // 既に初期化が終わっている場合はロックを取らず処理を継続
            synchronized (this) { // 自身のオブジェクトをミューテックスとしてロックを取得し、クリティカルセクションに突入
                if (mMap == null) {  // ロック解放待ちの間に mMap が初期化された場合は何もしないようにするためのチェック
                    mMap = new HashMap<String, Object>();
                }
            }
        }
        mMap.put(key, data);
    }
}

このイディオムを正しく動作させるためには、volatileの役割が欠かせない。 ただし、Java 1.4 と Java 1.5 でvolatileの保証する範囲が異なり、Java 1.4 の volatile修飾子で保証する範囲では不足があるため、Java 1.4 以前でこのイディオムは正しく動作しない。

以下のように、シングルトンパターンの実装にも用いられる。

public class Singleton {
    private static volatile Singleton sInstance;

    protected Singleton() {}

    public static void getInstance() {
        if (sInstance == null) {
            synchronized(Singleton.class) {
                if (sInstance == null) {
                    sInstance = new Singleton();
                }
            }
        }
        return sInstance;
    }
}

Initialization-on-demand Holder

staticなフィールドが、クラスをロードしたタイミングで初期化されることと、staticな内部クラスが、使用されるタイミングで初めてロードされることを利用したイディオム。
クラスのロードは VM 上で逐次実行されることと、staticなフィールドの初期化も逐次実行されることから、同期化のコードを書かなくてもよい。これにより、同期化に掛かるオーバヘッドも削減できるほか、Java のバージョンに依らず正しく動作する。

public class LazyInitializedObject {
	private LazyInitializedObject() {}
 
	private static class LazyHolder {
		private static final LazyInitializedObject INSTANCE = new LazyInitializedObject();
	}
 
	public static LazyInitializedObject getInstance() {
		return LazyHolder.INSTANCE; // LazyHolder がロードされた時のみコンストラクタが呼ばれる
	}
}

同期化を支援する仕組み

CountDownLatch

Semaphore

データ構造

ミュータブルとイミュータブル

Defensive Copying

Builder パターン

参照の管理

WeakReference

WeakHashMap

列挙型の活用

Singleton パターン

Strategy パターン

Enum Factory パターン

列挙型とコレクションフレームワーク

アノテーション

New I/O

バッファ

チャネル

New I/O2

非同期チャネル

GitHub Pagesへ移行しましたmixi-inc.github.ioへお願いします。

Clone this wiki locally