プログラムにおいて、独立して実行する一連の処理をスレッドという。
その中で、JavaScriptが代表的だが、並列して処理を実行せず、一連の流れで処理を行うプログラムをシングルスレッドという。
それに対し、複数のスレッドで同時に処理を行う仕組みをマルチスレッドという。マルチスレッドで別々の処理を平行して実行することを非同期処理という。
本記事では基本的なマルチスレッドの実装についてまとめる。
スレッドの作成と処理実行
新しくスレッドを作成するには、Threadクラスのインスタンスを生成し、startメソッドで実行するのが基本的である。
Thread th = new Thread(実行したい処理);
th.start();
以下のでは2つのスレッドを作成して処理を実行する。
Thread th1 = new Thread(() -> System.out.println("スレッド1の処理が実行されました"));
Thread th2 = new Thread(() -> System.out.println("スレッド2の処理が実行されました"));
Thread th3 = new Thread(() -> System.out.println("スレッド3の処理が実行されました"));
Thread th4 = new Thread(() -> System.out.println("スレッド4の処理が実行されました"));
// マルチスレッド実行
th1.start();
th2.start();
th3.start();
th4.start();
System.out.println("mainメソッド実行");
mainメソッドも合わせて計5つのスレッドが並列で処理を行っている。
出力結果①
mainメソッド実行
スレッド4の処理が実行されました
スレッド3の処理が実行されました
スレッド2の処理が実行されました
スレッド1の処理が実行されました
出力結果②
スレッド1の処理が実行されました
mainメソッド実行
スレッド4の処理が実行されました
スレッド2の処理が実行されました
スレッド3の処理が実行されました
出力結果③
スレッド1の処理が実行されました
スレッド3の処理が実行されました
mainメソッド実行
スレッド2の処理が実行されました
スレッド4の処理が実行されました
出力結果④
mainメソッド実行
スレッド4の処理が実行されました
スレッド3の処理が実行されました
スレッド1の処理が実行されました
スレッド2の処理が実行されました
全4回実行したときの出力結果が上記になる。
見ての通り、実行するたびに処理順番が違うため、実行順序はコントロールできないことがここからわかる。
コントロールを行いたい場合は処理を一定時停止させることで制御できる。
以下の処理ではスレッド1は実行され2秒待機後に後続処理を行い、スレッド2は実行され5秒後に後続処理を行う。
public class Sample {
public static void main(String[] args) {
Thread th1 = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(2);
} catch(InterruptedException e) {
throw new IllegalStateException(e);
}
System.out.println("スレッド1の処理が実行されました");
});
Thread th2 = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(5);
} catch(InterruptedException e) {
throw new IllegalStateException(e);
}
System.out.println("スレッド2の処理が実行されました");
});
th1.start();
th2.start();
System.out.println("mainメソッド実行");
}
}
出力結果
mainメソッド実行
スレッド1の処理が実行されました
スレッド2の処理が実行されました
TimeUnitは時間単位を表すenumでSECONDは秒単位を表す値である。
sleepメソッドをは引数に指定した時間だけスレッドの処理を停止させることができる。
スレッドセーフ
スレッドセーフとはマルチスレッドでも動かしても問題がないことを表す言葉である。
よくある問題としては、特定のオブジェクトやstaticな変数をいくつかのスレッドで共有して使用することであげられる。
以下のクラスを使用してマルチスレッドで処理を行うとする。
public class SampleObject {
private String field1;
private String field2;
public String getField1() {
return field1;
}
public void setField1(String field1) {
this.field1 = field1;
}
public String getField2() {
return field2;
}
public void setField2(String field2) {
this.field2 = field2;
}
public SampleObject(String field1, String field2) {
super();
this.field1 = field1;
this.field2 = field2;
}
}
import java.util.concurrent.TimeUnit;
public class Sample {
public static void main(String[] args) {
SampleObject so = new SampleObject();
Thread th1 = new Thread(() -> {
so.setField1("aaaaa");
so.setField2("bbbbb");
System.out.println("スレッド1でSampleObjectにセットした値はfield1=" + so.getField1() + ", " + "field2=" + so.getField2());
});
Thread th2 = new Thread(() -> {
so.setField1("ccccc");
so.setField2("ddddd");
System.out.println("スレッド2でSampleObjectにセットした値はfield1=" + so.getField1() + ", " + "field2=" + so.getField2());
});
th1.start();
th2.start();
System.out.println("mainメソッド実行");
}
}
出力結果
mainメソッド実行
スレッド1でSampleObjectにセットした値はfield1=ccccc, field2=ddddd
スレッド2でSampleObjectにセットした値はfield1=ccccc, field2=ddddd
スレッド1では"aaaaa"と"bbbbb"をフィールドにセットしていたはずなのにスレッド1でも"ccccc"と"ddddd"が出力されている。
これは2つのスレッドが1つのSampleObjectインスタンスを共有して処理の中で使用しているため発生する。
マルチスレッドで処理を行う場合はこのようなことを考慮しなければならない。
対策としてはローカル変数を使用するなどがある。
synchronized
複数のスレッドで共有される変数があるときにその値を変更するメソッドにsynchronizedを付けると同時に1つのスレッドだけしかアクセスできなくなる。
これでもスレッドセーフにできますが、処理速度は遅くなる。
public class SampleObject {
private String field1;
synchronized void setField1(String field1) {
this.field1 = field1;
}
}
スレッドプール
あらかじめいくつかのスレッドを起動しておき、必要に応じてそれらを使いまわす方法をスレッドプールという。
スレッドプールではプール内のスレッドを1つ使用して処理を実行するが、実行が終了すると、スレッドをプールに戻して再利用できる。
Threadクラスを使用するのは処理を実行するたびにインスタンスを作成するので効率が良くないのでこのような方法がとられるようになったそう。
名称(作成方法) | 特徴 |
---|---|
Fixed Thread Pool(Executors.newFixedThreadPool(n)) | プール内に指定した数のスレッドを入れておく |
Catched Thread Pool(Executors.newCachedThreadPool()) | プール内のスレッドが必要に応じて増減する |
Single Thread Pool(Executors.newSingleThreadPool(n)) | プール内に1つだけスレッドがあるタイプ |
workStealing Pool(Executors.newWorkStealingPool() or (n)) | CPUのコア数の最大値または指定された数のスレッドを入れておき、各スレッドにタスクのキューを割り当てる、キューに空きができるとほかのスレッドのタスクを横取りして処理する |
Executorsクラスを使用してスレッドプールを作成することができる。
また、ExecutorServiceを実装しているので、ExecutorService型の変数に代入して使用する。
以下はFixed Thread Poolを使用した例である。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class Sample {
public static void main(String[] args) {
ExecutorService exs = Executors.newFixedThreadPool(5);
// スレッド実行
exs.execute(() -> System.out.println("スレッド1の処理実行"));
exs.execute(() -> System.out.println("スレッド2の処理実行"));
exs.execute(() -> System.out.println("スレッド3の処理実行"));
// スレッドプールを終了する
exs.shutdown();
}
}
出力結果
スレッド1の処理実行
スレッド2の処理実行
スレッド3の処理実行
executeメソッドを使用してスレッドを起動する。
また、スレッドプールは作成するとシャットダウンするまで動き続ける。
不要になったときはshutDownメソッドを使用して停止させなければならない。