Javaでのマルチスレッドの10の活用方法

Javaのマルチスレッドのサンプルコードを表示しているスクリーンショットJava
この記事は約26分で読めます。

 

【サイト内のコードはご自由に個人利用・商用利用いただけます】

この記事では、プログラムの基礎知識を前提に話を進めています。

説明のためのコードや、サンプルコードもありますので、もちろん初心者でも理解できるように表現してあります。

基本的な知識があればカスタムコードを使って機能追加、目的を達成できるように作ってあります。

※この記事は、一般的にプロフェッショナルの指標とされる『実務経験10,000時間以上』を凌駕する現役のプログラマチームによって監修されています。

サイト内のコードを共有する場合は、参照元として引用して下さいますと幸いです

※Japanシーモアは、常に解説内容のわかりやすさや記事の品質に注力しております。不具合、分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)

はじめに

Javaのプログラミング言語には、さまざまな特性と機能が備わっておりますが、その中でもマルチスレッドという技術は非常に強力であります。

今回の記事では、Javaのマルチスレッドに関する基本的な概念から、具体的な利用方法までを、初心者にも理解しやすいよう超詳細に解説いたします。

この知識は、プログラムの処理速度を向上させるなど、あらゆるプロジェクトでの応用が可能です。

●Javaとマルチスレッドとは?

Javaとは、一般に広く利用されるプログラミング言語の一つです。

そして、マルチスレッドとは、一つのプログラム内で複数の処理を同時に行うための技術のことを指します。

ここでは、Javaにおけるマルチスレッドの基本的な概念と、その利用時のメリットとデメリットについて説明します。

○Javaにおけるマルチスレッドの概念

マルチスレッドは、一つのプロセス内で複数のスレッドを並行して実行する技術です。

スレッドはプロセス内の実行単位であり、複数のスレッドが同時に動作することで、多くのタスクを同時並行的に処理することが可能になります。

Javaでは、ThreadクラスやRunnableインターフェイスを利用して、マルチスレッドプログラムを実装することができます。

ここでは、これらの基本的なクラスとインターフェイスの利用方法について、簡単に解説します。

Javaにおけるマルチスレッドの実装は、Threadクラスを直接利用する方法と、Runnableインターフェイスを利用する方法の2種類があります。

前者は、Threadクラスを拡張して新しいクラスを作成し、runメソッドをオーバーライドして処理を実装する方法です。

後者は、Runnableインターフェイスを実装したクラスを作成し、そのrunメソッド内に処理を実装する方法です。

どちらの方法も、startメソッドを呼び出してスレッドを起動します。

○マルチスレッドのメリットとデメリット

マルチスレッドの技術を利用することによって得られるメリットは数多くあります。

まず第一に、プログラムの処理速度が向上します。

複数のスレッドが並行して作業を行うことで、タスク全体の実行時間が短縮される可能性があります。

また、リソースを効率的に利用できるという点もメリットとして挙げられます。

一方で、マルチスレッドにはいくつかのデメリットも存在します。

複数のスレッドが同時にアクセスすることで起こる競合状態や、デッドロックという問題があります。

また、プログラムの複雑性が増し、デバッグが困難になるというデメリットもあります。

これらの問題を避けるためには、適切なスレッド管理と同期処理が必要となります。

●Javaでのマルチスレッドの基本的な使い方

Javaのプログラムの実行スピードを向上させるためには、マルチスレッドの技術が非常に有効です。

複数のスレッドを使って作業を行えば、プログラムの実行時間を短縮することができます。

Java言語では、マルチスレッドプログラミングを支援するためのいくつかのクラスとインターフェイスを提供しています。

ここでは、マルチスレッドの基本的な使い方を説明します。

○スレッドの作成方法

スレッドの作成は基本的に二通りの方法があります。

一つ目はThreadクラスを継承して新しいクラスを作成する方法であり、二つ目はRunnableインターフェイスを実装する方法です。

Threadクラスを継承する方法は簡単ですが、Javaでは単一継承しか許されないため、他のクラスを継承することができなくなります。

そのため、Runnableインターフェイスを実装する方法が推奨されます。

ここでは、Runnableインターフェイスを実装する方法について簡単なサンプルコードとその説明を紹介します。

○サンプルコード1:シンプルなマルチスレッドの作成

下記のサンプルコードではRunnableインターフェイスを実装したクラスを作成し、そのインスタンスをThreadクラスのコンストラクタに渡してスレッドを作成します。

そして、スレッドのstartメソッドを呼び出すことでスレッドが起動し、runメソッドが実行されます。

public class SimpleThreadExample implements Runnable {
    @Override
    public void run() {
        for(int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + " : " + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        Thread thread1 = new Thread(new SimpleThreadExample(), "スレッド1");
        Thread thread2 = new Thread(new SimpleThreadExample(), "スレッド2");
        thread1.start();
        thread2.start();
    }
}

このサンプルコードを実行すると、”スレッド1″と”スレッド2″という名前が付けられた二つのスレッドが並行して実行されます。

各スレッドは0から4までの数字を一秒間隔でコンソールに出力します。

○スレッドの終了と中断

スレッドの終了は、runメソッドの実行が完了すると自然に終了します。

しかし、中断や強制終了も必要な場合があります。

スレッドを中断するためには、interruptメソッドを使用します。

そして、スレッド内部でInterruptedExceptionが発生した場合、中断フラグがセットされ、スレッドが安全に終了できます。

○サンプルコード2:スレッドの中断と再開

スレッドの中断と再開の方法を表すサンプルコードとその説明を紹介します。

public class InterruptThreadExample implements Runnable {
    @Override
    public void run() {
        try {
            for(int i = 0; i < 10; i++) {
                if(Thread.currentThread().isInterrupted()) {
                    System.out.println(Thread.currentThread().getName() + "が中断されました。");
                    return;
                }
                System.out.println(Thread.currentThread().getName() + " : " + i);
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName() + "が中断されました。");
        }
    }

    public static void main(String[] args) {
        Thread thread1 = new Thread(new InterruptThreadExample(), "スレッド1");
        Thread thread2 = new Thread(new InterruptThreadExample(), "スレッド2");
        thread1.start();
        thread2.start();

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread1.interrupt();
    }
}

このサンプルコードでは、スレッドが中断されたかどうかをチェックするためのisInterruptedメソッドを使用しています。

そして、中断フラグがセットされた場合、スレッドが安全に終了します。

●マルチスレッドの詳細な使い方

Javaにおけるマルチスレッドのプログラミングは、一度に多くのタスクを同時に処理することができるため、アプリケーションのパフォーマンスを向上させる有効な方法として広く利用されています。

それでは、具体的な使い方を次の項目で詳細に見ていきましょう。

○スレッドプールの活用

スレッドプールは、プリアロケートされたスレッドの集合体であり、その中から利用可能なスレッドを利用してタスクを実行します。

これにより、スレッドの生成と破棄にかかるコストを削減できます。

スレッドプールはExecutorServiceインターフェイスを通じて利用できます。

それでは、実際にスレッドプールを使ったサンプルコードとその詳細な説明を紹介します。

○サンプルコード3:スレッドプールを使ったタスクの実行

import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;

public class ThreadPoolExample {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(2);
        for (int i = 0; i < 5; i++) {
            Runnable worker = new WorkerThread("" + i);
            executor.execute(worker);
        }
        executor.shutdown();
        while (!executor.isTerminated()) {
        }
        System.out.println("Finished all threads");
    }
}

class WorkerThread implements Runnable {
    private String command;

    public WorkerThread(String s) {
        this.command = s;
    }

    public void run() {
        System.out.println(Thread.currentThread().getName() + " Start. Command = " + command);
        processCommand();
        System.out.println(Thread.currentThread().getName() + " End.");
    }

    private void processCommand() {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Override
    public String toString() {
        return this.command;
    }
}

このサンプルコードでは、スレッドプールを使用してタスクを実行します。

ExecutorServiceインターフェイスを利用し、スレッドプールのサイズを2に設定しています。

その後、5つのタスクをスレッドプールに送信し、各タスクが順番に処理されることを確認します。

スレッドプールの利用により、リソースの効率的な管理と高速なタスクの処理が可能となります。

○同期と排他制御

マルチスレッドプログラムでは、データ競合やデッドロックといった問題が発生する可能性があります。

同期と排他制御は、これらの問題を解決するための重要な技術です。

JavaではsynchronizedキーワードやReentrantLockクラスを利用して排他制御を行うことができます。

○サンプルコード4:synchronizedを使用した同期

public class SynchronizedExample {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public void runTest() {
        Thread thread1 = new Thread(new Runnable() {
            public void run() {
                for (int i = 0; i < 10000; i++) {
                    increment();
                }
            }
        });
        Thread thread2 = new Thread(new Runnable() {
            public void run() {
                for (int i = 0; i < 10000; i++) {
                    increment();
                }
            }
        });
        thread1.start();
        thread2.start();
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Count is: " + count);
    }

    public static void main(String[] args) {
        SynchronizedExample example = new SynchronizedExample();
        example.runTest();
    }
}

このサンプルコードでは、synchronizedキーワードを使用してメソッドを同期します。

このため、一度に一つのスレッドだけがincrementメソッドを実行できます。

これにより、複数のスレッドが同時にアクセスした際のデータ競合を防ぐことができます。

●マルチスレッドの応用例

Javaでのマルチスレッドプログラミングは、一度に多くのタスクを効率的に実行する際に非常に役立つ技術です。

この部分では、いくつかの高度なマルチスレッドの応用例を見ていきます。

初心者でも理解できるよう、詳細な説明とサンプルコードを交えて解説いたします。

○並列処理の最適化

並列処理は、複数の処理を同時に行うことで、プログラムの実行時間を短縮する技法です。

特に大量のデータを扱う際には、並列処理の最適化が非常に重要となります。

ここでは、Javaでの並列処理の最適化技法に焦点を当てて解説します。

○サンプルコード5:大量のデータの並列処理

下記のサンプルコードは、Javaで大量のデータを並列に処理するためのものです。

このコードではExecutorServiceを使用しています。

ExecutorServiceインターフェイスを利用すると、スレッドプールを作成し、多くのタスクを効率的に処理することが可能です。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ParallelProcessingExample {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(4);

        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            executorService.submit(() -> {
                System.out.println("Task " + finalI + " is being processed by thread: " + Thread.currentThread().getName());
            });
        }

        executorService.shutdown();
    }
}

このサンプルコードは、ExecutorServiceのnewFixedThreadPoolメソッドを使用してスレッドプールを作成しています。

スレッドプール内の各スレッドは、submitメソッドによって投げられたタスクを並列に処理します。

タスクが終了したら、executorServiceのshutdownメソッドを呼び出してスレッドプールを終了します。

○イベント駆動型のタスク管理

次に、イベント駆動型のタスク管理に関する解説を行います。

イベント駆動型のプログラミングは、特定のイベントが発生した際に特定のタスクがトリガされる方式を指します。

Javaでは、イベントリスナーを利用してイベント駆動型のタスク管理を実装することが可能です。

○サンプルコード6:イベントリスナーを使用したマルチスレッド

下記のサンプルコードは、Javaでイベントリスナーを利用したマルチスレッドのタスク管理の実装例です。

import java.util.ArrayList;
import java.util.List;

public class EventDrivenMultithreading {
    public static void main(String[] args) {
        EventManager eventManager = new EventManager();
        eventManager.addListener(event -> {
            System.out.println("Event received: " + event);
        });

        List<Thread> threads = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            int finalI = i;
            Thread thread = new Thread(() -> {
                eventManager.notifyEvent("Event from thread " + finalI);
            });
            threads.add(thread);
            thread.start();
        }

        for (Thread thread : threads) {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class EventManager {
    private final List<Event> listeners = new ArrayList<>();

    public void addListener(Event listener) {
        listeners.add(listener);
    }

    public void notifyEvent(String event) {
        for (Event listener : listeners) {
            listener.onEvent(event);
        }
    }

    interface Event {
        void onEvent(String event);
    }
}

このコードは、イベントマネージャクラス(EventManager)を使用しています。

イベントマネージャは、リスナーを登録し、特定のイベントが発生したときにリスナーに通知します。

この実装では、複数のスレッドが並列に動作し、それぞれのスレッドからイベントがトリガされると、登録されたリスナーに通知されます。

●マルチスレッドの注意点と対処法

マルチスレッドプログラミングは非常に効率的な方法である一方で、その複雑性が増すとともにいくつかの問題点が生じる可能性があります。

それに対処するための様々な技術や手法があります。

今回はその注意点と対処法について詳細に解説いたします。

○デッドロックとは?

デッドロックとは複数のスレッドがお互いのリソースを持ちながら他のスレッドのリソースの解放を待つ状態のことを指します。

この状態が発生すると、プログラムは停止してしまい、どのスレッドも進行できなくなります。

対処法としては、デッドロックの発生を予防する設計や、デッドロックが発生した際にそれを解消するための技術があります。

リソースの取得順序を固定するやリソースのタイムアウト設定を利用するなどのテクニックが挙げられます。

○サンプルコード7:デッドロックの回避方法

下記のサンプルコードは、デッドロックが発生しないようにするための一例を表しています。

public class DeadlockAvoidance {
    private final Object lock1 = new Object();
    private final Object lock2 = new Object();

    public void method1() {
        synchronized(lock1) {
            System.out.println("Method 1: Locked lock1");
            synchronized(lock2) {
                System.out.println("Method 1: Locked lock2");
            }
        }
    }

    public void method2() {
        synchronized(lock2) {
            System.out.println("Method 2: Locked lock2");
            synchronized(lock1) {
                System.out.println("Method 2: Locked lock1");
            }
        }
    }

    public static void main(String[] args) {
        DeadlockAvoidance deadlockAvoidance = new DeadlockAvoidance();
        new Thread(deadlockAvoidance::method1).start();
        new Thread(deadlockAvoidance::method2).start();
    }
}

このコードでは、デッドロックを避けるために、異なるメソッドで異なる順序でロックを取得しています。このようにして、デッドロックの発生を防止しています。

実行すると、スレッドは互いにロックを取得し、デッドロックが発生しないことを確認できます。

○リソースの適切な管理

リソースの適切な管理も、デッドロックや他のリソース競合の問題を避ける上で重要です。

リソースの取得と解放を適切に行い、必要なリソースのみをロックすることで、競合条件を最小限に抑えることができます。

また、リソースの階層化やタイムアウトの導入など、複数のテクニックを組み合わせることで更なる安全性を追求することが可能です。

○サンプルコード8:リソースの制限と監視

下記のサンプルコードでは、リソースの制限と監視の方法を表しています。

import java.util.concurrent.Semaphore;

public class ResourceManagement {
    private final Semaphore semaphore = new Semaphore(3);

    public void useResource() {
        try {
            semaphore.acquire();
            System.out.println("Resource acquired");
            // Simulate resource usage
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            semaphore.release();
            System.out.println("Resource released");
        }
    }

    public static void main(String[] args) {
        ResourceManagement resourceManagement = new ResourceManagement();
        for(int i = 0; i < 5; i++) {
            new Thread(resourceManagement::useResource).start();
        }
    }
}

このサンプルコードでは、セマフォを使ってリソースの同時アクセス数を制限しています。

この方法を利用すると、リソースへのアクセスを効果的に制御し、競合状態を避けることができます。

実行すると、一度に最大3つのスレッドしかリソースを利用できないことを確認できます。

●マルチスレッドのカスタマイズ方法

Javaでのマルチスレッドプログラミングは高度な計算タスクや非同期処理を効率的に行えるよう設計されております。

そしてここでは、それを更にパーソナライズする方法に焦点を当てて解説します。

○スレッドの優先順位の設定

スレッドの優先順位設定は、マルチスレッド環境でのタスクの実行順序を制御するための重要な要素です。

Javaでは、スレッドの優先順位を1から10の範囲で設定できます。デフォルトの優先順位は5です。

下記のサンプルコードは、優先順位の設定とその効果を表すものとなります。

○サンプルコード9:スレッドの優先度を変更する

下記のサンプルコードはスレッドの優先順位を設定し、それによってスレッドがどのように実行されるかを観察するものです。

ここでは3つのスレッドを作成し、それぞれ異なる優先順位を設定しています。

public class ThreadPriorityExample {
    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            for(int i = 0; i < 1000; i++) {
                System.out.println("スレッド1: " + i);
            }
        });
        Thread thread2 = new Thread(() -> {
            for(int i = 0; i < 1000; i++) {
                System.out.println("スレッド2: " + i);
            }
        });
        Thread thread3 = new Thread(() -> {
            for(int i = 0; i < 1000; i++) {
                System.out.println("スレッド3: " + i);
            }
        });

        thread1.setPriority(1);
        thread2.setPriority(5);
        thread3.setPriority(10);

        thread1.start();
        thread2.start();
        thread3.start();
    }
}

このコードは、三つのスレッドを作成し、それぞれに異なる優先順位を設定しています。

スレッド3が最も高い優先順位を持っているため、他のスレッドよりも先に完了する可能性が高いです。

それに対して、スレッド1は最も低い優先順位を持っているので、他のスレッドよりも後に完了する可能性が高いです。

○カスタムスレッドプールの作成

カスタムスレッドプールの作成は、プログラムの性能を最適化するための有効な方法です。

ThreadPoolExecutorクラスを利用することで、スレッドプールの動作を細かく制御することができます。

○サンプルコード10:特定の条件下でのスレッドプールの動作

下記のサンプルコードは、特定の条件下でのスレッドプールの動作を表しています。

このコードでは、固定サイズのスレッドプールを作成し、それにタスクを投げる例を表しています。

import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;

public class CustomThreadPoolExample {
    public static void main(String[] args) {
        ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(4);

        for (int i = 0; i < 10; i++) {
            int finalI = i;
            executor.execute(() -> {
                System.out.println("タスク" + finalI + "が実行されました。");
            });
        }

        executor.shutdown();
    }
}

このコードは、4つのスレッドを持つ固定サイズのスレッドプールを作成し、10のタスクをそのプールに送信します。

スレッドプールはタスクの送信が終わった後にシャットダウンされます。

このコードを実行すると、タスクが並行して実行され、すべてのタスクが終了するとスレッドプールはシャットダウンします。

まとめ

Javaのマルチスレッドプログラミングは、効率的なプログラムの実行を実現できる素晴らしい方法です。

この記事を通じて、Javaのマルチスレッドプログラミングの概念や基本的な使い方、詳細な使い方、そして応用例や注意点を理解していただければ幸いです。

そして、これからもJavaのマルチスレッドプログラミングを楽しみながら学んでいくことを心より願っています。

どうぞ、今後のJavaプログラミングの学習や実務での活用に役立ててください。

また、何か疑問点や不明点がありましたら、気軽にお問い合わせから質問していただければと思います。

実行結果とその解説が手助けとなり、Javaプログラミングのスキルアップに寄与することを期待しています。