こんにちは。鎌田です。
これは TECHSCORE Advent Calendar 2018の13日目の記事です。
Java でのプロセスやスレッドの実装方法ついてあまり理解できている気がせず、あらためて勉強する意味で触ったことを記事にしました。本記事で触れているスレッドやマルチスレッドについては、TECHSCORE本家サイトでも詳しく説明がされているのでぜひご参照ください!
今回の記事で使用したバージョンは以下の通りです。
Java:1.8.0
目次
プロセス
プロセスを生成する場合はjava.lang.ProcessBuilderを使います。
start() を呼び出すことでプロセスを起動します。標準出力があるコマンドはサンプルコードのように、java.io.InputStreamを使うことで結果を取得できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import java.io.IOException; import java.io.InputStream; public class ProcessTest { public static void main(String args[]) { try { ProcessBuilder builder = new ProcessBuilder("ps"); Process process = builder.start(); InputStream is = process.getInputStream(); while (true) { int c = is.read(); if (c == -1) { is.close(); break; } System.out.print((char) c); } } catch (IOException e) { e.printStackTrace(); } } } |
コンパイルして実行するとちゃんと「ps」コマンドの結果が表示されました!
1 2 3 4 5 |
$ javac ProcessTest.java $ java ProcessTest PID TTY TIME CMD 13223 pts/4 00:00:00 java 13238 pts/4 00:00:00 ps |
Linux 上ではどのように見えるのでしょうか。コマンドを「ps」から「sleep 100」に変えて、プロセスの状態をみます。プログラムを実行中にプロセスを確認すると、子プロセスとしてコマンドが実行されていることが分かります。
1 2 3 4 5 6 |
$ java ProcessTest & [1] 13346 $ ps f --ppid $! --pid $! PID TTY STAT TIME COMMAND 13346 pts/4 Sl+ 0:00 java ProcessTest 13361 pts/4 S+ 0:00 \_ sleep 100 |
スレッド
スレッドを使うことで非同期な処理をさせることができます。スレッドを生成するには java.lang.Threadを継承したクラスを作成して start() を呼び出すことで実現できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
public class ThreadTest { static class SampleThread1 extends Thread { @Override public void run() { for (int i = 0; i < 26; i++) { System.out.print(i); try { sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } } public static void main(String args[]) { Thread thread1 = new SampleThread1(); thread1.start(); String[] Alphabet = { "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z" }; for (int i = 0; i < 26; i++) { System.out.print(Alphabet[i]); try { Thread.sleep(1500); } catch (InterruptedException e) { e.printStackTrace(); } } } } |
実行結果をみると数字とアルファベットが交互に出力されています。スレッドに分けずに実行すると1-25までの数字のあとにa-zまでのアルファベットが出力されるので、非同期に処理がされてそうです。
1 2 |
$ java ThreadTest a0b1c23de4f5g6h7i89j10kl1112m13no1415p16qr17s18t19u20v21w22x2324y25z |
サンプルコードを実行してjstackで確認するとメインスレッドとは別にスレッドが生成されて処理していることが分かります。
jstackはjavaプロセスのスレッドの状態を確認できるコマンドで、スレッド名(Thread-0),優先度(prio),スレッドの状態(java.lang.Thread.State),jvm上のスレッドID(tid),OS上のスレッドID(nid)や、どのメソッドを処理中かなどを表示してくれるので調査などで非常に有用です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
$ java ThreadTest & [1] 14587 $ jstack 14587 ... # 生成されたサブスレッド "Thread-0" #9 prio=5 os_prio=0 tid=0x00007f7a340ea330 nid=0x3aad waiting on condition [0x00007f7a215e6000] java.lang.Thread.State: TIMED_WAITING (sleeping) at java.lang.Thread.sleep(Native Method) at ThreadTest$SampleThread1.run(ThreadTest.java:9) ... # メインスレッド "main" #1 prio=5 os_prio=0 tid=0x00007f7a34009340 nid=0x3a9d waiting on condition [0x00007f7a3c387000] java.lang.Thread.State: TIMED_WAITING (sleeping) at java.lang.Thread.sleep(Native Method) at ThreadTest.main(ThreadTest.java:26) ... |
排他制御(synchronized)
排他制御のやり方はいくつかありますが、今回はsynchronizedを使います。synchronizedを使うことで1つのインスタンスに対して、複数のスレッドが同時に処理を行わないように制御できます。synchronizedするオブジェクトには注意が必要です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
public class ThreadTest { private static final Object lock = new Object(); // synchronizedのためのオブジェクト static class SampleThread1 extends Thread { @Override public void run() { synchronized (lock) { // lock オブジェクトに対して同期を取る for (int i = 0; i < 10; i++) { System.out.print(i + " "); try { sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } } } public static void main(String args[]) { Thread thread1 = new SampleThread1(); Thread thread2 = new SampleThread1(); thread1.start(); thread2.start(); } } |
結果の比較を用意したので分かりやすいと思います。スレッドのときの実行結果とは違い、順番どおりに数字が出力されており lock オブジェクトを持っているスレッドのみが処理をしており、持っていないスレッドは待ち状態です。
1 2 3 4 |
$ java ThreadTest #排他制御している 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 $ java ThreadTest #排他制御していない 0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9 |
jstack も取りました。Thread-0 は sleep() を実行中で11行目に「- locked <0x00000000ec4622f8> (a java.lang.Object)」と表示されています。これはソースコード中の lock オブジェクトのロックを取得していることを示しています。
Thread-1 は状態がBLOCKEDになっており処理をブロックされているようです。5行目で「 - waiting to lock <0x00000000ec4622f8> (a java.lang.Object)」と表示されているので Thread-0 がロックをしている lock オブジェクトのロック待ちをしていることを示しています。
(スレッドの状態はJavaDocに説明が記載されています)
1 2 3 4 5 6 7 8 9 10 11 12 13 |
... #ロック待ちしているスレッド "Thread-1" #11 prio=5 os_prio=0 tid=0x00007f55f40fb590 nid=0x4404 waiting for monitor entry [0x00007f55cfaf9000] java.lang.Thread.State: BLOCKED (on object monitor) at ThreadTest$SampleThread1.run(ThreadTest.java:8) - waiting to lock <0x00000000ec4622f8> (a java.lang.Object) #先にオブジェクトのロックを取得したスレッド "Thread-0" #10 prio=5 os_prio=0 tid=0x00007f55f40fa520 nid=0x4403 waiting on condition [0x00007f55cfbfa000] java.lang.Thread.State: TIMED_WAITING (sleeping) at java.lang.Thread.sleep(Native Method) at ThreadTest$SampleThread1.run(ThreadTest.java:11) - locked <0x00000000ec4622f8> (a java.lang.Object) ... |
実行数制御
スレッドの同時実行数の制御です。java.util.concurrent.Semaphoreを使います。Semaphoreも排他制御の1つです。共有資源に対して同時にアクセスできる数を示しています。今回のサンプルコードでは例となるような共有資源は用意せず、Semaphoreの機能だけを試しています。
Semaphoreを5に設定して、スレッドが同時に5つまで処理できるようなプログラムです。28行目でSemaphoreを設定してスレッドを生成するときにSemaphoreを渡しています。Semaphoreは15行目のacquire() によって獲得され1減ります。Semaphoreが0になるとSemaphoreを獲得できないスレッドは処理が始まりません。
22行目のrelease()が呼び出された時点で、Semaphoreが解放され1増えます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
import java.time.LocalTime; import java.util.concurrent.Semaphore; public class SemaphoreTest { static class SampleThread1 extends Thread { private Semaphore semaphore; public SampleThread1(Semaphore semaphore) { this.semaphore = semaphore; } @Override public void run() { try { this.semaphore.acquire(); System.out.println("Thread:" + Thread.currentThread().getId()); sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } finally { System.out.println(LocalTime.now()); this.semaphore.release(); } } } public static void main(String args[]) { Semaphore semaphore = new Semaphore(5); //Semaphoreを5に設定して渡す for (int i = 0; i <= 10; i++) { Thread thread = new SampleThread1(semaphore); thread.start(); } } } |
実行するとちゃんと5スレッド処理したところでSemaphoreが0になり、新しいスレッドが処理されなくなりました。
1秒後にSemaphoreが解放されたことでSemaphoreの獲得を待っていたスレッドが処理を開始しました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
Thread:10 Thread:11 Thread:12 Thread:14 Thread:13 // 5スレッド分の処理がされSemaphore が0 になる 08:29:02.034 08:29:02.034 08:29:02.034 08:29:02.034 08:29:02.034 Thread:17 // Semaphoreが解放されるまで実行されない Thread:19 Thread:18 Thread:16 Thread:15 08:29:03.035 // 1秒後にSemaphoreが解放され、次のスレッドの処理が実行される。 08:29:03.035 Thread:20 08:29:03.035 ... |
jstackで確認すると実行待ちをしているスレッドの状態がWAITINGになっています。WAITINGということは他のスレッドのアクションを待っています。「- parking to wait for <0x00000000ec4638f8> (a java.util.concurrent.Semaphore$NonfairSync)」の表示とサンプルコードの acquire() の処理で止まっていることからSemaphoreの解放を待っているのだろうということが分かります。
1 2 3 4 5 6 7 8 9 10 11 12 |
... "Thread-10" #20 prio=5 os_prio=0 tid=0x00007efeb00ffb50 nid=0x1198 waiting on condition [0x00007efe8a8e7000] java.lang.Thread.State: WAITING (parking) at sun.misc.Unsafe.park(Native Method) - parking to wait for <0x00000000ec4638f8> (a java.util.concurrent.Semaphore$NonfairSync) at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175) at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:836) at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireSharedInterruptibly(AbstractQueuedSynchronizer.java:997) at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireSharedInterruptibly(AbstractQueuedSynchronizer.java:1304) at java.util.concurrent.Semaphore.acquire(Semaphore.java:312) at SemaphoreTest$SampleThread1.run(SemaphoreTest.java:15) ... |
ちなみにどのスレッドがSemaphoreを獲得しているかスレッドダンプからは分からないそうです。理由はSemaphoreには所有という概念がないためです。そのためSemaphoreを獲得したスレッドではなくてもSemaphoreを解放できるようです。
* 参考:[java]new Semaphore(0)
スレッドプール
スレッドプールとはあらかじめいくつかのスレッドを作成して待機させ、タスクが来たらすぐにスレッドを割り当てることで、スレッドの生成のコストを減らそうというものです。
Javaのスレッドプールもいくつか種類があります。今回は指定した数のスレッドプールを生成する newFixedThreadPoolを使います。他にはnewCachedThreadPool,newWorkStealingPoolがあります。
サンプルコードは100回スレッドを実行するプログラムで、処理しているスレッドIDを表示するプログラムです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ThreadPoolTest { static class SampleThread1 extends Thread { @Override public void run() { System.out.println("Thread:" + Thread.currentThread().getId()); try { sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String args[]) { ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5); // スレッドプールを生成 for (int i = 0; i <= 100; i++) // 100個のスレッドを実行 fixedThreadPool.execute(new SampleThread1()); fixedThreadPool.shutdown(); } } |
スレッドプールを使わないプログラムも比較として実行しました。
スレッドプールありではスレッドIDが5種類しか表示されないのでスレッドを使いまわしているようです。
スレッドプールなしでは連番でスレッドIDが表示されており、毎回新しいスレッドを生成しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
$ java ThreadPoolTest # スレッドプールあり Thread:11 Thread:19 Thread:17 Thread:13 Thread:15 Thread:11 Thread:19 Thread:17 Thread:13 Thread:15 Thread:11 Thread:17 ... Thread:19 $ java ThreadPoolTest # スレッドプールなし Thread:11 Thread:12 Thread:10 Thread:13 Thread:14 Thread:15 Thread:16 Thread:17 Thread:18 Thread:19 Thread:20 Thread:21 ... Thread:110 |
もちろん今回も jstack で確認しました。想定通りスレッドが5つ以上に増えることはありませんでした。スレッド名の「pool-1-thread-...」はExecutorServiceを使った際のデフォルトのスレッド名なので、スレッドプールが使われていることがわかります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
... "pool-1-thread-5" #19 prio=5 os_prio=0 tid=0x00007ffabc0ffa10 nid=0x53cd waiting on condition [0x00007ffa7f4f3000] java.lang.Thread.State: TIMED_WAITING (sleeping) at java.lang.Thread.sleep(Native Method) at ThreadPoolTest$SampleThread1.run(ThreadPoolTest.java:10) ... "pool-1-thread-4" #17 prio=5 os_prio=0 tid=0x00007ffabc0fe5d0 nid=0x53cc waiting on condition [0x00007ffa7f5f4000] java.lang.Thread.State: TIMED_WAITING (sleeping) at java.lang.Thread.sleep(Native Method) at ThreadPoolTest$SampleThread1.run(ThreadPoolTest.java:10) ... "pool-1-thread-3" #15 prio=5 os_prio=0 tid=0x00007ffabc0fd110 nid=0x53cb waiting on condition [0x00007ffa7f6f5000] java.lang.Thread.State: TIMED_WAITING (sleeping) at java.lang.Thread.sleep(Native Method) at ThreadPoolTest$SampleThread1.run(ThreadPoolTest.java:10) ... "pool-1-thread-2" #13 prio=5 os_prio=0 tid=0x00007ffabc0fbd10 nid=0x53ca waiting on condition [0x00007ffa7f7f6000] java.lang.Thread.State: TIMED_WAITING (sleeping) at java.lang.Thread.sleep(Native Method) at ThreadPoolTest$SampleThread1.run(ThreadPoolTest.java:10) ... "pool-1-thread-1" #11 prio=5 os_prio=0 tid=0x00007ffabc0fab80 nid=0x53c9 waiting on condition [0x00007ffa7f8f7000] java.lang.Thread.State: TIMED_WAITING (sleeping) at java.lang.Thread.sleep(Native Method) at ThreadPoolTest$SampleThread1.run(ThreadPoolTest.java:10) ... |
おわりに
Java でのプロセス/スレッド周りを触ってみました。プログラムの実行結果だけでなく、「ps」や「jstack」といったコマンドやツールを使うことでより内部の動きをイメージしやすくなりました!
jstack は本番環境での調査などで使うことがあるので、慣れておこうと思い一緒に勉強したのでついでに載せました!スレッドについては効率よく並列処理するためのテクニックや詰まりポイント、便利なフレームワークがたくさんあるようなので、引き続き勉強していきたいと思います。