##はじめに
皆さんは、マルチスレッドについてご存知でしょうか。
正しくマルチスレッドを使うことによって、システムのパフォーマンスを向上させることが出来ます。今回は初心者に向けてマルチスレッドにについて概念から使い方まで解説していきます。
##マルチスレッドはなぜ必要なのか?
プログラムの流れは、プログラムの処理が終わるまで次の処理に進めないという特徴を持っています。この特徴の問題点は処理が終わるまで待つ時間ができてしまうことです。
このような問題を解決するのがマルチスレッド並列処理です。マルチスレッド並列処理を解説する前に、並列処理と並行処理について解説していきます。
##並行処理と並列処理
並列処理を一言で言えば、マルチコアで同時に複数の処理を実行することです。詳しく説明すると、コンピュータに搭載しているCPUには複数のコアを持っています。コアの数だけ、同時に複数の処理を進めることができるので、待ち時間を解消することが出来ます。飲食店を例にすると、お店の人が1人だった場合は料理、配膳、食器洗いと時間ごとに仕事をします。このことを並行処理だとイメージしてください。それが複数いると、料理をする人、配膳する人、皿洗いする人に分けることができるので同時進行で仕事を進めることができます。このことを並列処理だとイメージしてください。
###①Runnableインターフェース
Runnableインターインターフェースは新しいスレッドで実行したいrunメソッドのみ持っています。Threadのstartメソッドでstartメソッドを呼び出したスレッドで動作します。一方runメソッドはstartメソッドによって作られた新しいスレッドで動作します。
public class Sample {
public static void main (String[] args) {
//Runnableを実現した匿名クラスをThreadコンストラクタに渡してスレッドを作成
Thread t = new Thread (new Runnable() {
public void run() {
System.out.println("sub");
}
});
t.start();
System.out.println("main");
}
}
main
sub
###②Threadクラスのサブクラスを実装
新しいスレッドで実行したいことをThreadクラスのrunメソッドで実装します。
public class SampleThread extends Thread{
public void run() {
System.out.println("sub");
}
}
新しいスタックを生成してスレッドを開始するにはThreadクラスのインスタンスを生成し、startメソッドを実行します。
public class Sample {
public static void main (String[] args) {
Thread t = new SampleThread();
t.start();
System.out.println("main");
}
}
main
sub
##問題点
先程の2つの方法だと実行したい処理の数だけ、新しいスレッドを生成する必要性があります。例えば、実行したい処理が50あれば、50個のスレッドを作成する必要性があるということです。この時に__空きの状態になったスレットがあるにも関わらず、新しくスレットを作らなけらばなりません__。これではコンピュータに負荷をかけてしまいます。スレッドの無駄遣いを解消するのがスレットプールです。
##スレッドプールとは
スレッドプールを一言で言えば、スレッドを無駄遣いしない仕組みのことです。
具体的には、空のスレッドとタスク(処理)を実行するスレッドを分けることで、スレッドを効率的に作成できます。飲食店を例にすると注文が来てから、料理をすれば無駄に料理をすることはなくなりますよね。
詳しい説明はこちらのサイトがおすすめ
#スレッドプールの実装方法
実装方法としては2通りあります。
・1つ目はExecutorsのサブインターフェースであるExecutorServiceを利用
・2つ目はExecutorsのサブインターフェースであるScheduledExecutorServiceを利用
この2通りについてサンプルコードと合わせて、解説していきます。
##①ExecutorService
#####1つだけ新しいスレッドを使う場合
Executeorsクラスの__newSingleThreadExecutorメソッド__を使用して、ExecutorServiceを作成します。submitメソッドはスレッドにタスクを与えて、実行します。
下記のコードは、スレッドIDを表示させています。
mport java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Sample {
public static void main (String[] args) {
//タスクを持つ新しいスレッドを作成
ExecutorService exec = Executors.newSingleThreadExecutor();
//ExecutorServiceにタスクを与えて、実行する
exec.submit(()->{
System.out.println(Thread.currentThread().getId());
});
}
}
12
#####決まった数のスレッドを生成し、スレッドプールに貯めておきたい場合
Executorsクラスの__newFixedThreadPool__メソッドを使用します。
生成したいスレッド数を引数で受け取りタスク持ちのスレッドをを保持するスレッドプールを作成します。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Sample {
public static void main (String[] args) {
//3つタスクを持つ新しいスレッドを作成
ExecutorService exec = Executors.newFixedThreadPool(3);
//タスクを与えて、実行する
for(int i =0 ; i<3;i++) {
exec.submit(()->{
System.out.println(Thread.currentThread().getId());
});
}
}
}
12
13
14
#####必要に応じてスレッドを増減させる場合
newCatchedThreadPoolメソッドを使用します。
下記のコードはsubmitメソッドでスレッドを5つ作成し、sleepメソッドで60秒間mainメソッドを停止させています。1度生成されたスレッドは60秒間使用されないと破棄されます。また、submitクラスで新しいスレッドを作成しています。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Sample {
public static void main (String[] args) throws InterruptedException {
//タスクを持つ新しいスレッドを作成
ExecutorService exec = Executors.newCachedThreadPool();
Runnable test = () ->{
System.out.println(Thread.currentThread().getId());
};
//タスクを与えて、実行する
for(int i =0 ; i<3;i++) {
exec.submit(test);
}
//65秒間mainメソッドを停止
Thread.sleep(1 * 65000);
System.out.println("------65秒後-------");
//タスクを与えて、実行する
for(int i =0 ; i<5;i++) {
exec.submit(test);
}
}
}
12
14
13
------65秒後-------
16
17
18
18
19
##②SucheduleExecutorServiceインターフェースの利用
処理を実行するタイミングを制御できます。今回は3つのパターンを解説していきます。
####処理を1回だけ遅延時間を設定したい場合
ExecutorsクラスのnewSingleThreadScheduledExecutorメソッドを使います。遅延実行するにはSucheduleExecutorServiceのsucheduleメソッドを使用します。3つの引数を受け取ります。第1引数にはRunnable型の実行したい処理を入れます。第2引数にはlong型の遅延させる時間を入れます。第3引数にはTimeUnitを使用した、時間の単位を入れます。
public class Sample {
public static void main (String[] args) throws InterruptedException {
//新しいスレッドを1つ作成
ScheduledExecutorService exec = Executors.newSingleThreadScheduledExecutor();
//4秒後に処理を実行
exec.schedule(()->{
System.out.println("だー!");
exec.shutdown();
},4,TimeUnit.SECONDS);
int count = 0;
//4秒待機している間に実行
while(true){
Thread.sleep(1000);
if(exec.isShutdown()) {
break;
}
System.out.println(++count);
}
}
}
1
2
3
だー!
####定期的に繰り返し実行する場合
scheduleAtFixedRateメソッドを使用します。第1引数にRunnable型の実行したい処理を入れます。第2引数に、初期遅延。第3引数にインターバルを指定します。第4引数に時間単位を指定します。
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class Sample {
public static void main (String[] args) throws InterruptedException {
//新しいスレッドを1つ作成
ScheduledExecutorService exec = Executors.newSingleThreadScheduledExecutor();
//1秒ごとに処理を実行
exec.scheduleAtFixedRate(()->{
System.out.println("interrupt");
},1,1,TimeUnit.SECONDS);
int count = 0;
//countが50になるまでループ
while(true){
Thread.sleep(100);
System.out.print(">");
count++;
if(count==50) {
exec.shutdown();
break;
}
}
}
}
>>>>>>>>>interrupt
>>>>>>>>>>interrupt
>>>>>>>>>interrupt
>>>>>>>>>>interrupt
>>>>>>>>>interrupt
>>>
####処理の時間関係なく、インターバルを一定にしたい場合
scheduleWithFixedDelayメソッドを使用します。
引数は第1引数にRunnable型の処理、第2引数に初期遅延、第3引数にインターバル、第4引数にインターバルの単位を受け取ります。
import java.util.Random;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class Sample {
public static void main (String[] args) throws InterruptedException {
//新しいスレッドを1つ作成
ScheduledExecutorService exec = Executors.newSingleThreadScheduledExecutor();
//一定のタイミングで実行
exec.scheduleWithFixedDelay(()->{
//ランダムなタイミングで実行
int r = new Random().nextInt(10);
System.out.print(r);
try {
Thread.sleep(r*100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("interrupt");
},1,1,TimeUnit.SECONDS);
int count = 0;
//countが50になるまでループ
while(true){
Thread.sleep(100);
System.out.print(">");
count++;
if(count==50) {
exec.shutdown();
break;
}
}
}
}
下記の数字部分を見れば分るように、処理が一定だと言うことがわかります。
>>>>>>>>>1>interrupt
>>>>>>>>>>6>>>>>>interrupt
>>>>>>>>>5>>>>>interrupt
>>>>>>>>>>
##mainメソッドからスレッドの結果を受け取る方法
ThreadクラスやRunnableインtーフェースでは、mainメソッドからでは、結果を知ることが出来ません。その場合はFutureインターフェースを使います。スレッドの処理結果を受け取るには、getメソッドを使用します。処理が終了すればnullを戻します。
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class Sample {
public static void main (String[] args) throws InterruptedException, ExecutionException {
//スレッドを1つ作成
ExecutorService exec = Executors .newSingleThreadExecutor();
Future future = exec.submit(()->{
try {
System.out.println("start");
Thread.sleep(2000);
System.out.println("end");
}catch(InterruptedException e) {
throw new RuntimeException(e);
}
});
//最後にnullを戻ったら、finishを出力
if(future.get() == null) {
System.out.println("finish");
}
}
}
start
end
finish
###null以外を戻したい場合
submitメソッドを使用します。第1引数は処理を指定し、最後に戻したいものを第2引数で指定します。
public class Sample {
public static void main (String[] args) throws InterruptedException, ExecutionException {
//スレッドを1つ作成
ExecutorService exec = Executors .newSingleThreadExecutor();
Future future = exec.submit(()->{
try {
System.out.println("start");
Thread.sleep(2000);
System.out.println("end");
}catch(InterruptedException e) {
throw new RuntimeException(e);
}
},"finish");
Object result = future.get();
System.out.println(result);
}
}
start
end
finish
###処理や、例外をスローした場合
getメソッドは固定の値しか戻せません。なので、検査例外をスローしたり、処理を戻したり出来ません。ここで使用するのが、Callableです。唯一callメソッド持っていいて、Callableのインスタンスを取得する際に指定する際に型パラメータとなります。
import java.util.List;
import java.util.Random;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class Sample {
public static void main (String[] args) throws InterruptedException, ExecutionException {
//スレッドを1つ作成
ExecutorService exec = Executors.newSingleThreadExecutor();
//偶数かどうか調べる
Callable <Boolean> task = new Callable<Boolean>() {
public Boolean call() throws Exception{
return new Random().nextInt()%2 ==0;
}
};
List<Future<Boolean>> futures = new ArrayList<>();
for(int i = 0; i<5 ; i++) {
futures.add(exec.submit(task));
}
//trueの合計を求める
int total = 0;
for(Future<Boolean>future : futures) {
Boolean result = future.get();
System.out.println(result);
if(result) {
total++;
}
}
System.out.println(total);
}
}
false
true
true
true
false
3
##複数のスレッドが並行して実行されているときに、処理の順番を制御する(同期化処理)方法
CyclicBariierクラスのインスタンスを生成するとき、同期を取るスレッドの数とバリアーアクションをコンストラクターで受け取ります。第1引数に同期化したいスレッドの数、第2引数にバリアアクションが入ります。バリアアクションとは、複数のスレッドが待ち合わせるポイントに全てのスレッドが到達した時の処理。
import java.util.Random;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class Task implements Runnable{
private CyclicBarrier barrier;
public Task(CyclicBarrier barrier) {
super();
this.barrier = barrier;
}
@Override
public void run() {
long id = Thread.currentThread().getId();
System.out.println("START" + id);
int r = new Random().nextInt(10);
try {
Thread.sleep(r*100);
}catch(InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("END"+id);
try {
//処理を中断
this.barrier.await();
}catch(InterruptedException | BrokenBarrierException e) {
throw new RuntimeException(e);
}
}
}
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Sample {
public static void main(String[] args) {
ExecutorService exec = Executors.newFixedThreadPool(3);
CyclicBarrier barrier = new CyclicBarrier(3,()->{
System.out.println("it's all done.");
});
for(int i = 0 ; i<3; i++) {
exec.submit(new Task(barrier));
}
}
}
START14
START12
START13
END13
END12
END14
it's all done.
##終わりに
複数のスレッドを使用すると、一つのインスタンスに複数のスレッドが共有して起こる「競合」という問題が起きてしまいます。競合については、別の記事にまとめましたので、こちらを参考にしてください。
##参考文献
徹底攻略Java SE 11 Gold問題集