Humanity

Edit the world by your favorite way

Java 8 でインスタンスを遅延して構築するいくつかの方法

シングルトンを構築するには、enum を使うのがスレッドセーフだしお手軽です。 なぜなら enum の初期化は1つのスレッドから1度だけ行われると規定されているからです。 また、コンストラクタを private にしてもコンパイルエラーにはならず、何の問題もなく初期化できます。

@ApplicationScoped
class EntityManagerProducer {

    @Produces
    public EntityManager getEntityManager() {
        return EntityManagerSingleton.INSTANCE.get();
    }

    private static enum EntityManagerSingleton {
        INSTANCE;
        private EntityManager em;
        private EntityManagerSingleton() {
            em = Persistence.createEntityManagerFactory("h2-unit").createEntityManager();
        }
        public EntityManager get() {
            return em;
        }
    }
}

しかし enum を使う方法だと引数を受け取る事ができません。 つまりシングルトンには使えますが、何らかの値をキャッシュする、最初の一度だけ動くような処理には使えません。

Supplier

注意:冒頭の enum を使うコードはスレッドセーフですが、この章で記述しているコードはスレッドセーフではありません。紛らわしかったのでスレッドセーフにしたバージョンも本記事の末尾に追記しました。

Supplier クラスを使います。 このクラスは Java 8 で追加されたクラスで、ラムダ式を代入する事ができます。

Supplier<Integer> two = () -> 1 + 1;
System.out.println(two.get());

以下は初回だけ「1たす1は?」と聞いて2回目以降は「にー」と答える間抜けなクラスです。 (import 等は省略しています)

public class LazyCalc {

    private static Supplier<Integer> two = () -> init();

    public static void main(String[] args) {
        two.get();
        two.get();
        two.get();
    }

    private static int init() {
        System.out.println("1たす1は?");
        two = () -> {
            System.out.println("にー");
            return 2;
        };
        return 1 + 1;
    }
}

出力は以下の通りです。

1たす1は?
にー
にー

…というようなテクニックが Javaによる関数型プログラミング ―Java 8ラムダ式とStream に書いてありました。 他にも平行・並列処理を割と *1 楽に書けたり簡潔に書けるテクニック満載なのでぜひ。

Javaによる関数型プログラミング ―Java 8ラムダ式とStream

Javaによる関数型プログラミング ―Java 8ラムダ式とStream

他のコード例はこちらをご覧ください。

github.com

チェック例外をかいくぐる

今度は一歩進んで、今度は重い処理を疑似的に再現させるため、入力待ちにしてエンターを押すまで実行を止めるようにしてみます。 先ほどの init()System.in.read() を追加して、IOException を投げるようスロー宣言をメソッドに追加します。

    private static int init() throws IOException {
        System.out.println("1たす1は?");
        System.in.read();
        two = () -> {
            System.out.println("にー");
            return 2;
        };
        return 1 + 1;
    }

しかしこれはコンパイルエラーになります。 理由は Supplier#get() に throws IOException が付いていないためです。 上記のチェック例外の制約をかいくぐるために、IOException を RuntimeException にラップする必要があります。 Java 8 にはこのための標準のクラスがあります。UncheckedIOException クラスです。

   public static void main(String[] args) {
        try {
            two.get();
            two.get();
            two.get();
        } catch (UncheckedIOException e) {
            e.getCause().printStackTrace();
        }
    }

    private static int init() {
        System.out.println("1たす1は?");
        try {
            System.in.read();
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
        two = () -> {
            System.out.println("にー");
            return 2;
        };
        return 1 + 1;
    }

これを実行すると

1たす1は?
(エンター)
にー
にー

無事初回だけ入力待ちにする事ができました!

他のチェック例外に関しては UncheckedIOException のようなラッパークラスは用意されていない事もありますが、自作するのはそう難しくないはずです。

感想

Java のライブラリ自身が最近チェック例外ではなく割と実行時例外を投げるように設計されてるっぽい*2のは、ラムダ式との相性が悪いのもあるんだろうなーと思いました(まる)。 でもチェック例外が全面的に悪いものだとは思えないんだよなー。レイガイムズカシイネ。 結論としては、あるメソッドが投げる例外とスレッドセーフかどうかはなるべく javadoc に書くべき。

あとチェック例外に関しては、代わりにアノテーション、というより Pluggable Annotation Processing API でチェックできるんじゃないか・していくんじゃないかとは思う。 あれは他の言語にはない機能で、C++ 以上にメタ的な情報を、メタな世界ではなく動的なコードで扱える(警告したりコンパイルエラーにしたり)という点で素晴らしいと思ってる。 Java がバージョンを重ねるにつれ、段々とアノテーションだらけになっていくのも無理はない。 Java で Web アプリ書くと設定ファイルだらけと言われてたけど、今は設定ファイル1個も書かずにアノテーションだけで設定を完結させたりもできる。*3

Java はゆっくりだけど、もっともっと変わっていくと思う。

参考リンク

追記(2016/10/8 23:45):「Supplier」の章のコードをスレッドセーフにする

public class LazyCalc {
    private static Supplier<Integer> two = () -> init();
    private static boolean initialized = false;
    // Future の Thread name 変えるの面倒なので横着して ThreadLocal を使用
    private static ThreadLocal<String> threadName = new ThreadLocal<String>();

    public static void main(String[] args) {
        final long start = System.nanoTime();
        final ExecutorService executor = Executors.newFixedThreadPool(3);
        final Future<Integer> a = executor.submit(() -> { threadName.set("a"); return two.get(); });
        final Future<Integer> b = executor.submit(() -> { threadName.set("b"); return two.get(); });
        try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
        final Future<Integer> c = executor.submit(() -> { threadName.set("c"); return two.get(); });
        executor.shutdown();
        try {
            System.out.println(a.get());
            System.out.println(b.get());
            System.out.println(c.get());
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
        System.out.printf("Program ran in %d seconds.\n", (System.nanoTime() - start) / 1_000_000_000L);
    }

    private static synchronized int init() {
        log("init()");
        if (!initialized) {
            log("1たす1は?");
            // 何か重い処理をする
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                throw new UncheckedInterruptedException(e);
            }
            two = () -> {
                log("にー");
                return 2;
            };
            initialized = true;
        }
        // synchronized の挙動が分かりづらくなる?のでコメントアウトしたけど普通は初期化された後の値を取得するよね…
        // return two.get();
        return 1 + 1;
    }

    private static void log(String msg) {
        System.out.println(threadName.get() + ":" + msg);
    }

    static class UncheckedInterruptedException extends RuntimeException {
        public UncheckedInterruptedException(InterruptedException e) {
            super(e);
        }
    }
}

出力結果(スレッド名を付けるようにしてみました)

a:init()
a:1たす1は?
b:init()
c:にー
2
2
2
Program ran in 2 seconds.

コメントにも書いてありますが、上記のコードを説明すると

  • a と b がほぼ同時に実行されると期待、2秒スリープした後、c を実行する
  • a と b は (A) 経由で two.get() を呼び出しているがこれは synchronized が付いているので2つとも init() を実行する事はない
  • どちらかの実行が終了した後は initialized == true となるので if の中の初期化は2つ目のスレッドは実行しない
  • a と b のスレッドが終了した後に c が実行されると期待される
  • ここでは two.get() は既に置き換わっているから synchronized は付いていない Supplier (B) が直接実行される
  • よって c 以降は synchronized でロックされる事はない

ぐだぐだ文章で書くより図で説明するとこんな感じ。

  • a:two.get()(A)→ synchronized init()(initialized == false なので初期化処理が実行される)
  • b:two.get()(A)→ synchronized init()(initialized == true なので初期化処理が実行される)
  • c:two.get()(B)(two 変数は置き換わった後なのでロック無しの(B)の処理が実行される)

*1:まぁ正直言うと、個人的には平行・並列処理書くならメッセージパッシングできる言語機能なりライブラリがある言語(Go、ScalaErlang 等々)の方がもっと楽に安全に書けるんだろうな…というのはあるけど

*2:JAX-RSアノテーションベースでなるべく throws を書かないとか。最近は throws Exception とかいうかっこ悪い API を実装させるインターフェースは少なくなってきてるように思います

*3:本格的に最近の Java EE 調べ始めたのが最近なので「今更?」と言うのはご容赦ください…

Java のマルチスレッドに関する本を読んだ

並行処理に限る訳ではないけど、ここに挙げた本読めば Java でそこそこ安全にマルチスレッドなプログラム書けそうな気がしてくる。 まぁ実際並行・並列処理は量書いて身につけないとしょうがないけど…

ぶっちゃけて言えば、読む前から変わらなかった意見は「並行・並列処理書くならメッセージパッシングの言語機能なりライブラリを持った言語で書くのが一番良いだろうな」だった。Go とか Scala とか Erlang とか。

まぁでも Stream API とか使えば結構カジュアルに並列処理できて便利という事が分かったので便利。 Java 8 になって Concurrency Utilities も色々強化されてるみたいなのでおさらいすると楽しそう。

Javaによる関数型プログラミング ―Java 8ラムダ式とStream

Javaによる関数型プログラミング ―Java 8ラムダ式とStream

Java仮想マシン仕様 (The Java series)

Java仮想マシン仕様 (The Java series)

  • 作者: ティムリンドホルム,フランクイェリン,Tim Lindholm,Frank Yellin,村上雅章
  • 出版社/メーカー: ピアソンエデュケーション
  • 発売日: 2001/05
  • メディア: 単行本
  • 購入: 5人 クリック: 98回
  • この商品を含むブログ (35件) を見る

Java 仮想マシン仕様は飛ばし読み(ニーモニック一覧の所とか)。 並行・並列処理に関しては8章のスレッドの部分。

あと定番のやつ。 もう一度読み直したい。

Effective Java 第2版 (The Java Series)

Effective Java 第2版 (The Java Series)

Java並行処理プログラミング ―その「基盤」と「最新API」を究める―

Java並行処理プログラミング ―その「基盤」と「最新API」を究める―

ちなみに

並行性と並列性について簡潔な説明が Oracle の公式ドキュメントにあるのを見つけた。

並行性と並列性 (マルチスレッドのプログラミング)

さらに

もっと詳しく知りたい人は以下の記事が公式ドキュメントへのリンクとその内容を例を交えて分かりやすく解説してくれてらっしゃる神記事なのでどうぞ。

マルチコア時代に備えて本気でメモリモデルを理解しておこう - リオーダー & finalフィールド 編 - - じゅんいち☆かとうの技術日誌

関連記事