Javaにおけるスレッドの基本:並行処理の導入

docs

今回は、ちょっとレベルアップして「並行処理(Concurrency)」という考え方と、それを実現するための「スレッド(Thread)」について見ていこう。

「並行処理」って聞くと難しそうに感じるかもしれないけど、簡単に言えば「複数の処理を同時に進めること」だよ。僕たちが普段使っているパソコンやスマホも、裏ではたくさんの処理を同時にこなしているんだ。例えば、音楽を聴きながらWebブラウジングしたり、ファイルをダウンロードしながらゲームをしたり。これら全部、並行処理のおかげなんだよ。

なんで並行処理が必要なの?

昔のコンピューターは、一度に一つの処理しかできなかった。でも、これだとすごく効率が悪いよね。もしインターネットブラウザで画像がたくさんあるページを開いた時、画像を一つずつしか表示できなかったら、待っている間にイライラしちゃう。

そこで登場するのが「並行処理」だよ。複数の処理を同時に進めることで、コンピューターのCPUを有効活用し、プログラム全体の実行効率を上げたり、ユーザーの待ち時間を減らしたりできるんだ。

スレッドって何?

Javaで並行処理を実現するための最も基本的な仕組みが「スレッド」だよ。スレッドは、プログラムの中で「処理の実行単位」と考えると分かりやすいかな。一つのプログラムは、デフォルトで一つのメインスレッド(主となる処理の流れ)を持っているんだけど、必要に応じて複数のスレッドを生成して、それぞれのスレッドに異なる処理を実行させることができるんだ。

例えば、ラーメン屋さんを想像してみて。

  • メインスレッド: 店長さんがお客さんの注文を受けたり、レジを担当したりする(全体の管理)
  • スレッド1: 麺を茹でる担当
  • スレッド2: スープを作る担当
  • スレッド3: 具材を盛り付ける担当

もし店長さん一人で全部やっていたら、時間がかかってお客さんを待たせちゃうよね。でも、役割分担して同時に作業を進めれば、効率よくラーメンを提供できる。これがスレッドのイメージに近いんだ。

スレッドの作り方と動かし方

Javaでスレッドを使うには、主に2つの方法があるよ。

1. Threadクラスを継承する

一つ目は、java.lang.Threadクラスを継承して、run()メソッドをオーバーライドする方法。run()メソッドの中に、そのスレッドで実行したい処理を書くんだ。

Java

class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + ": " + i);
            try {
                Thread.sleep(100); // ちょっとだけ処理を停止(ミリ秒)
            } catch (InterruptedException e) {
                System.out.println(Thread.currentThread().getName() + "が中断されました。");
            }
        }
    }
}

public class ThreadExample1 {
    public static void main(String[] args) {
        MyThread thread1 = new MyThread();
        thread1.setName("スレッドA"); // スレッドに名前を付ける
        MyThread thread2 = new MyThread();
        thread2.setName("スレッドB");

        thread1.start(); // スレッドを開始!
        thread2.start(); // スレッドを開始!

        System.out.println("メインスレッドの処理が終了しました。");
    }
}

このコードを実行すると、スレッドAスレッドB、そしてメインスレッドが同時に動いているのがわかるはずだよ。出力順は実行するたびに変わる可能性があるんだけど、それが「並行処理」の面白いところだね。

  • start()メソッドを呼び出すことで、新しいスレッドが生成され、そのスレッド内でrun()メソッドが実行されるんだ。run()メソッドを直接呼び出すだけだと、新しいスレッドは作られずに、現在のスレッド(この場合はメインスレッド)でrun()メソッドの中身が実行されてしまうから注意してね。
  • Thread.sleep()は、指定した時間だけスレッドの実行を一時停止させるメソッドだよ。これを使うと、より並行処理っぽく見えるようになるね。

2. Runnableインターフェースを実装する

もう一つは、java.lang.Runnableインターフェースを実装する方法。この方法の方が、Javaでは推奨されているんだ。なぜかというと、Javaでは多重継承ができないから、すでに別のクラスを継承している場合にThreadクラスを継承できないけど、インターフェースならいくつでも実装できるからね。

Java

class MyRunnable implements Runnable {
    private String threadName;

    public MyRunnable(String name) {
        this.threadName = name;
    }

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(threadName + ": " + i);
            try {
                Thread.sleep(150);
            } catch (InterruptedException e) {
                System.out.println(threadName + "が中断されました。");
            }
        }
    }
}

public class ThreadExample2 {
    public static void main(String[] args) {
        Thread thread1 = new Thread(new MyRunnable("タスクX"));
        Thread thread2 = new Thread(new MyRunnable("タスクY"));

        thread1.start();
        thread2.start();

        System.out.println("メイン処理が終わったよ。");
    }
}

Runnableインターフェースを実装したクラスのインスタンスをThreadクラスのコンストラクタに渡して、Threadオブジェクトを生成するんだ。あとは同じようにstart()メソッドを呼べばOK。

スレッドのライフサイクル

スレッドには、生まれてから終わるまでの「ライフサイクル」があるんだ。

  • New(新規): new Thread()でスレッドが作られたばかりの状態。まだstart()は呼ばれてないよ。
  • Runnable(実行可能): start()が呼ばれて、CPUが使える状態になった。いつ実行されるかはOSやJVM(Java Virtual Machine)が決めるよ。
  • Running(実行中): 実際にCPUが割り当てられて、処理が実行されている状態。
  • Blocked/Waiting/Timed Waiting(ブロック/待機/時間指定待機): 何らかの理由で一時的に実行が停止している状態。例えば、I/O(ファイルの読み書きなど)が終わるのを待っていたり、sleep()で寝ていたりする時だね。
  • Terminated(終了): run()メソッドの処理が全て終わり、スレッドの実行が終了した状態。一度終了したスレッドは、再度開始することはできないから注意してね。

並行処理の注意点:同期(Synchronization)

複数のスレッドが同時に同じデータにアクセスしようとすると、問題が発生することがあるんだ。これを「競合状態(Race Condition)」って呼ぶよ。

例えば、銀行口座の残高を考えてみて。

  • スレッドAが「残高を100円減らす」処理
  • スレッドBが「残高を50円増やす」処理

これらが同時に行われた時に、もし同期が取れていないと、最終的な残高が意図しない値になってしまう可能性があるんだ。

これを防ぐために「同期(Synchronization)」という仕組みを使うよ。Javaでは、synchronizedキーワードを使って、ある処理を複数のスレッドが同時に実行できないようにロックすることができるんだ。

Java

class Counter {
    private int count = 0;

    // synchronized をつけることで、このメソッドは一度に一つのスレッドしか実行できなくなる
    public synchronized void increment() {
        count++;
        System.out.println(Thread.currentThread().getName() + ": " + count);
    }

    public int getCount() {
        return count;
    }
}

class CounterRunnable implements Runnable {
    private Counter counter;

    public CounterRunnable(Counter counter) {
        this.counter = counter;
    }

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            counter.increment();
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class SyncExample {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Thread thread1 = new Thread(new CounterRunnable(counter), "カウントスレッド1");
        Thread thread2 = new Thread(new CounterRunnable(counter), "カウントスレッド2");

        thread1.start();
        thread2.start();

        // 両方のスレッドが終わるのを待つ
        thread1.join();
        thread2.join();

        System.out.println("最終的なカウント: " + counter.getCount());
    }
}

increment()メソッドにsynchronizedキーワードを付けることで、このメソッドが実行されている間は他のスレッドがincrement()メソッドを実行できないようにロックされるんだ。これにより、複数スレッドからの同時アクセスによるデータの不整合を防ぐことができるよ。

  • join()メソッドは、そのスレッドが終了するまで現在のスレッド(この場合はメインスレッド)の実行を待機させるものだよ。

同期は並行処理の肝なんだけど、使いすぎるとスレッドの並行性が失われて、逆にパフォーマンスが悪くなることもあるから、適切な場面で使うことが重要だよ。

まとめ

今回はJavaの「スレッド」と「並行処理」の基本について学んだね。

  • 並行処理: 複数の処理を同時に進めること。
  • スレッド: プログラムの処理の実行単位。
  • スレッドの作り方にはThreadクラスの継承とRunnableインターフェースの実装がある。Runnableの方が推奨されることが多いよ。
  • スレッドにはライフサイクルがある。
  • 複数スレッドが同じデータにアクセスする際には「同期」が必要になることがある。

並行処理は、Javaプログラミングの中でも少し難しいトピックだけど、現代のアプリケーション開発には欠かせない技術なんだ。今回は入門編だったけど、もっと奥が深い世界だから、ぜひこれからも学習を続けてみてね。


次の記事では、さらに高度な並行処理の概念や、スレッドプールなどの便利な機能について解説していく予定だよ。お楽しみに!

これまでの記事はこちらから確認できるよ! ToolDocs | 様々なツールや技術情報を紹介します

コメント

タイトルとURLをコピーしました