- 並行処理:シングルコアのCPUで見られる。実行すべき複数の処理が存在する時、一定の時間(0.1秒とか0.01秒とか)が経過するごとに実行する処理を切り替えることにより実現する(ある瞬間だけを切り取ると実行している処理は当然1つのみ)
- 並列処理:マルチコアのCPUで見られる。実行すべき複数の処理について、それぞれのコアで別々に処理を行うことにより実現する(ある瞬間だけを切り取っても当然複数の処理を実行している)
- Javaにおけるスレッドとは、プログラムを実際に走らせた際、実行するメソッドの情報がメモリのスタック領域に出入りする一連の流れのこと(複数のスタックが並行処理を行うのがマルチスレッド)
- 別のスレッドを生成したい場合はjava.lang.Thread(またはそのサブクラス)のstartメソッドを利用する(startメソッドの処理は重く、新しいスタックが完成するまでにstartメソッドを呼び出している側の処理が進行する場合が多い。ただし必ずしもそうなるわけではなく、新しいスレッドの方が早く処理を進行させる場合もある。これはJVMの実装やCPUのタイミングなどによるため、プログラム自体で制御させることはできない)
- Threadのstartメソッドを使うサンプル(startメソッドを呼び出すとスレッドを新しく作り、そのスレッドでrunメソッドを実行)
プログラム以外の条件により、出力順が以下の2パターンのどちらかになるSystem.out.println("main thread started"); Thread th = new Thread() { @Override public void run() { System.out.println("a new thread started"); } }; th.start(); System.out.println("main thread finished");
main thread started main thread finished a new thread started
main thread started a new thread started main thread finished
- java.lang.Runnableインタフェース(新しいスレッドで実行するrunメソッドのみを持つ)を実現したクラスをThreadのコンストラクタに渡すことによっても新しいスレッドを作ることができる
System.out.println("main thread started"); Thread th = new Thread(() -> System.out.println("th started")); th.start(); Thread thh = new Thread(() -> { System.out.println("thh started"); System.out.println("thh thh thh"); }); thh.start(); System.out.println("main thread finished");
- 新しく生成するスレッドの数が多くなりすぎると、既に処理を終えているスレッドがあるにもかかわらずまた新しくスレッドを生成するなど、かえってパフォーマンスを悪化させる場合がある
- スレッドプール:あらかじめ一定数空のスレッドを用意しておき、処理を終えて次のタスク待ちになっているものを使い回す仕組み
- スレッドプールを実現したうえで新しくスレッドを生成するための、java.util.concurrent.Executorsクラスのメソッドの使い方は以下の通り(いずれもjava.util.concurrent.ExecutorServiceで返される)
- newSingleThreadExecutor:プール内に1スレッドのみ
- newFixedThreadPool:引数で指定した個数のスレッドをプール内に生成
- newCachedThreadPool:必要に応じてプール内のスレッド数が増減する(60秒以上使用されないスレッドは破棄され、60秒未満であれば再利用)
ExecutorService es = Executors.newFixedThreadPool(3); for (int i = 0; i < 10; ++i) { es.submit(() -> System.out.println(Thread.currentThread().getId())); // submitメソッドに与えたRunnableインタフェースの実装に従いプール内のスレッドがタスク実行 }
- java.util.concurrent.ScheduledExecutorServiceは、ExecutorServiceを拡張したインタフェースで、submitメソッドではなくscheduleメソッド(引数は3つ:処理内容Runnable, 遅延させる時間long, 時間の単位java.util.concurrent.TimeUnit)を使うことにより引数で指定した処理を指定した時間だけ遅延させて実行可能
- ScheduledExecutorServiceのメソッドscheduleAtFixedRate(引数4つ:処理内容, 最初の処理までの遅延時間, 処理同士の時間間隔, 時間の単位)を使うことにより、指定した処理を繰り返し実行可能(処理が始まってから終わるまでの時間が、指定した間隔よりも長くなる場合、処理が終わってから次回の処理が始まる。それに対し、処理時間が指定した間隔よりも短くなる場合、指定した時間間隔が経過するまで次の処理は行われない)
- ScheduledExecutorServiceのメソッドscheduleWithFixedDelay(引数のパターンはscheduleAtFixedRateと同じ)を使うことにより、指定した処理が完了した瞬間から一定時間だけ間隔をあけて繰り返し同じ処理を行うことが可能
- ExecutorsクラスのメソッドnewScheduledThreadPool(1つの引数:スレッド数)により、複数のScheduledExecutorServiceをスレッドプールとして扱うことが可能(以下使用例参照)
public static final void main(String[] args) throws InterruptedException { ScheduledExecutorService ses = Executors.newScheduledThreadPool(2); ses.scheduleWithFixedDelay(() -> System.out.print("A"), 0, 1, TimeUnit.SECONDS); ses.scheduleWithFixedDelay(() -> System.out.print("B"), 1, 1, TimeUnit.SECONDS); Thread.sleep(10000L); ses.shutdown(); // ABABABABABABABABABAと出力される }
- java.util.concurrent.Futureインタフェースを使うことにより(ここでのfutureは「見込み」程度の意味)、スレッドを生成した側のメソッドが、新しく作ったスレッドの実行結果を知ることができる(以下使用例参照)
public static final void main(String[] args) throws Exception { ExecutorService es = Executors.newSingleThreadExecutor(); // Runnableは値を受け取らず、返しもしない Future f = es.submit(() -> System.out.println("thread f")); if (f.get() == null) { System.out.println("thread f successfully finished"); } // submitの第2引数でスレッド完了時の値を指定可能 Future<Integer> g = es.submit(() -> System.out.println("thread g"), 0); if (g.get() == 0) { System.out.println("thread f successfully finished"); } es.shutdown(); }
- Runnableとjava.util.concurrent.Callableは構造がよく似た関数型インタフェースなので混同しないようにする:Runnableのメソッドrunは引数・返り値ともにないのに対し、Callableのメソッドcallは引数なし・返り値(型はCallableに与えたジェネリクスと同じ)あり・throws Exceptionとなる(ExecutorService#submitにCallableを渡して返り値および例外に関して制御する以下の使用例を参照)
public static final void main(String[] args) throws InterruptedException { int[] arr = { 3, 1, 4, 1, 5, 9, 2, 6, 5, 3 }; ExecutorService es = Executors.newSingleThreadExecutor(); List<Future<Boolean>> futures = new ArrayList<Future<Boolean>>(); for (int i = 0; i < 10; ++i) { final int idx = i; // submitにCallableを渡すことにより、例外の場合も含めてFutureに保存可能 futures.add(es.submit(() -> { if (arr[idx] > 5) { throw new Exception(arr[idx] + " is greater than 5"); } return true; })); } int c = 0; for (Future<Boolean> f : futures) { try { if (f.get()) { ++c; } System.out.println("valid"); } catch (ExecutionException e) { // 生成したスレッドで発生した例外はExecutionExceptionとしてcatch System.out.println(e.getCause()); } } es.shutdown(); System.out.println(c); }
- 同期化:複数のスレッドが並行して進行しているときに処理の順序を制御すること。
- java.util.concurrent.CyclicBarrierクラスにより同期化を実現可能:特定の待機ポイント(バリア)に全スレッドが到達するとバリアアクションを行い順序を制御する
new CyclicBarrier(int numOfThreads, Runnable barrierAction)
- 競合:複数のスレッド間で共有されているインスタンスのフィールド値を、あるスレッドが読み出してから変更するまでの間に、別のスレッドが変更すること
- 排他制御:複数のスレッド間で共有されるインスタンスに対し、あるスレッドが処理を実行している間は別のスレッドが処理を加えられないようにすること
// someメソッドが複数のスレッドから同時には呼び出されなくなる public synchronized void some() { System.out.println("Java"); }
- デッドロック:複数の共有インスタンスに対して排他制御を行った結果、それぞれのインスタンスが連携することで発生(例えば2インスタンスa/bが2スレッドA/B間で共有されている場合、Aがa、bの順に、Bがb、aの順にロックしようとすると、A/Bが互いに相手のリソース開放を永久に待ち続けることになり発生する。回避策:スレッドどうしが同じ順序でリソースをロックするように変更する)
public static final void main(String[] args) { Deque<Integer> a = new ArrayDeque<Integer>(); a.addLast(7); Deque<Integer> b = new ArrayDeque<Integer>(); b.addLast(11); ExecutorService es = Executors.newFixedThreadPool(2); es.submit(() -> { synchronized (a) { System.out.println("a poll"); int aFirst = a.pollFirst(); synchronized (b) { System.out.println("b add"); b.addLast(aFirst); } } }); es.submit(() -> { synchronized (b) { System.out.println("b poll"); int bFirst = b.pollFirst(); synchronized (a) { System.out.println("a add"); a.addLast(bFirst); } } }); }
- ライブロック:2つのスレッドが互いにデッドロックを回避しようとした結果としてロック状態に陥ること
- 原子性:一連の処理が完全に終わる、または全く実行されないのどちらかにしかなりえない性質(java.util.concurrent.atomicパッケージのクラスが持つxxxAndGetメソッドにより、読み出しから値の変更までの一連の処理の間に別スレッドが割り込んで処理を行わないようにできる)
AtomicInteger n = new AtomicInteger(3); ExecutorService es = Executors.newFixedThreadPool(2); es.submit(() -> { System.out.println("あ"); n.addAndGet(100); }); es.submit(() -> { System.out.println("い"); n.addAndGet(100); }); Thread.sleep(2000); // 必ず203を表示 System.out.println(n);