-
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
通常、メンバ変数の初期化はコンストラクタで行う。しかし、コンストラクタでの処理がパフォーマンスに影響をおよぼす場合、メンバ変数を、それが必要になった時に初めて初期化をするようにすることで、コンストラクタのパフォーマンスを向上させることが出来る。このようなチューニングのノウハウを遅延初期化と言う。
遅延初期化は、シングルトンパターンの実装にも見られる。
このイディオムは、マルチスレッドで正しく動作させるために工夫が必要になるため、特に理由のない限り、必要なければ使わないことが推奨されている。
メンバ変数を、必要になったタイミングで初期化する単純な実装は以下のとおり。
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.