-
Notifications
You must be signed in to change notification settings - Fork 353
A.04. Java の活用
この章では、より発展的な Java の活用と実践について簡単に解説します。
数々の API の使い方と合わせて、様々なプラクティスやイディオムについても含まれます。
-
マルチスレッド
- スレッドプール
- 原子性と可視性
- スレッドセーフ
- 遅延初期化
- スレッドセーフでない実装
- 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
の初期化がスレッドセーフではない。
よって、以下のようなイディオムを使用して、スレッドセーフな実装とする。
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;
}
}
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 がロードされた時のみコンストラクタが呼ばれる
}
}
Portions of this page are reproduced from work created and shared by the Android Open Source Project and used according to terms described in the Creative Commons 2.5 Attribution License.