いくつかのパターン

ライブラリ

いつくかあるので紹介しておきます。

  • Atomic クラス: java.util.concurrent.atomic.AtomicInteger などのクラス群
    • アトミックに各値にアクセスするための手段を提供します。CompareAndSet (所謂Compare And Swap (CAS)相当)や addAndGet のように、二つの命令をまとめて atomic に実行する命令も提供されます。
    • アトミックというのは、対象データへの命令実行中、途中に他のメモリアクセス命令が割り込まない形でという意味です。
    • 実装はしばしばマシン依存になるのですが、皆さんは使うだけでOK.
  • Concurrent 集合ライブラリ:java.util.concurrent.ConcurrentLinkedQueue などのクラス群
    • 集合データは、実装の仕方によって読み書きの並列度を上げることもできます。ConcurrentLinkedQueue などには、そのような素晴らしい実装が施されています。皆さんは使うだけで OK.
  • Executor: java.util.concurrent.ExecutorService などのクラス
    • 仕事の依頼を受けて実行をおこなう主体で、単一スレッドもしくはスレッドプールから構成
    • このあと利用法を紹介

仕事の依頼

既に、スレッド間の排他制御については、説明しました。

ただ、

  • Thread A の仕事 J1 の完了を待って、次の仕事を行いたい

などのケースもあるでしょうから、いくつか事例紹介します。必要に応じて見てもらえばいいでしょう。実現したいことに応じて、やり方も様々です。

3つめ以降の例は、SyncSample にまとめてあります。

Thread A が自分で仕事 J2 をこなす

話は、簡単ですね。Thread A が引き続き J2 を処理すればいいんですから。マルチスレッド不要です。

void method()  {
   ....
   J1();
   J2();
   ...
}

新規 Thread に J2 を任せる

Thread A には、別の作業があるような場合、別スレッドで J2 を処理しましょう。

void method() {
    ....
    new Thread() {
        public void run() { J2(); }
    }.start();
    /* で、自分は別の仕事 J1 を行う */    
    J1();
}

Executor に任せる

java.util.concurrent.ExecutorService には、各種 Executor 作成用メソッドがあります。今回は単一スレッド版を使っていますが、複数スレッドを用いるのも簡単です。このようなプログラム記述をしておけば、あとで、executor だけ変更するのも簡単です。

// Executor 作成
ExecutorService executor = Executors.newSingleThreadExecutor();
// 依頼
executor.execute(new Runnable() {
    public void run() {
        J2();
    }
});

lambda を使うともっとすっきりかけます。

executor.execute(()->{ // lambda expression の場合
    J2();
});

実際に、J1(), J2() の中では、Thread.currentThread() を用いて実行スレッド情報をプリントしていますので、main thread と別の「あるスレッド」が仕事を順に処理しているのが分かることでしょう。こんな感じ。

J2@Thread[pool-1-thread-1,5,main] // Executor
J1@Thread[main,5,main]            // Main Thread
J2@Thread[pool-1-thread-1,5,main] // Executor

待ち合わせをおこなう

複数の Thread が、互いの処理を待つというのはアリガチです。例えば、Executor に処理は依頼したが、結果を待ちたいんだとか。

Java は、Thread 間の同期のために、Object クラスが、wait, notify というメソッドを提供しています。

  • Thread B: タイミングが来るまで寝て待つ。
    • obj.wait() メソッドを呼び出すと、メソッドを呼び出したスレッドが、この obj 上で待ち状態に入る。
  • Thread A: 仕事が終わった際に、寝ている人を起こす
    • obj.notify() メソッドを呼び出すと、この obj で待っているThread を起こす。
    • 複数待っている人がいる場合、notify()で一人だけ起こし、notifyAll()呼出しで全員起こす。

プログラムで書くとこんな感じになります。先ほどの仕事の依頼の件で、結果を受け取ってみました。

System.out.println("----- Wait/Notify ------");
final IntBox box = new IntBox(); // 同期用 Object
executor.execute(()->{
    int val = J2(); // 依頼した仕事
    System.out.println("Call notify..");
    synchronized (box) {
        box.val = val;
        box.notifyAll();
    }
}); 
J1(); // 他の仕事
System.out.println("waiting result...");
int val;
while(true) {
    synchronized (box) {
        val = box.val; // 結果を確認
        if(val >= 0) break; // 結果がきてたら待たない (brak while loop)
        box.wait();
    }
}
System.out.println("Val: " + val);

ちょっと面倒ですね。あとでライブラリを使った簡単な書き方を紹介します。

ただ、これは基本形になります。いろんなライブラリもこういった仕組みの上で作られています。 また、応用も利きます。例えば、「結果がある基準に達するまで待つ」なんてのも、待ち条件を変えればすぐに実現できちゃいます。

wait/notify の基本的な注意点:

  • wait(), notify() は、当該オブジェクトの synchronzied を行った状態でないと実行できない。
    • つまり、notify() されたメソッドも、synchronized が開放されるまで、再開出来ない。
  • wait() する場合、 その thread は lock をいったん解放してから眠りにつき、再開する際は lock を保持してから再開になります。
  • wait() 中は、別のスレッドから interupt() かけられることもあるので, InterruptedException の可能性があります。
  • notify() は、待ちThreadが複数いる際を一つだけ起こすメソッドですが、待ちThread全部起こしたい場合は notifyAll()を使いましょう。

共有データアクセスに関する注意点

  • 当然、box.val は、共有データです。アクセスは synchronized の中でおこないましょう。

仕事の結果を待つ

単に仕事の依頼結果を待つのにあんなに面倒なのは嫌ですよね。そんな人は java.util.concurrent.Future を使いましょう。結果を待ち合わせるための箱を作ってくれます。

Future<Integer> future = executor.submit(()-> { // Callable<Integer>#call()
    return J2();
});
J1();
System.out.println("Waiting result...");
int val2 = future.get();
System.out.println("Val: " + val2);

java.util.concurrent.ExecutorService は、 Future<T> submit(Callable<T> task) というメソッドを提供し、Executor の準備したスレッドで task を実行します。 タスクを作る方は、Callable interface にそって、 call() メソッドを実現します(上記では lambda を使って call() の中身だけ書いてますが)。 タスクの実行を依頼したら、別の仕事をしても別のタスクもさらに依頼しても大丈夫(複数タスクを実行する場合は、)。 タスクの結果は、future に格納されます。あとで結果が欲しくなったらget()で取り出しますが、まだだったら結果が来るまで待つ(wait()相当)ことになります。

最後に、executor の作り方について。 同時に大量にタスクを実行しないなら、1スレッドのみ利用するsingle thread executor (Executors.newSingleThreadExecutor();)で十分ですが、大量のリクエストを同時実行したい場合は実行用に複数スレッドを確保した(スレッドプールと呼ぶ)、thread pool executor を使うといいでしょう。各種 executor の生成用メソッドがは java.util.concurrent.Executorsに並んでいます。