Javaでのプロセス・スレッドについて勉強したまとめ

こんにちは。鎌田です。
これは TECHSCORE Advent Calendar 2018の13日目の記事です。

Java でのプロセスやスレッドの実装方法ついてあまり理解できている気がせず、あらためて勉強する意味で触ったことを記事にしました。本記事で触れているスレッドやマルチスレッドについては、TECHSCORE本家サイトでも詳しく説明がされているのでぜひご参照ください!

今回の記事で使用したバージョンは以下の通りです。
Java:1.8.0

目次

プロセス

プロセスを生成する場合はjava.lang.ProcessBuilderを使います。
start() を呼び出すことでプロセスを起動します。標準出力があるコマンドはサンプルコードのように、java.io.InputStreamを使うことで結果を取得できます。

コンパイルして実行するとちゃんと「ps」コマンドの結果が表示されました!

Linux 上ではどのように見えるのでしょうか。コマンドを「ps」から「sleep 100」に変えて、プロセスの状態をみます。プログラムを実行中にプロセスを確認すると、子プロセスとしてコマンドが実行されていることが分かります。

スレッド

スレッドを使うことで非同期な処理をさせることができます。スレッドを生成するには java.lang.Threadを継承したクラスを作成して start() を呼び出すことで実現できます。

実行結果をみると数字とアルファベットが交互に出力されています。スレッドに分けずに実行すると1-25までの数字のあとにa-zまでのアルファベットが出力されるので、非同期に処理がされてそうです。

サンプルコードを実行してjstackで確認するとメインスレッドとは別にスレッドが生成されて処理していることが分かります。
jstackはjavaプロセスのスレッドの状態を確認できるコマンドで、スレッド名(Thread-0),優先度(prio),スレッドの状態(java.lang.Thread.State),jvm上のスレッドID(tid),OS上のスレッドID(nid)や、どのメソッドを処理中かなどを表示してくれるので調査などで非常に有用です。

排他制御(synchronized)

排他制御のやり方はいくつかありますが、今回はsynchronizedを使います。synchronizedを使うことで1つのインスタンスに対して、複数のスレッドが同時に処理を行わないように制御できます。synchronizedするオブジェクトには注意が必要です。

結果の比較を用意したので分かりやすいと思います。スレッドのときの実行結果とは違い、順番どおりに数字が出力されており lock オブジェクトを持っているスレッドのみが処理をしており、持っていないスレッドは待ち状態です。

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に説明が記載されています)

実行数制御

スレッドの同時実行数の制御です。java.util.concurrent.Semaphoreを使います。Semaphoreも排他制御の1つです。共有資源に対して同時にアクセスできる数を示しています。今回のサンプルコードでは例となるような共有資源は用意せず、Semaphoreの機能だけを試しています。
Semaphoreを5に設定して、スレッドが同時に5つまで処理できるようなプログラムです。28行目でSemaphoreを設定してスレッドを生成するときにSemaphoreを渡しています。Semaphoreは15行目のacquire() によって獲得され1減ります。Semaphoreが0になるとSemaphoreを獲得できないスレッドは処理が始まりません。
22行目のrelease()が呼び出された時点で、Semaphoreが解放され1増えます。

実行するとちゃんと5スレッド処理したところでSemaphoreが0になり、新しいスレッドが処理されなくなりました。
1秒後にSemaphoreが解放されたことでSemaphoreの獲得を待っていたスレッドが処理を開始しました。

jstackで確認すると実行待ちをしているスレッドの状態がWAITINGになっています。WAITINGということは他のスレッドのアクションを待っています。「- parking to wait for <0x00000000ec4638f8> (a java.util.concurrent.Semaphore$NonfairSync)」の表示とサンプルコードの acquire() の処理で止まっていることからSemaphoreの解放を待っているのだろうということが分かります。

ちなみにどのスレッドがSemaphoreを獲得しているかスレッドダンプからは分からないそうです。理由はSemaphoreには所有という概念がないためです。そのためSemaphoreを獲得したスレッドではなくてもSemaphoreを解放できるようです。
* 参考:[java]new Semaphore(0)

スレッドプール

スレッドプールとはあらかじめいくつかのスレッドを作成して待機させ、タスクが来たらすぐにスレッドを割り当てることで、スレッドの生成のコストを減らそうというものです。
Javaのスレッドプールもいくつか種類があります。今回は指定した数のスレッドプールを生成する newFixedThreadPoolを使います。他にはnewCachedThreadPool,newWorkStealingPoolがあります。
サンプルコードは100回スレッドを実行するプログラムで、処理しているスレッドIDを表示するプログラムです。

スレッドプールを使わないプログラムも比較として実行しました。
スレッドプールありではスレッドIDが5種類しか表示されないのでスレッドを使いまわしているようです。
スレッドプールなしでは連番でスレッドIDが表示されており、毎回新しいスレッドを生成しています。

もちろん今回も jstack で確認しました。想定通りスレッドが5つ以上に増えることはありませんでした。スレッド名の「pool-1-thread-...」はExecutorServiceを使った際のデフォルトのスレッド名なので、スレッドプールが使われていることがわかります。

おわりに

Java でのプロセス/スレッド周りを触ってみました。プログラムの実行結果だけでなく、「ps」や「jstack」といったコマンドやツールを使うことでより内部の動きをイメージしやすくなりました!
jstack は本番環境での調査などで使うことがあるので、慣れておこうと思い一緒に勉強したのでついでに載せました!スレッドについては効率よく並列処理するためのテクニックや詰まりポイント、便利なフレームワークがたくさんあるようなので、引き続き勉強していきたいと思います。

Comments are closed, but you can leave a trackback: Trackback URL.