Java – スレッドの使い方を初心者向けにわかりやすく解説
Javaでスレッドを使う方法は主に2つあります。
1つ目はThread
クラスを継承し、run
メソッドをオーバーライドする方法です。
2つ目はRunnable
インターフェースを実装し、run
メソッドを定義してThread
に渡す方法です。
後者は柔軟性が高く推奨されます。
スレッドを開始するにはstart
メソッドを呼びます。
スレッドは並行処理を実現するために使われ、例えば複数のタスクを同時に実行したい場合に便利です。
スレッドとは何か
スレッドは、プログラムの中で独立して実行される処理の単位です。
Javaでは、スレッドを使用することで、複数の処理を同時に実行することが可能になります。
これにより、アプリケーションの応答性を向上させたり、リソースを効率的に利用したりすることができます。
スレッドの特徴
- 並行処理: 複数のスレッドが同時に実行されることで、処理の効率が向上します。
- リソースの共有: スレッドは同じメモリ空間を共有するため、データのやり取りが容易です。
- 軽量性: スレッドはプロセスよりも軽量で、作成や管理が比較的簡単です。
スレッドの種類
スレッドの種類 | 説明 |
---|---|
ユーザースレッド | アプリケーションが作成するスレッド。通常の処理を行う。 |
デーモンスレッド | バックグラウンドで動作するスレッド。ユーザースレッドが全て終了すると自動的に終了する。 |
スレッドを利用することで、アプリケーションのパフォーマンスを向上させることができますが、適切な管理が求められます。
次のセクションでは、Javaでスレッドを作成する方法について解説します。
Javaでスレッドを作成する方法
Javaでは、スレッドを作成する方法が主に2つあります。
1つはThread
クラスを継承する方法、もう1つはRunnable
インターフェースを実装する方法です。
それぞれの方法について詳しく解説します。
Threadクラスを継承する方法
Thread
クラスを継承して、run
メソッドをオーバーライドすることでスレッドを作成します。
以下はそのサンプルコードです。
// App.java
public class App extends Thread {
@Override
public void run() {
// スレッドが実行する処理
for (int i = 0; i < 5; i++) {
System.out.println("スレッドからのメッセージ: " + i);
try {
// 1秒待機
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
// スレッドのインスタンスを作成
App thread = new App();
// スレッドを開始
thread.start();
}
}
このコードでは、App
クラスがThread
クラスを継承し、run
メソッド内でスレッドが実行する処理を定義しています。
main
メソッドでは、スレッドのインスタンスを作成し、start
メソッドを呼び出すことでスレッドを開始します。
Runnableインターフェースを実装する方法
Runnable
インターフェースを実装することで、スレッドを作成することもできます。
この方法では、Runnable
を実装したクラスのインスタンスをThread
クラスに渡してスレッドを作成します。
以下はそのサンプルコードです。
// App.java
public class App implements Runnable {
@Override
public void run() {
// スレッドが実行する処理
for (int i = 0; i < 5; i++) {
System.out.println("Runnableからのメッセージ: " + i);
try {
// 1秒待機
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
// Runnableを実装したインスタンスを作成
App runnable = new App();
// Threadクラスのインスタンスを作成
Thread thread = new Thread(runnable);
// スレッドを開始
thread.start();
}
}
このコードでは、App
クラスがRunnable
インターフェースを実装し、run
メソッド内でスレッドが実行する処理を定義しています。
main
メソッドでは、Runnable
を実装したインスタンスを作成し、それをThread
クラスに渡してスレッドを開始します。
Thread
クラスを継承する方法とRunnable
インターフェースを実装する方法の2つがある。- どちらの方法でも、
run
メソッド内にスレッドが実行する処理を記述する。
次のセクションでは、スレッドの制御方法について解説します。
スレッドの制御方法
Javaでは、スレッドの実行を制御するためのさまざまなメソッドが用意されています。
これにより、スレッドの開始、停止、一時停止、再開などを行うことができます。
以下に、主なスレッド制御メソッドを紹介します。
スレッドの開始
スレッドを開始するには、start()
メソッドを使用します。
このメソッドを呼び出すことで、スレッドが新しい実行の流れを持ち、run()
メソッドが実行されます。
スレッドの一時停止
スレッドを一時的に停止させるには、sleep(long millis)
メソッドを使用します。
このメソッドは、指定したミリ秒数だけスレッドを停止します。
以下はそのサンプルコードです。
// App.java
public class App extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("スレッドからのメッセージ: " + i);
try {
// 1秒待機
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
App thread = new App();
thread.start();
}
}
スレッドの停止
スレッドを停止させるには、interrupt()
メソッドを使用します。
このメソッドを呼び出すことで、スレッドが中断され、InterruptedException
がスローされることがあります。
以下はそのサンプルコードです。
// App.java
public class App extends Thread {
@Override
public void run() {
try {
for (int i = 0; i < 5; i++) {
System.out.println("スレッドからのメッセージ: " + i);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
System.out.println("スレッドが中断されました。");
}
}
public static void main(String[] args) {
App thread = new App();
thread.start();
try {
// 3秒後にスレッドを中断
Thread.sleep(3000);
thread.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
スレッドの優先度
スレッドの優先度を設定するには、setPriority(int priority)
メソッドを使用します。
優先度は1から10の範囲で設定でき、数値が大きいほど優先度が高くなります。
以下はそのサンプルコードです。
// App.java
public class App extends Thread {
@Override
public void run() {
System.out.println("スレッドの優先度: " + getPriority());
}
public static void main(String[] args) {
App thread1 = new App();
App thread2 = new App();
// スレッドの優先度を設定
thread1.setPriority(Thread.MIN_PRIORITY); // 最低優先度
thread2.setPriority(Thread.MAX_PRIORITY); // 最高優先度
thread1.start();
thread2.start();
}
}
- スレッドの開始には
start()
メソッドを使用する。 - スレッドを一時停止させるには
sleep()
メソッドを使用する。 - スレッドを停止させるには
interrupt()
メソッドを使用する。 - スレッドの優先度は
setPriority()
メソッドで設定できる。
次のセクションでは、スレッドの同期と競合の回避について解説します。
スレッドの同期と競合の回避
スレッドを使用する際には、複数のスレッドが同じリソースにアクセスすることによる競合状態を避けるために、スレッドの同期が重要です。
競合状態が発生すると、データの不整合や予期しない動作が引き起こされる可能性があります。
ここでは、Javaにおけるスレッドの同期方法と競合の回避策について解説します。
同期化の必要性
複数のスレッドが同時に同じデータにアクセスし、変更を行う場合、データの整合性を保つために同期が必要です。
例えば、カウンターの値をインクリメントする処理を考えます。
以下のようなコードでは、競合状態が発生する可能性があります。
// App.java
public class App extends Thread {
private static int counter = 0; // 共有リソース
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
counter++; // 競合状態が発生する可能性がある
}
}
public static void main(String[] args) throws InterruptedException {
App thread1 = new App();
App thread2 = new App();
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("最終カウンターの値: " + counter); // 期待される値は2000
}
}
このコードでは、counter
が2つのスレッドから同時にアクセスされるため、最終的な値が期待通りにならないことがあります。
同期化の方法
synchronizedキーワード
Javaでは、synchronized
キーワードを使用してメソッドやブロックを同期化することができます。
これにより、同時に1つのスレッドだけがそのメソッドやブロックにアクセスできるようになります。
以下はそのサンプルコードです。
// App.java
public class App extends Thread {
private static int counter = 0; // 共有リソース
// synchronizedメソッド
private synchronized void increment() {
counter++; // 競合状態を回避
}
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
increment(); // synchronizedメソッドを呼び出す
}
}
public static void main(String[] args) throws InterruptedException {
App thread1 = new App();
App thread2 = new App();
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("最終カウンターの値: " + counter); // 期待される値は2000
}
}
このコードでは、increment
メソッドがsynchronized
として宣言されているため、同時に1つのスレッドだけがこのメソッドを実行できます。
これにより、競合状態を回避できます。
synchronizedブロック
特定のコードブロックだけを同期化したい場合は、synchronized
ブロックを使用します。
以下はそのサンプルコードです。
// App.java
public class App extends Thread {
private static int counter = 0; // 共有リソース
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
synchronized (App.class) { // synchronizedブロック
counter++; // 競合状態を回避
}
}
}
public static void main(String[] args) throws InterruptedException {
App thread1 = new App();
App thread2 = new App();
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("最終カウンターの値: " + counter); // 期待される値は2000
}
}
このコードでは、synchronized
ブロックを使用して、counter
のインクリメント処理を同期化しています。
これにより、必要な部分だけを同期化することができます。
- スレッドの競合状態を避けるために、同期が必要である。
synchronized
キーワードを使用してメソッドやブロックを同期化することができる。synchronized
メソッドとsynchronized
ブロックの2つの方法がある。
次のセクションでは、実践例として複数スレッドでタスクを並行処理する方法について解説します。
実践例:複数スレッドでタスクを並行処理する
ここでは、複数のスレッドを使用してタスクを並行処理する実践例を紹介します。
この例では、複数のスレッドが異なるタスクを同時に実行し、最終的な結果を集約するシンプルなプログラムを作成します。
具体的には、各スレッドが数値の合計を計算し、その結果をメインスレッドで集約します。
以下のコードは、複数のスレッドがそれぞれ異なる範囲の数値の合計を計算する例です。
// App.java
public class App extends Thread {
private int start; // 開始値
private int end; // 終了値
private int sum; // 合計値
// コンストラクタ
public App(int start, int end) {
this.start = start;
this.end = end;
this.sum = 0;
}
@Override
public void run() {
// 指定された範囲の合計を計算
for (int i = start; i <= end; i++) {
sum += i;
}
System.out.println("範囲 " + start + " から " + end + " の合計: " + sum);
}
// 合計値を取得するメソッド
public int getSum() {
return sum;
}
public static void main(String[] args) throws InterruptedException {
// スレッドのインスタンスを作成
App thread1 = new App(1, 50); // 1から50の合計
App thread2 = new App(51, 100); // 51から100の合計
// スレッドを開始
thread1.start();
thread2.start();
// スレッドの終了を待機
thread1.join();
thread2.join();
// 合計値を集約
int totalSum = thread1.getSum() + thread2.getSum();
System.out.println("最終合計: " + totalSum); // 期待される値は5050
}
}
- クラスの定義:
App
クラスはThread
を継承し、各スレッドが計算する範囲を指定するためのstart
とend
のフィールドを持っています。
また、計算結果を格納するためのsum
フィールドもあります。
- コンストラクタ: コンストラクタで開始値と終了値を受け取り、合計値を初期化します。
- runメソッド:
run
メソッド内で、指定された範囲の合計を計算します。
計算が完了すると、結果をコンソールに出力します。
- 合計値の取得:
getSum
メソッドを使用して、計算した合計値をメインスレッドで取得します。 - メインメソッド: メインメソッドでは、2つのスレッドを作成し、それぞれ異なる範囲の合計を計算します。
join
メソッドを使用して、両方のスレッドが終了するのを待機し、最終的な合計を計算して出力します。
このプログラムを実行すると、以下のような出力が得られます。
範囲 1 から 50 の合計: 1275
範囲 51 から 100 の合計: 3775
最終合計: 5050
- 複数のスレッドを使用して、異なるタスクを並行処理することができる。
- 各スレッドが独立して計算を行い、最終的な結果をメインスレッドで集約することが可能である。
次のセクションでは、スレッドを使う際のベストプラクティスについて解説します。
スレッドを使う際のベストプラクティス
スレッドを使用する際には、パフォーマンスや安全性を考慮したベストプラクティスを守ることが重要です。
以下に、Javaでスレッドを効果的に利用するためのポイントをいくつか紹介します。
スレッドの数を適切に設定する
- スレッドの数は、システムのリソースや実行するタスクの性質に応じて適切に設定する必要があります。
- 一般的には、CPUコア数に基づいてスレッド数を決定することが推奨されます。
例えば、CPUコア数が4の場合、スレッド数を4またはそれ以下に設定するのが理想です。
スレッドプールを利用する
- スレッドプールを使用することで、スレッドの生成と破棄にかかるオーバーヘッドを削減できます。
- Javaでは、
ExecutorService
を使用してスレッドプールを簡単に管理できます。
以下はそのサンプルコードです。
// App.java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class App implements Runnable {
@Override
public void run() {
System.out.println("スレッドが実行中: " + Thread.currentThread().getName());
}
public static void main(String[] args) {
// スレッドプールを作成
ExecutorService executor = Executors.newFixedThreadPool(3);
// タスクをスレッドプールに送信
for (int i = 0; i < 5; i++) {
executor.submit(new App());
}
// スレッドプールをシャットダウン
executor.shutdown();
}
}
適切な同期を行う
- 共有リソースにアクセスする際は、必ず適切な同期を行い、競合状態を回避することが重要です。
synchronized
キーワードやReentrantLock
を使用して、必要な部分だけを同期化するように心がけましょう。
デッドロックを避ける
- デッドロックは、複数のスレッドが互いにリソースを待ち続ける状態です。
これを避けるためには、リソースの取得順序を統一することが重要です。
- 可能であれば、タイムアウトを設定して、リソースの取得に失敗した場合にスレッドが待機し続けないようにします。
スレッドの例外処理
- スレッド内で発生した例外は、メインスレッドに影響を与える可能性があります。
スレッド内で適切に例外処理を行い、エラーログを記録することが重要です。
// App.java
public class App extends Thread {
@Override
public void run() {
try {
// 例外を発生させる処理
throw new RuntimeException("エラーが発生しました。");
} catch (Exception e) {
System.err.println("スレッド内で例外が発生: " + e.getMessage());
}
}
public static void main(String[] args) {
App thread = new App();
thread.start();
}
}
スレッドの状態を監視する
- スレッドの状態を監視することで、パフォーマンスのボトルネックや問題を早期に発見できます。
- スレッドの状態を確認するために、
Thread
クラスのメソッド(getState()
など)を活用しましょう。
- スレッドの数を適切に設定し、スレッドプールを利用することでパフォーマンスを向上させる。
- 適切な同期を行い、デッドロックを避けることが重要である。
- スレッド内での例外処理や状態監視を行い、安定したアプリケーションを実現する。
これらのベストプラクティスを守ることで、Javaにおけるスレッドの利用がより効果的かつ安全になります。
まとめ
この記事では、Javaにおけるスレッドの基本的な概念から、スレッドの作成方法、制御方法、同期と競合の回避、さらには実践例を通じて複数スレッドでのタスクの並行処理について詳しく解説しました。
また、スレッドを使用する際のベストプラクティスについても触れ、効率的かつ安全にスレッドを活用するためのポイントを紹介しました。
これらの知識を活かして、実際のプログラムにスレッドを取り入れ、パフォーマンスの向上を図ってみてください。