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

データ構造

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

ミュータブルとは、オブジェクトの生成後にその状態を変更可能であることで、イミュータブルはその反対に、状態を変更できないこと。

final修飾子は、参照を変更不可能にすることを保証するが、オブジェクトの状態を変更不可能にすることは保証しないことに注意する。

イミュータブルなオブジェクトは、生成後に状態の変更ができないため、複数のスレッドで同じイミュータブルなオブジェクトを使用している場合でも安全に使用できる。

Defensive Copying

Java ではオブジェクトは参照によって共有されるため、ミュータブルなオブジェクトの参照を共有する場合、誰かがそのオブジェクトに変更を加えた時点で、すべての参照を共有している箇所にその変更の影響が波及してしまう。

以下の例では、初期化や状態の取得時にこの問題を誘発する。

public class SomethingMutable {
    private final List<Object> mList; // 参照は書き換えられないが…

    public SomethingMutable(List<Object> list) {
        mList = list; // 参照をそのまま渡すので、コンストラクタの呼び出し側で、渡したリストを変更すると、その影響を受けてしまう
    }

    public List<Object> getList() {
        return mList; // 参照をそのまま返すので、このメソッドの呼び出し側で、返って来たリストを変更すると、その影響を受けてしまう
    }
}

フィールドが保持する参照が変更不可能であること意外にも、参照を通じた変更を許容しないことがイミュータブルであることの条件となるので、以下のように防御的コピーの手法を用いる。

public class SomethingImmutable {
    private final List<Object> mList;

    public SomethingImmutable(List<Object> list) {
        mList = new ArrayList<Object>(list); // 別のオブジェクトの参照を保持することで、呼び出し側の変更の影響を受けないようにする(防御的コピー)
        // mList = Arrays.asList(list.toArray()); 配列に変換して再度 List 化する方法もある
    }

    public List<Object> getList() {
        return new ArrayList<Object>(mList); // 別のオブジェクトの参照を返すことで、呼び出し側の変更の影響を受けないようにする(防御的コピー)
        // return Collections.unmodifiableList(mList); // 変更不可能なコレクションを生成するユーティリティを使うことも可
    }
}

Builder パターン

多数のメンバ変数を持つオブジェクトを生成する際には、コンストラクタよりも Builder パターンを用いることが推奨されている。
このパターンを用いて、イミュータブルなオブジェクトを生成する。

public class Something {
    private final String mName;
    private final String mLocation;
    private final int mAge;
    private final Date mBirthday;

    private Something(Builder builder) {
        mName = builder.name;
        mLocation = builder.location;
        mAge = builder.age;
        mBirthday = builder.birthday;
    }

    public String getName() {
        return mName; // String はイミュータブルなので参照をそのまま返しても問題ない
    }

    public int getAge() {
        return mAge;
    }

    public Date getBirthday() {
        return new Date(mBirthday.getTime()); // Date はミュータブルなので、防御的コピーをする
    }

    // Something のビルダー
    public static class Builder {
        private final String name;
        private String location;
        private int age;
        private Date birthday;

        public Builder(String name) {
            this.name = name;
        }

        public Builder setLocation(final String value) {
            location = value;
            return this;
        }

        public Builder setAge(final int value) {
            age = value;
            return this;
        }

        public Builder setBirthday(final Date value) {
            birthday = new Date(value.getTime());
            return this;
        }

        public Something create() {
            return new Something(this);
        }
    }
}

参照の管理

WeakReference

WeakHashMap

列挙型の活用

Singleton パターン

Strategy パターン

Enum Factory パターン

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

アノテーション

New I/O

バッファ

チャネル

New I/O2

非同期チャネル

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

Clone this wiki locally