13
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

マルチスレッド概念から使い方まで 「初心者から中級者向け」

Last updated at Posted at 2022-02-23

##はじめに
皆さんは、マルチスレッドについてご存知でしょうか。
正しくマルチスレッドを使うことによって、システムのパフォーマンスを向上させることが出来ます。今回は初心者に向けてマルチスレッドにについて概念から使い方まで解説していきます。

##マルチスレッドはなぜ必要なのか?
プログラムの流れは、プログラムの処理が終わるまで次の処理に進めないという特徴を持っています。この特徴の問題点は処理が終わるまで待つ時間ができてしまうことです。
このような問題を解決するのがマルチスレッド並列処理です。マルチスレッド並列処理を解説する前に、並列処理と並行処理について解説していきます。

##並行処理と並列処理
並列処理を一言で言えば、マルチコアで同時に複数の処理を実行することです。詳しく説明すると、コンピュータに搭載しているCPUには複数のコアを持っています。コアの数だけ、同時に複数の処理を進めることができるので、待ち時間を解消することが出来ます。飲食店を例にすると、お店の人が1人だった場合は料理、配膳、食器洗いと時間ごとに仕事をします。このことを並行処理だとイメージしてください。それが複数いると、料理をする人、配膳する人、皿洗いする人に分けることができるので同時進行で仕事を進めることができます。このことを並列処理だとイメージしてください。

##マルチスレッドとは 一言で言うとアプリケーションのプロセス(タスク)を複数のスレッドに分けて並行処理する方式のことをマルチスレッドといいます。スレッドとは、簡単に言えば処理の最小単位です。先程の例だと料理や盛り付けなと1つの仕事だと思ってください。 ##マルチスレッドでの並行処理の実装方法 新しいスレッドを作るには2通りあります。 ・1つ目はRunnableインターフェースを実現したクラスを用意し、そのインスタンスをThreadクラスのコンストラクターに渡します。2つ目はThreadクラスを継承したサブクラスを定義します。 この2通りについて詳しく解説していきます。

###①Runnableインターフェース
Runnableインターインターフェースは新しいスレッドで実行したいrunメソッドのみ持っています。Threadのstartメソッドでstartメソッドを呼び出したスレッドで動作します。一方runメソッドはstartメソッドによって作られた新しいスレッドで動作します。

Sample.java
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メソッドで実装します。

SampleThread.java
public class SampleThread extends Thread{
	public void run() {
		System.out.println("sub");
	}
}

新しいスタックを生成してスレッドを開始するにはThreadクラスのインスタンスを生成し、startメソッドを実行します。

Sample.java
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を表示させています。

Sample.java
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__メソッドを使用します。
生成したいスレッド数を引数で受け取りタスク持ちのスレッドをを保持するスレッドプールを作成します。

Sample.java
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クラスで新しいスレッドを作成しています。

Sample.java
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を使用した、時間の単位を入れます。

Sample.java
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引数に時間単位を指定します。

Sample.java
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引数にインターバルの単位を受け取ります。

Sample.java
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を戻します。

Sample.java
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引数で指定します。

Sample.java
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のインスタンスを取得する際に指定する際に型パラメータとなります。

Sample.java
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引数にバリアアクションが入ります。バリアアクションとは、複数のスレッドが待ち合わせるポイントに全てのスレッドが到達した時の処理。

Task.java
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);
		}
	}
}
Sample.java
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問題集

13
17
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?