Java – スレッドとは?意味や簡単なプログラムを紹介
スレッドとは、プログラム内で並行して実行される処理の単位を指します。
Javaでは、マルチスレッドを利用することで複数のタスクを同時に実行可能です。
これにより、CPUリソースを効率的に活用し、応答性の向上や処理時間の短縮が期待できます。
スレッドは、Thread
クラスを継承するか、Runnable
インターフェースを実装して作成します。
Javaでスレッドを作成する方法
Javaにおけるスレッドの作成方法は主に2つあります。
1つはThread
クラスを継承する方法、もう1つはRunnable
インターフェースを実装する方法です。
それぞれの方法について詳しく解説します。
Threadクラスを継承する方法
Thread
クラスを継承することで、スレッドを作成することができます。
以下はそのサンプルコードです。
// App.java
public class MyThread 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) {
MyThread thread = new MyThread(); // スレッドのインスタンスを作成
thread.start(); // スレッドを開始
}
}
このコードを実行すると、スレッドが1秒ごとにメッセージを出力します。
スレッドからのメッセージ: 0
スレッドからのメッセージ: 1
スレッドからのメッセージ: 2
スレッドからのメッセージ: 3
スレッドからのメッセージ: 4
Runnableインターフェースを実装する方法
Runnable
インターフェースを実装することで、スレッドを作成することもできます。
この方法では、スレッドの処理を別のクラスに分けることができ、より柔軟な設計が可能です。
以下はそのサンプルコードです。
// App.java
public class MyRunnable 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) {
MyRunnable myRunnable = new MyRunnable(); // Runnableのインスタンスを作成
Thread thread = new Thread(myRunnable); // スレッドを作成
thread.start(); // スレッドを開始
}
}
このコードを実行すると、Runnable
インターフェースを実装したクラスが1秒ごとにメッセージを出力します。
Runnableからのメッセージ: 0
Runnableからのメッセージ: 1
Runnableからのメッセージ: 2
Runnableからのメッセージ: 3
Runnableからのメッセージ: 4
Javaでは、Thread
クラスを継承する方法とRunnable
インターフェースを実装する方法の2つでスレッドを作成できます。
どちらの方法も簡単にスレッドを利用できるため、用途に応じて使い分けることが重要です。
スレッドの制御
Javaでは、スレッドの制御を行うためのさまざまなメソッドが用意されています。
これにより、スレッドの実行を一時停止したり、再開したり、終了させたりすることができます。
以下に、主なスレッド制御メソッドを紹介します。
スレッドの開始と停止
- start(): スレッドを開始します。
スレッドが実行可能状態になります。
- stop(): スレッドを強制的に停止しますが、非推奨です。
代わりにフラグを使用してスレッドを終了させる方法が推奨されます。
スレッドの一時停止と再開
- sleep(long millis): 指定した時間(ミリ秒)だけスレッドを一時停止します。
- wait(): オブジェクトのモニターを解放し、他のスレッドがそのオブジェクトにアクセスできるようにします。
再開するには、notify()またはnotifyAll()を使用します。
- notify(): 待機中のスレッドを1つ再開します。
- notifyAll(): 待機中のすべてのスレッドを再開します。
スレッドの優先度
スレッドには優先度を設定することができ、これによりスケジューリングの際の実行順序に影響を与えます。
優先度は1から10の整数で設定され、デフォルトは5です。
- setPriority(int newPriority): スレッドの優先度を設定します。
- getPriority(): スレッドの優先度を取得します。
以下は、スレッドの制御を示すサンプルコードです。
スレッドを一時停止し、再開する例を示します。
// App.java
public class ControlThread extends Thread {
private boolean running = true; // スレッドの実行フラグ
@Override
public void run() {
while (running) {
System.out.println("スレッドが実行中...");
try {
// 1秒待機
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("スレッドが終了しました。");
}
public void stopThread() {
running = false; // スレッドを終了させるフラグを設定
}
public static void main(String[] args) throws InterruptedException {
ControlThread thread = new ControlThread(); // スレッドのインスタンスを作成
thread.start(); // スレッドを開始
// 5秒後にスレッドを停止
Thread.sleep(5000);
thread.stopThread(); // スレッドを停止
}
}
このコードを実行すると、スレッドが5秒間実行され、その後終了します。
スレッドが実行中...
スレッドが実行中...
スレッドが実行中...
スレッドが実行中...
スレッドが実行中...
スレッドが終了しました。
スレッドの制御は、スレッドプログラミングにおいて非常に重要な要素です。
start()
, sleep()
, wait()
, notify()
などのメソッドを適切に使用することで、スレッドの動作を柔軟に制御することができます。
スレッドの同期と安全性
スレッドプログラミングにおいて、複数のスレッドが同じリソースにアクセスする場合、データの整合性を保つために「同期」が必要です。
スレッドの同期を適切に行わないと、データ競合や不整合が発生する可能性があります。
ここでは、スレッドの同期と安全性について解説します。
スレッドの競合状態
スレッドが同時に同じリソースにアクセスすると、競合状態が発生します。
これにより、予期しない動作やデータの不整合が生じることがあります。
例えば、カウンターをインクリメントするスレッドが複数ある場合、正しい結果が得られないことがあります。
同期の方法
Javaでは、スレッドの同期を行うために以下の方法があります。
- synchronizedキーワード: メソッドまたはブロックに
synchronized
を付けることで、同時に1つのスレッドだけがそのメソッドまたはブロックにアクセスできるようにします。 - Lockインターフェース:
java.util.concurrent.locks
パッケージに含まれるLock
インターフェースを使用することで、より柔軟なロック機構を実現できます。
synchronizedキーワードの使用例
以下は、synchronized
を使用してスレッドの安全性を確保するサンプルコードです。
// App.java
public class SynchronizedCounter {
private int count = 0; // カウンター
// カウンターをインクリメントするメソッド
public synchronized void increment() {
count++; // カウンターをインクリメント
}
// カウンターの値を取得するメソッド
public int getCount() {
return count; // カウンターの値を返す
}
public static void main(String[] args) throws InterruptedException {
SynchronizedCounter counter = new SynchronizedCounter(); // カウンターのインスタンスを作成
// スレッドを作成
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment(); // カウンターをインクリメント
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment(); // カウンターをインクリメント
}
});
thread1.start(); // スレッド1を開始
thread2.start(); // スレッド2を開始
thread1.join(); // スレッド1の終了を待つ
thread2.join(); // スレッド2の終了を待つ
// カウンターの値を表示
System.out.println("最終カウント: " + counter.getCount());
}
}
このコードを実行すると、2つのスレッドが同時にカウンターをインクリメントしますが、synchronized
を使用しているため、最終的なカウントは2000になります。
最終カウント: 2000
Lockインターフェースの使用例
Lock
インターフェースを使用することで、より細かい制御が可能になります。
以下はそのサンプルコードです。
// App.java
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockCounter {
private int count = 0; // カウンター
private Lock lock = new ReentrantLock(); // ロックのインスタンスを作成
// カウンターをインクリメントするメソッド
public void increment() {
lock.lock(); // ロックを取得
try {
count++; // カウンターをインクリメント
} finally {
lock.unlock(); // ロックを解放
}
}
// カウンターの値を取得するメソッド
public int getCount() {
return count; // カウンターの値を返す
}
public static void main(String[] args) throws InterruptedException {
LockCounter counter = new LockCounter(); // カウンターのインスタンスを作成
// スレッドを作成
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment(); // カウンターをインクリメント
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment(); // カウンターをインクリメント
}
});
thread1.start(); // スレッド1を開始
thread2.start(); // スレッド2を開始
thread1.join(); // スレッド1の終了を待つ
thread2.join(); // スレッド2の終了を待つ
// カウンターの値を表示
System.out.println("最終カウント: " + counter.getCount());
}
}
このコードを実行すると、Lock
を使用してカウンターを安全にインクリメントします。
最終カウント: 2000
スレッドの同期と安全性は、マルチスレッドプログラミングにおいて非常に重要です。
synchronized
やLock
を使用することで、データの整合性を保ちながらスレッドを安全に制御することができます。
適切な同期を行うことで、競合状態を防ぎ、信頼性の高いプログラムを作成することができます。
スレッドを活用した簡単なプログラム例
スレッドを活用することで、同時に複数の処理を実行することができます。
ここでは、スレッドを使った簡単なプログラムの例をいくつか紹介します。
これにより、スレッドの実用的な使い方を理解することができます。
数字のカウントとメッセージの表示
以下のプログラムでは、1つのスレッドが数字をカウントし、もう1つのスレッドがメッセージを表示します。
これにより、同時に異なる処理を実行することができます。
// App.java
public class CountAndMessage {
public static void main(String[] args) {
// 数字をカウントするスレッド
Thread countThread = new Thread(() -> {
for (int i = 1; i <= 5; i++) {
System.out.println("カウント: " + i);
try {
// 1秒待機
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// メッセージを表示するスレッド
Thread messageThread = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("メッセージ: Hello, World!");
try {
// 1.5秒待機
Thread.sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
countThread.start(); // カウントスレッドを開始
messageThread.start(); // メッセージスレッドを開始
}
}
このプログラムを実行すると、カウントとメッセージが交互に表示されます。
出力結果は以下のようになります(実行のタイミングによって異なる場合があります)。
カウント: 1
メッセージ: Hello, World!
カウント: 2
メッセージ: Hello, World!
カウント: 3
カウント: 4
メッセージ: Hello, World!
カウント: 5
複数のスレッドによる計算
次のプログラムでは、複数のスレッドを使用して、異なる範囲の数値の合計を計算します。
これにより、計算処理を並行して実行することができます。
// App.java
public class SumCalculator {
private static int totalSum = 0; // 合計値
private static final Object lock = new Object(); // ロックオブジェクト
public static void main(String[] args) throws InterruptedException {
// スレッド1: 1から50までの合計
Thread thread1 = new Thread(() -> {
int sum = 0;
for (int i = 1; i <= 50; i++) {
sum += i;
}
synchronized (lock) {
totalSum += sum; // 合計値を更新
}
System.out.println("スレッド1の合計: " + sum);
});
// スレッド2: 51から100までの合計
Thread thread2 = new Thread(() -> {
int sum = 0;
for (int i = 51; i <= 100; i++) {
sum += i;
}
synchronized (lock) {
totalSum += sum; // 合計値を更新
}
System.out.println("スレッド2の合計: " + sum);
});
thread1.start(); // スレッド1を開始
thread2.start(); // スレッド2を開始
thread1.join(); // スレッド1の終了を待つ
thread2.join(); // スレッド2の終了を待つ
// 最終合計を表示
System.out.println("最終合計: " + totalSum);
}
}
このプログラムを実行すると、各スレッドが計算した合計と最終合計が表示されます。
スレッド1の合計: 1275
スレッド2の合計: 5050
最終合計: 6325
スレッドプールを使用したタスクの実行
JavaのExecutorService
を使用して、スレッドプールを作成し、複数のタスクを並行して実行する例を示します。
これにより、スレッドの管理が容易になります。
// App.java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(3); // スレッドプールを作成
// タスクを実行
for (int i = 1; i <= 5; i++) {
final int taskId = i;
executor.submit(() -> {
System.out.println("タスク " + taskId + " が実行中...");
try {
// 2秒待機
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("タスク " + taskId + " が完了しました。");
});
}
executor.shutdown(); // スレッドプールをシャットダウン
}
}
このプログラムを実行すると、スレッドプール内のスレッドがタスクを並行して実行します。
出力結果は以下のようになります(実行のタイミングによって異なる場合があります)。
タスク 1 が実行中...
タスク 2 が実行中...
タスク 3 が実行中...
タスク 1 が完了しました。
タスク 2 が完了しました。
タスク 3 が完了しました。
タスク 4 が実行中...
タスク 5 が実行中...
タスク 4 が完了しました。
タスク 5 が完了しました。
スレッドを活用することで、同時に複数の処理を実行することができます。
上記の例では、カウントとメッセージの表示、複数のスレッドによる計算、スレッドプールを使用したタスクの実行を示しました。
これにより、スレッドの実用的な使い方を理解し、さまざまなシナリオでスレッドを活用することができるようになります。
スレッドを使用する際の注意点
スレッドを使用する際には、いくつかの注意点があります。
これらを理解し、適切に対処することで、スレッドプログラミングのトラブルを避け、より安定したアプリケーションを作成することができます。
以下に、主な注意点を挙げます。
競合状態の回避
複数のスレッドが同じリソースに同時にアクセスすると、競合状態が発生する可能性があります。
これにより、データの不整合や予期しない動作が生じることがあります。
競合状態を回避するためには、以下の方法を使用します。
- 同期化:
synchronized
キーワードやLock
インターフェースを使用して、同時に1つのスレッドだけがリソースにアクセスできるようにします。 - 不変オブジェクトの使用: 変更されないオブジェクトを使用することで、競合状態を避けることができます。
デッドロックの防止
デッドロックは、2つ以上のスレッドが互いにリソースを待ち続ける状態です。
これにより、プログラムが停止してしまうことがあります。
デッドロックを防ぐためには、以下の対策を講じます。
- ロックの順序を統一: 複数のリソースをロックする場合、常に同じ順序でロックを取得するようにします。
- タイムアウトを設定: ロックを取得する際にタイムアウトを設定し、一定時間内に取得できない場合は処理を中断します。
スレッドの管理
スレッドを適切に管理しないと、リソースの無駄遣いやメモリリークが発生することがあります。
スレッドの管理に関する注意点は以下の通りです。
- スレッドの終了: スレッドが不要になったら、適切に終了させることが重要です。
interrupt()
メソッドを使用してスレッドを終了させることができます。
- スレッドプールの利用: スレッドを直接作成するのではなく、
ExecutorService
を使用してスレッドプールを管理することで、リソースの効率的な利用が可能になります。
スレッドの優先度
スレッドには優先度があり、これによりスケジューリングの際の実行順序に影響を与えます。
ただし、スレッドの優先度は必ずしも実行順序に反映されるわけではないため、過度に依存しないように注意が必要です。
優先度を設定する際は、以下の点に留意します。
- デフォルトの優先度を使用: 特別な理由がない限り、デフォルトの優先度(5)を使用することが推奨されます。
- 優先度の変更は慎重に: 優先度を変更する場合は、他のスレッドとのバランスを考慮し、慎重に行います。
スレッドセーフなコレクションの使用
スレッドが同時にアクセスするコレクションを使用する場合、スレッドセーフなコレクションを選択することが重要です。
Javaには、スレッドセーフなコレクションがいくつか用意されています。
- ConcurrentHashMap: スレッドセーフなハッシュマップ。
- CopyOnWriteArrayList: スレッドセーフなArrayList。
読み取りが多い場合に適しています。
スレッドを使用する際には、競合状態やデッドロック、スレッドの管理、優先度、スレッドセーフなコレクションなど、さまざまな注意点があります。
これらを理解し、適切に対処することで、安定したマルチスレッドアプリケーションを構築することができます。
スレッドの応用例
スレッドは、さまざまなアプリケーションで利用されており、特に並行処理が求められる場面でその効果を発揮します。
ここでは、スレッドの具体的な応用例をいくつか紹介します。
Webサーバーのリクエスト処理
Webサーバーは、同時に複数のクライアントからのリクエストを処理する必要があります。
スレッドを使用することで、各リクエストを独立したスレッドで処理し、応答を迅速に返すことができます。
// App.java
import java.io.*;
import java.net.*;
public class SimpleWebServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(8080); // ポート8080でサーバーを開始
while (true) {
Socket clientSocket = serverSocket.accept(); // クライアントからの接続を待機
new Thread(new RequestHandler(clientSocket)).start(); // 新しいスレッドでリクエストを処理
}
}
}
class RequestHandler implements Runnable {
private Socket clientSocket;
public RequestHandler(Socket socket) {
this.clientSocket = socket;
}
@Override
public void run() {
try (PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()))) {
String inputLine;
while ((inputLine = in.readLine()) != null) {
if (inputLine.isEmpty()) break; // リクエストの終わり
System.out.println("リクエスト: " + inputLine);
}
out.println("HTTP/1.1 200 OK");
out.println("Content-Type: text/plain");
out.println();
out.println("Hello, World!"); // レスポンスを返す
} catch (IOException e) {
e.printStackTrace();
}
}
}
このプログラムを実行すると、ポート8080でリクエストを受け付け、各リクエストを新しいスレッドで処理します。
バックグラウンド処理
スレッドは、ユーザーインターフェースをブロックせずにバックグラウンドで処理を行うためにも使用されます。
例えば、ファイルのダウンロードやデータの処理をバックグラウンドで行うことができます。
// App.java
public class BackgroundTaskExample {
public static void main(String[] args) {
Thread downloadThread = new Thread(() -> {
System.out.println("ダウンロードを開始します...");
try {
// ダウンロード処理のシミュレーション
Thread.sleep(5000); // 5秒待機
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("ダウンロードが完了しました。");
});
downloadThread.start(); // バックグラウンドでダウンロードを開始
// メインスレッドで他の処理を行う
System.out.println("メインスレッドで他の処理を実行中...");
}
}
このプログラムでは、ダウンロード処理をバックグラウンドで行いながら、メインスレッドで他の処理を続けることができます。
ゲームのフレーム更新
ゲーム開発において、スレッドはゲームのフレームを更新するために使用されます。
ゲームのロジックや描画処理を別のスレッドで実行することで、スムーズなプレイ体験を提供します。
// App.java
public class GameExample {
public static void main(String[] args) {
Thread gameThread = new Thread(() -> {
while (true) {
updateGame(); // ゲームの状態を更新
renderGame(); // ゲームを描画
try {
Thread.sleep(16); // 約60FPSで更新
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
gameThread.start(); // ゲームスレッドを開始
}
private static void updateGame() {
// ゲームのロジックを更新する処理
System.out.println("ゲームの状態を更新中...");
}
private static void renderGame() {
// ゲームを描画する処理
System.out.println("ゲームを描画中...");
}
}
このプログラムでは、ゲームの状態を更新し、描画を行う処理をスレッドで実行しています。
データベースの非同期処理
データベースへのクエリを非同期で実行することで、アプリケーションの応答性を向上させることができます。
スレッドを使用して、データベースからのデータ取得をバックグラウンドで行うことができます。
// App.java
import java.sql.*;
public class AsyncDatabaseQuery {
public static void main(String[] args) {
Thread queryThread = new Thread(() -> {
try {
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb", "user", "password");
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery("SELECT * FROM my_table");
while (resultSet.next()) {
System.out.println("データ: " + resultSet.getString("column_name"));
}
resultSet.close();
statement.close();
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
});
queryThread.start(); // データベースクエリをバックグラウンドで実行
}
}
このプログラムでは、データベースへのクエリを別のスレッドで実行し、メインスレッドの応答性を保っています。
スレッドは、Webサーバーのリクエスト処理、バックグラウンド処理、ゲームのフレーム更新、データベースの非同期処理など、さまざまな場面で活用されています。
これらの応用例を通じて、スレッドの利点を理解し、実際のアプリケーションに適用することができます。
まとめ
この記事では、Javaにおけるスレッドの基本的な概念から、スレッドの制御、同期、安全性、応用例まで幅広く取り上げました。
スレッドを適切に活用することで、アプリケーションのパフォーマンスを向上させ、ユーザー体験を向上させることが可能です。
これを機に、実際のプロジェクトにスレッドを取り入れ、並行処理の利点を体感してみてはいかがでしょうか。