Java EE 7で並列処理がケタ違いに速くなる! 使いこなしのポイントは?──Java Day Tokyo 2013レポート

builder by ZDNet Japan Ad Special
2013-07-26 11:15:00
  • このエントリーをはてなブックマークに追加

Javaにおける非同期処理の歴史


日本オラクル 製品事業統括Fusion Middleware事業統括本部 Javaエバンジェリストの寺田佳央氏

 Java Day Tokyo 2013において、「エンタープライズ環境における並列処理の実装方法について」と題したセッションの講師を務めた寺田氏は、Java EE 7から利用可能になるConcurrency Utilities for Java EE(JSR-236)の歴史や実装方法、ポイント、注意点などについて解説した。

 セッションの冒頭、寺田氏はJavaにおける非同期処理の歴史として、Java SE環境におけるスレッド処理の実装などの変遷を振り返った。

 Javaは1996年に正式リリースされたJDK 1.0からマルチスレッドをサポートしており、2004年にJava SE 5でConcurrency Utilitiesを導入。これにより、並列処理やスレッド・プーリング、キューイングなどが簡単に実装できるようになった。

 また、2011年にリリースされたJava SE 7ではFork/Joinに対応。さらに、2014年にリリース予定のJava SE 8ではラムダ式をサポートし、これまで以上に並列処理が簡単に記述できるようになるという。

 このように、Javaにおける非同期処理の仕組みは着々と進化を続けているが、残念ながら開発の現場では、まだこれらの機能が十分に活用されていないと寺田氏は嘆く。氏はサンプル・コードを示しながら、「スレッド処理に関して、いまだにJavaの登場当初からのnew Thread(r).start(); などと書いているプログラムを多く見かけるが、もうこのような実装はやめたほうがよい」とアドバイスした。スレッドを無限に生成するこのコードでは、スレッドを生成する度にスタック領域が確保されてメモリを消費するほか、コンテキスト・スイッチも発生するため、パフォーマンスは決して良くならない。スレッド生成は処理コストが高く、こうした方法ではマルチコア・プロセッサ環境でその性能を十分に発揮できないのだという。

 そこで現在、スレッドを直接生成する方法に代わって推奨されているのが、Java SE 5 から導入されたConcurrency Utilitiesである。Concurrency Utilitiesは並列処理の実装を簡素化するために導入されたAPIで、これを利用することによってスレッドのライフサイクル管理が簡単になるほか、スケーラビリティやパフォーマンスが大幅に向上するのだという。

 それではConcurrency Utilities の導入により、パフォーマンスはどのくらい変わるのだろうか。寺田氏は、256個のプロセッサを持つ環境上で、シングルスレッド、マルチスレッド、Concurrency Utilitiesを使用してそれぞれどの程度パフォーマンスが変わるのかをグラフィカルに確認することのできるデモを披露した。このデモでは、シングルスレッドやマルチスレッドのプログラムが256個のうち一部のプロセッサしか利用できないのに対して、Concurrency Utilitiesを使った場合は256個のプロセッサの負荷がほぼ同時に限界まで達し、瞬時に処理を終えることが確認できた。

 「Javaはパフォーマンスが悪いという声を聞くことがあるが、それは間違い。パフォーマンスが悪いのは、いまだに古いやり方でプログラムを書いているからにすぎない。Concurrency Utilitiesでマルチコア・プロセッサの能力をフルに使い切れば、Javaは驚異的なパフォーマンスを発揮する」(寺田氏)

Java EE 7からサーバ・サイドでもConcurrency Utilitiesが使える

次に寺田氏は、Java EEにおける非同期処理の歴史を振り返る中で、1999年リリースのJ2EE 1.2で追加されたJava Message Service(JMS)、2001年リリースのJ2EE 1.3で追加された「Message-Driven Bean(MDB)」、さらには2009年リリースのJava EE 6で追加された「Async Servlet」、「Async EJB」について説明した。

 JMSとMDBを使用すると、アプリケーション・サーバ管理者が事前にMQ用の接続を設定しておくことで、その設定情報をプログラマーがインジェクト(注入)してMQのプロバイダにアクセスするといったプログラミングが可能になる。

 このJMSやMDBの実装は、Java EE 7にバージョンアップすることで「これまで以上に楽になる」と寺田氏は話す。また、Java EE 6で採用されたJava Servlet 3.0から、「asyncSupported = true」というアノテーションを付加することで非同期処理が行えるようになった。

【Java Servlet 3.0におけるサーブレットでの非同期処理】

@WebServlet(name = "MailSenderServlet", urlPatterns = 
     {"/MailSenderServlet"},asyncSupported = true)
public class MailSenderServlet extends HttpServlet {
   protected void processRequest(
          HttpServletRequest request, 
          HttpServletResponse response)
              throws ServletException, IOException {
          AsyncContext ac = request.startAsync();
          ac.start(new MailSenderRunnable(ac));
}

 EJB 3.1でも、POJO(Plain Old Java Object)に「@Stateless」というアノテーションを付加し、メソッドに「@Asynchronous」というアノテーションを付加すれば非同期で動作させられるという。

【EJB 3.1における非同期処理】

@Stateless
public class SyncEmailSenderEJB {
    @Inject
    MailSender mailsend;

    public void syncSendMessage(String email){
        mailsend.sendMessage(email);
    }

    @Asynchronous
    public void asyncSendMessage(String email){
        mailsend.sendMessage(email);
    }
}

 ただし、「このように簡単に実装できるようになるものの、これは単なる非同期処理であって、並列(Concurrency)処理ではないことを忘れないでほしい」と寺田氏は注意を促す。ここで氏は、同期処理と非同期処理の違い、そして並列処理と非同期処理の違いを説明した。

 同期処理と非同期処理の違いの説明で寺田氏が用いたのは、iPhoneにメール・メッセージを送るEJBアプリケーションのデモである。

 このデモで同期処理の場合には、メッセージの送信命令を実行した後にメール送信が完了するまで待ち続けることになる。その間、ユーザーにレスポンスを返すことはなく、結果として、ユーザーは処理が完了するまで画面を操作できなくなる。 これに対して 非同期処理の場合、メッセージの送信命令を実行した後、 ただちに処理完了のレスポンスを返すことができるため、 ユーザーはすぐに画面操作が行える。このとき、アプリケーションは非同期でメール送信の処理を行っている。

 それでは、非同期処理と並列処理はどう違うのだろうか。寺田氏は、複数の旅行代理店に対してホテルの空室状況や飛行機の空席情報を検索するシステムを想定し、各旅行代理店に並列で検索要求を出し、検索が完了した順に結果を一覧表示するデモ・アプリケーションを披露した。このアプリケーションは、Concurrency Utilities for Java EEの技術に、同じくJava EE 7の新機能の1つであるWebSocket(JSR-356)の技術を組み合わせて実装されている。

 非同期処理をJava EE 6で採用されたJava Servlet 3.0のAsync Servletを利用した場合、ユーザーが検索処理を実行すると、サーブレットのインスタンスから派生した別の非同期処理用のAsync Contextを生成して“単一の”非同期タスクを実行する。この非同期実行されるタスクでは、複数の旅行代理店に問い合わせを行う場合でも、それぞれの旅行代理店に対して順番に問い合わせる必要がある。言い換えれば、Java EE 6では非同期で処理は行えるが、非同期処理の実装で並列処理(新たなスレッドの生成)をサポートしていないため、タスクの実装では複数の旅行代理店に対して順番に問い合わせざるをえなかったのだ。

 一方、並列処理では、上のような非同期処理を1つだけ実行するのではなく、複数のタスクを並列に同時実行することができる。したがって、複数の旅行代理店がある場合は、それぞれに対して一斉に並行して問い合わせることが可能となる。実際にデモの画面では、各旅行代理店から検索結果が返ってきた順に結果が表示された。

 「Java EE 6までは、EJBコンポーネントやサーブレットの中から新しいスレッドを生成することは仕様として禁止されていた。新しいスレッドを作った場合、同じJVM上でスレッドが作られるものの、アプリケーション・サーバが管理するスレッドとは別のスレッドとして生成されるため、アプリケーション・サーバからそのスレッドを管理する手段がなかったのだ」(寺田氏)

 したがって、新たに作ったタスクに対してコンテキスト情報やクラス・ローダ、セキュリティなどの情報を渡すことができず、さらに生成されたタスクからアプリケーション・サーバのリソースを呼び出すこともできなかった。加えて、「アプリケーション・サーバが停止、またはアプリケーションが無効化された場合でも、ライフサイクルが異なるために生成されたタスクは処理を続行してしまうといった管理不能の状態に陥る可能性もあった」(寺田氏)。

 それに対して、Java EE 7で追加されるConcurrency Utilities for Java EEでは、APIを介してアプリケーション・サーバ内でタスク管理が行えるようになるのだ。

Concurrency Utilities for Java EEを使ううえでの4つのポイント

 こうして、これからはJava EE開発で並列処理にConcurrency Utilitiesが使えるようになるわけだが、「その使用に際しては、まずパッケージjavax.enterprise.concurrentに含まれる次の4つのインタフェースの特徴を押さえておいてほしい」と寺田氏は説明する。

(1)インタフェースManagedExecutorService
(2)インタフェースManagedScheduledExecutorService
(3)インタフェースContextSerivce
(4)インタフェースThreadFactory

 これらのうち、(1)と(2)は非常に簡単に非同期のタスクを利用できるインタフェースであり、(3)、(4)は使い方が少し難しいが、自分で並列処理をコントロールしたい場合に利用するインタフェースである。

 最も簡単に並列処理を利用できるのは、(1)のインタフェースManagedExecutorServiceだ。このインタフェースを使う場合には、まずインタフェースRunnableかCallableをimplementsしたタスクを作成する。

【インタフェースRunnableでタスクを作成】

public class MyRunnableTask implements Runnable {
    @Override
    public void run() {        
        try {
            Thread.sleep(10000); //何らかの処理
        } catch (InterruptedException ex) {
            logger.log(Level.SEVERE, null, ex);
        }
    }
}

 続いて、サーバ側の設定を行う(デフォルト設定でも可)。

 そして最後に、作成したタスクに対して「managedExecsvc.submit(task);」といったコードを実装するだけでよい。

【非同期タスクを実行する EJBコンポーネント(サーバ側で設定したリソースをインジェクト)】

@Stateless
public class MyManagedExecutorService {
    @Resource(name = 
      "concurrent/DefaultManagedExecutorService")
    ManagedExecutorService managedExecsvc;
    public void execExecutorService() {
      MyRunnableTask task = new MyRunnableTask();
      managedExecsvc.submit(task);
        MyCallableTask singleTask = 
          new MyCallableTask("Foo Bar");
      Future<String> singleFuture =
        managedExecsvc.submit(singleTask);}

 (2)のインタフェースManagedScheduledExecutorServiceを使う場合も同様で、まずタスクを作成してサーバの設定を行い、「managedScheduledExecsvc.schedule(task, 60L, TimeUnit.SECONDS);」」といったコードを実装するだけで、タスクのスケジューリング(ここでは1分後にタスクを実行)が可能になる。

【非同期タスクを実行する EJBコンポーネント(1分後にタスクを実行する例)】

@Stateless
public class MyManagedScheduledExecutorService{
 @Resource(name = "concurrent/
             DefaultManagedScheduledExecutorService")
 ManagedScheduledExecutorService managedScheduledExecsvc;

 public void execScheduledExecutorService() {
   MyRunnableTask task = new MyRunnableTask();
   managedScheduledExecsvc.schedule(
      task, 60L, TimeUnit.SECONDS);
 }

 上のコードは時間を指定してタスクを実行する場合の例だが、独自のトリガを指定することも可能であり、どのようなときにタスクを実行するのか、あるいはスキップするのかをプログラム側でコントロールすることもできる。

【非同期タスクを実行する EJBコンポーネント(独自のトリガを指定)】

@Stateless
public class MyManagedScheduledExecutorService{
 @Resource(name = "concurrent/
             DefaultManagedScheduledExecutorService")
 ManagedScheduledExecutorService managedScheduledExecsvc;

 public void execScheduledExecutorService() {
   MyRunnableTask task = new MyRunnableTask();
   managedScheduledExecsvc.schedule(
      task, new MyTrigger(new Date(), 10, 1000) }

 大抵の場合は(1)、(2)の方法で目的を達せられるだろうが、Java SEで培った資産をJava EE環境で再利用したいといった場合、そしてConcurrency Utilities for Java EEの本質を理解するためには、以降で紹介する(3)、(4)の方法を理解することが重要だと寺田氏は語る。

 (3)のインタフェースContextSerivceは、先のインタフェースManagedExecutorServiceやManagedScheduledExecutorServiceも内部的に使用しており、「Concurrency Utilitiesの肝になる部分」(寺田氏)である。Java EEのConcurrency Utilities for Java EE では、内部的にJava SEのパッケージjava.lang.reflectが提供するクラスProxyの動的プロキシ機能を使い、Java EE環境上で動作させるために必要なコンテキスト情報などを付加して実行する。オリジナルのタスクに対してコンテキスト情報などを付加することで、次のサンプル・コードのようにインタフェースManagedExecutorServiceの代わりに、Java SEで提供されるインタフェースExecutorServiceを利用することも可能になる。

【コンテキスト付きタスクの実行(動的プロキシを生成)】

@Stateless
public class ContextServiceManager {
  @Resource(name = "concurrent/DefaultContextService")
  ContextService ctxSvc;

  public void execSimpleContextService() {
    ExecutorService singleThreadExecutor = 
      Executors.newSingleThreadExecutor(threadFactory);
    MyRunnableTask task = new MyRunnableTask();
    Runnable proxiedTask =
      ctxSvc.createContextualProxy(task,Runnable.class);
    singleThreadExecutor.submit(proxiedTask);}}

 さらに、(4)のインタフェースThreadFactoryを使うことで、Java SEのパッケージjava.util.concurrentが提供するクラスThreadPoolExecutor (もしくはThreadPoolExecutorのユーティリティ・クラスであるExecutors)を使って独自のスレッド・プールを定義し、そこから新たなスレッドを生成することも可能だという。

【カスタム・スレッド・プールからスレッドを生成】

@Resource(name = "concurrent/
                   DefaultManagedThreadFactory")
ManagedThreadFactory threadFactory;
public void execThreadFactory() {
  MyRunnableTask task = new MyRunnableTask();
  ExecutorService exec = 
           new ThreadPoolExecutor(4, 4,
           0L, TimeUnit.MILLISECONDS,
            new LinkedBlockingQueue(), 
           threadFactory);
  exec.submit(task);}

 なお、Concurrency Utilities for Java EE 7を使う際の注意点として、「Java SE 7で提供されるFork/Joinのスレッド・プールは使えない」、 「アプリケーション・クライアント・コン テナでは使えない」、「CDI (Contexts and Dependency Injection)では使用に制限がある」といったことが挙げられる。

 最後に寺田氏は、「Concurrency Utilities for Java EE 7を使い非同期マルチタスクを並列に実行することで、計算機資源をより有効に活用できるようになる。Java EE環境で簡単かつ安心してスレッド生成が行えるようになり、柔軟性も非常に高いので、ぜひ皆さんもこの機能をフルに活用していただきたい」と呼びかけて講演を締めくくった。