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

遅延初期化

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

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

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

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

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

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