Java道|15章のまとめ

1本の処理から、仲間と連携して動く処理へ。
スレッドを学ぶと、Javaプログラムは一本道ではなく、複数の流れが協力して進む立体的な世界として見えてきます。

15章では、Javaプログラムを「1本の処理の流れ」だけで考える段階から、さらに一歩進みました。

これまでの基本的なプログラムでは、main メソッドから処理が始まり、上から下へ順番に実行される流れを中心に学んできました。

main の流れ
  ↓
処理1
  ↓
処理2
  ↓
処理3

この考え方は、Javaの基本を理解するうえでとても大切です。

しかし、15章で学んだスレッドを使うと、プログラムの中に複数の処理の流れを作れるようになります。

鬼滅の刃風にたとえると、鬼殺隊本部の任務で、1人の隊士だけがすべてを順番にこなすのではなく、水月、蒼真、記録係、支援隊士がそれぞれ別の役割を持って同時に動くような状態です。

水月は型の修行を進める。
蒼真は作戦の流れを整理する。
記録係は修行記録をまとめる。
支援隊士は道具や回復薬を準備する。

このように、複数の流れが同じプログラムの中で動くようになると、Javaの処理はかなり立体的に見えてきます。

ただし、仲間が増えればそれだけで安全に任務が進むわけではありません。

誰がいつ動くのか。
少し待つ必要があるのか。
仲間の終了を待つ必要があるのか。
同じ修行記録帳をどう安全に使うのか。

こうしたことまで考える必要があります。

15章は、スレッドの基本から、起動、制御、作り方の違い、共有データの安全な扱い方までを学ぶ章でした。ここでは、15章で学んだ内容を、鬼滅の刃風の世界観に置き換えながら整理していきます。

スレッドとは何だったのか

スレッドとは、プログラムの中にある処理の流れです。

これまでの基本的なJavaプログラムでは、main メソッドから始まる1本の流れを中心に考えてきました。

しかし、スレッドを使うと、main の流れとは別に、新しい処理の流れを作れます。

考え方鬼滅の刃風のイメージ処理の流れ
基本的なプログラム1人の隊士が順番に任務を進める1本
スレッドを使うプログラム複数の隊士が別々に任務を進める複数本

鬼殺隊本部で考えてみましょう。

1本の流れだけなら、支援隊士が次のように順番に作業します。

修行場を準備
  ↓
型の確認
  ↓
記録の整理
  ↓
道具の片付け

しかし、複数のスレッドを使うと、次のように分担できます。

main の流れ       :支援隊士が修行場を準備
水月スレッド      :水月が型の修行を進める
蒼真スレッド      :蒼真が作戦の流れを整理する
記録係スレッド    :修行記録をまとめる

このように、スレッドは「処理の担当者」を増やすような仕組みです。

ただし、スレッドを使ったからといって、必ず処理が単純になるわけではありません。
複数の流れがあるからこそ、実行順序や共有データへのアクセスに注意が必要になります。

スレッドは複数起動できる

15章で最初に押さえた大切な考え方は、スレッドは1つだけでなく複数起動できるということです。

1つのスレッドを起動すると、main とは別の流れが1本増えます。
さらにもう1つスレッドを起動すれば、流れはさらに増えます。

start の呼び出し増える流れ
start を1回呼ぶ新しいスレッドが1本増える
start を2回呼ぶ新しいスレッドが2本増える
main も動くmain スレッドも1本の流れとして残る

たとえば、水月スレッドと蒼真スレッドを起動すると、main を含めて3本の流れになります。

流れ内容
main スレッド支援隊士が本部の準備を進める
水月スレッド水月が型の修行を進める
蒼真スレッド蒼真が作戦確認を進める

ここで大切なのは、main が消えるわけではないことです。

新しいスレッドが動き出しても、main は main として処理を続けます。

鬼滅の刃風に言えば、水月が修行へ向かっても、蒼真が作戦確認を始めても、本部の支援隊士は自分の準備を続けています。

図:1本の流れから複数スレッドへ

↓クリックすると拡大表示されます。

この図が示していること

この図は、15章前半で学んだ「処理の流れを増やす」という考え方を表しています。

左側では、main の1本の流れだけで、すべての処理を順番に進めています。

右側では、main スレッドに加えて、水月スレッド、蒼真スレッド、記録係スレッドがそれぞれ別々に動いています。

図の要素意味
main最初から存在する処理の流れ
1本の矢印順番に進む基本的な処理
複数のレーン複数スレッドによる並行した処理
水月スレッド女性隊士が型の修行を進める流れ
蒼真スレッド男性隊士が作戦確認を進める流れ

この図から分かることは、スレッドは単に速くするための魔法ではなく、処理の担当を分けて、複数の流れとして動かす仕組みだということです。

Thread クラスを継承してスレッドを起動する

スレッドを作る基本的な方法として、Thread クラスを継承する方法を学びました。

基本形は次のようになります。

class DemonSlayerTask extends Thread
{
    public void run()
    {
        // 別スレッドで行いたい処理
    }
}

この形では、DemonSlayerTask クラス自身が Thread の機能を受け継ぎます。

そのため、DemonSlayerTask のオブジェクトに start を呼び出すことで、新しいスレッドを起動できます。

DemonSlayerTask task1 = new DemonSlayerTask();
task1.start();
要素役割鬼滅の刃風のイメージ
extends Threadスレッドとして動けるクラスにする別任務へ出られる隊士の型
run別スレッドで行う処理を書く任務内容を書いた作戦書
start新しいスレッドを起動する出撃命令

ここで特に大切なのは、run を直接呼ぶのではなく、start を呼ぶことで新しいスレッドが起動することです。

run を直接呼ぶと、普通のメソッド呼び出しになります。
新しい流れは生まれません。

一方、start を呼ぶと、新しい処理の流れが作られ、その流れの中で run が実行されます。

run メソッドは新しい流れの中身

run メソッドは、新しいスレッドで実行したい処理を書く場所です。

たとえば、水月スレッドなら、水月が型の修行を進める処理を run に書きます。

public void run()
{
    for(int i = 0; i < 5; i++) {
        System.out.println("水月が型の修行を進めています。");
    }
}

この run が、新しいスレッドの中身です。

流れ中身
main スレッド本部側の処理を進める
run のスレッド水月や蒼真が個別任務を進める

複数のスレッドが動くと、画面への表示順が毎回同じになるとは限りません。

水月の表示が先に出ることもあります。
蒼真の表示が先に出ることもあります。
main の表示が途中に入ることもあります。

これは異常ではありません。

複数の流れが独立して進んでいるため、どのスレッドが先に少し進むかは、実行タイミングによって変わります。

鬼滅の刃風に言えば、複数の隊士が別々の場所で動いているので、誰の報告が先に本部へ届くかは毎回同じとは限らない、ということです。

sleep でスレッドを一時停止する

15章では、スレッドを一定時間だけ一時停止する sleep も学びました。

Thread.sleep(1000);

1000 はミリ秒です。

つまり、Thread.sleep(1000) は、現在動いているスレッドを1秒間止めるという意味です。

書き方意味
Thread.sleep(1000)現在のスレッドを1秒止める
Thread.sleep(500)現在のスレッドを0.5秒止める
Thread.sleep(2000)現在のスレッドを2秒止める

ここで大切なのは、止まるのは sleep を実行したスレッドだけだという点です。

プログラム全体が止まるわけではありません。

たとえば、水月スレッドの run の中で sleep を使えば、水月スレッドが一時停止します。
main スレッドの中で Thread.sleep を使えば、main スレッドが一時停止します。

sleep を書く場所一時停止する流れ
run の中その run を実行しているスレッド
main の中main スレッド

鬼滅の刃風に言えば、水月が一呼吸置いて型を整えていても、蒼真や支援隊士まで一緒に止まるわけではありません。

止まるのは、その場で呼吸を整えている隊士だけです。

join で別スレッドの終了を待つ

sleep が「時間で待つ」処理なら、join は「相手の終了で待つ」処理です。

worker1.join();

これは、worker1 のスレッドが終わるまで、join を呼び出した側のスレッドが待つという意味です。

ここで大切なのは、join は相手を止める命令ではないことです。

join は、自分が相手の終了を待つ命令です。

メソッド何を待つか鬼滅の刃風のイメージ
sleep指定した時間一呼吸置いてから再開する
join別スレッドの終了仲間の任務完了を待ってから進む

たとえば、main の中で worker1.join() を実行した場合、待つのは main スレッドです。

worker1 は止められるわけではありません。
worker1 は自分の run を最後まで進めます。
main は worker1 が終わるまで待ちます。

鬼滅の刃風に言えば、支援隊士が水月に「止まれ」と命じているのではありません。

支援隊士が「水月の修行が終わるまで、こちらは最終確認を待とう」としている状態です。

sleep と join の違い

sleep と join は、どちらもスレッドの流れを調整するために使います。

しかし、待つ理由が違います。

メソッド待つ理由再開する条件
sleep指定時間だけ待つ指定時間が過ぎる
join指定したスレッドの終了を待つ相手のスレッドが終わる

この違いはとても重要です。

sleep は時間による待機です。
join は仲間の完了による待機です。

鬼滅の刃風に言えば、sleep は「一呼吸置く」ことです。
join は「仲間の任務完了を確認するまで待つ」ことです。

図:sleep と join でスレッドの流れを整える

↓クリックすると拡大表示されます。

この図が示していること

この図は、15章中盤で学んだ sleep と join の違いを表しています。

左側の sleep では、水月スレッドが Thread.sleep(1000) の場所で一時停止します。
止まるのは、sleep を実行している水月スレッド自身です。

右側の join では、main スレッドが蒼真スレッドの終了を待っています。
蒼真スレッドは止められているわけではなく、自分の run を最後まで進めます。

図の要素意味
sleep現在のスレッドを指定時間だけ止める
join指定したスレッドの終了を待つ
砂時計アイコン一時停止や待機
水月スレッドsleep で一呼吸置く処理
main スレッドjoin によって仲間の終了を待つ処理

この図から分かることは、sleep と join はどちらも待機に関係しますが、待つ理由と止まる側が違うということです。

スレッドの作成方法は1つではない

15章では、スレッドの作り方が Thread クラスの継承だけではないことも学びました。

もう1つの方法が、Runnable インターフェイスを実装する方法です。

方法書き方特徴
Thread クラスを継承するextends Thread分かりやすく、基本を理解しやすい
Runnable インターフェイスを実装するimplements Runnableほかのクラス継承と両立しやすい

Thread 継承では、クラスそのものがスレッドとして動けます。

class DemonSlayerTask extends Thread
{
    public void run()
    {
        // 別スレッドで行う処理
    }
}

この場合は、作成したオブジェクトに start を呼び出せます。

DemonSlayerTask task1 = new DemonSlayerTask();
task1.start();

一方、Runnable を使う場合は、Runnable 実装オブジェクトを Thread に渡して起動します。

class DemonSlayerTask implements Runnable
{
    public void run()
    {
        // 別スレッドで行う処理
    }
}

DemonSlayerTask task1 = new DemonSlayerTask();
Thread th1 = new Thread(task1);
th1.start();

Runnable 方式では、task1 自身に start を呼ぶのではありません。

start を呼ぶのは Thread オブジェクトである th1 です。

オブジェクト役割
Runnable 実装オブジェクト別スレッドで行う処理内容を持つ
Thread オブジェクトその処理内容を新しいスレッドとして起動する

鬼滅の刃風にたとえると、Runnable 実装オブジェクトは「任務内容が書かれた指令札」を持つ隊士です。

ただし、その指令札だけでは任務は始まりません。
Thread という出撃役に渡し、Thread に start を呼び出して、初めて新しい流れとして動き出します。

Runnable が必要になる理由

Runnable が重要なのは、Javaではクラスの多重継承ができないからです。

Javaでは、1つのクラスが同時に複数のクラスを継承することはできません。

状況Javaで可能か
1つのクラスを継承する可能
2つ以上のクラスを同時に継承する不可能
クラスを継承しつつインターフェイスを実装する可能

たとえば、あるクラスがすでに記録係の親クラスを継承している場合、さらに Thread クラスを継承することはできません。

しかし、Runnable インターフェイスなら実装できます。

class TrainingRecorder extends RecordKeeper implements Runnable
{
    public void run()
    {
        // 記録整理の処理
    }
}

鬼滅の刃風に言えば、水月がすでに「記録係の流派」を受け継いでいる場合でも、「別任務で動く約束」である Runnable は身につけられるということです。

このように、Runnable はクラス設計を柔軟にするために役立ちます。

複数スレッドでは共有データに注意が必要

15章の後半で特に重要だったのが、共有データの扱いです。

スレッドを複数動かすだけなら、処理の流れが増えるという理解で進められます。

しかし、複数のスレッドが同じデータを読み書きし始めると、値の食い違いが起こる可能性があります。

たとえば、鬼殺隊本部に共通の修行記録帳が1冊だけあるとします。

水月も蒼真も、その修行記録帳に自分の修行ポイントを加算します。

加算処理は、見た目には単純です。

total = total + 50;

しかし、考え方としては次の3段階に分かれます。

手順内容
1total の現在値を読む
2読んだ値に50を足す
3結果を total に書き戻す

この途中で別のスレッドが割り込むと、古い値を読んでしまい、片方の更新が消えたような結果になることがあります。

水月スレッド蒼真スレッドtotal
total を読む → 00
total を読む → 00
0 + 50 = 500
0 + 50 = 500
total に50を書く50
total に50を書く50

本来なら100になってほしい場面でも、結果が50になる可能性があります。

このような問題は、複数スレッドが同じ共有資源を同時に扱うことで起こります。

synchronized による同期が必要になる理由

共有データの矛盾を防ぐために使うのが synchronized です。

メソッドに synchronized を付けると、そのメソッドをあるスレッドが実行している間、ほかのスレッドは同じメソッドに入れなくなります。

public synchronized void addPoint(int p)
{
    int tmp = total;
    tmp = tmp + p;
    total = tmp;
}

これにより、total を読む、計算する、書き戻すという一連の処理が、途中で割り込まれにくくなります。

用語意味
同期スレッドどうしの処理タイミングを整える仕組み
排他制御同時に1つのスレッドしか入れないようにする仕組み
synchronizedJavaで排他制御を行うための指定

鬼滅の刃風に言えば、修行記録帳に青白い結界を張るようなものです。

水月が書いている間は、蒼真は待つ。
水月が書き終わったあと、蒼真が更新後の合計を見て書く。
このように、1人ずつ順番に記録することで、修行ポイントの合計を正しく保てます。

synchronized が守るものと守らないもの

synchronized を付けると、共有データを扱う処理を排他的に実行できます。

ただし、すべての実行順序を完全に固定するわけではありません。

synchronized が守ることsynchronized が固定しないこと
共有データを扱うメソッドへの同時侵入を防ぐスレッド全体の実行順序
読む、計算する、書き戻す流れを守るどちらのスレッドが先に動くか
更新の整合性を守る画面表示のすべての順番

つまり、synchronized は「共有データの正しさを守るための仕組み」です。

表示順を完全に固定するためのものではありません。

ここを混同しないことが大切です。

図:15章全体の学習内容

↓クリックすると拡大表示されます。

この図が示していること

この図は、15章で学んだ内容を1つの流れとして整理しています。

最初に、スレッドとは処理の流れを増やす仕組みだと学びました。

次に、Thread クラス、run、start を使って新しいスレッドを起動する方法を学びました。

その後、sleep で一時停止し、join で別スレッドの終了を待つ方法を学びました。

さらに、Runnable によってスレッドの作成方法が1つではないことを理解しました。

最後に、synchronized によって共有データを安全に扱う必要があることを学びました。

学習内容理解したこと
スレッドの基本処理の流れは複数持てる
Thread と run別スレッドで動く処理を書く
start新しいスレッドを起動する
sleep時間で待つ
join仲間の終了を待つ
Runnable処理内容と起動役を分ける
synchronized共有資源を守る

この図から分かることは、15章が「スレッドを起動して終わり」の章ではなかったということです。

スレッドを作る、増やす、止める、待つ、作り方を選ぶ、共有データを守る。
これらが1つの流れとしてつながっています。

15章で使われたオブジェクト指向の考え方

15章はスレッドの章ですが、その中でもオブジェクト指向の考え方が自然に使われていました。

スレッド用のクラスを作る。
そのクラスからオブジェクトを作る。
run や addPoint のようなメソッドを使う。
Thread を継承する。
Runnable を実装する。

これらはすべて、Javaのオブジェクト指向とつながっています。

鬼滅の刃風に整理すると、次のようになります。

オブジェクト指向の要素鬼滅の刃風のたとえ
クラス隊士の設計図
オブジェクト実際に任務へ出る水月や蒼真
フィールド隊士が持つ名前や修行ポイント
メソッド隊士が行う動作
継承もとの型を受け継いで新しい隊士クラスを作る
インターフェイス守るべき行動の約束

Thread クラスを継承する方法では、隊士クラスそのものが「別スレッドとして動ける性質」を受け継ぎます。

Runnable を実装する方法では、「別スレッドで行う処理内容」という約束を守る形になります。

このように、スレッドの学習は、単に並行処理だけでなく、クラス設計の考え方とも深く関係しています。

15章を学んで見えるようになること

15章を学ぶ前は、Javaプログラムは1本の一本道として見えやすかったはずです。

しかし、15章を学ぶと、次のような視点が加わります。

見えるようになること内容
main 以外の流れどこで新しいスレッドが生まれるか
run の役割別スレッドで何をするか
start の意味新しい流れを起動する場所
sleep の意味どのスレッドが時間で待つか
join の意味どのスレッドが誰の終了を待つか
Runnable の意味処理内容と起動役を分ける考え方
synchronized の意味共有データを守る必要性

鬼殺隊の任務で考えると、ただ「誰かが動いている」と見るだけでは不十分です。

今、水月は何をしているのか。
蒼真はどの流れで動いているのか。
支援隊士は待っているのか。
記録係は共有の修行記録帳を安全に使えているのか。

このように、複数の流れを見分け、タイミングを整理し、共有資源を守る視点が必要になります。

Javaのスレッドも同じです。

15章で身についた力

15章で身についた力を整理すると、次のようになります。

身についた力内容
流れを複数本で考える力プログラムを一本道だけで見ない
スレッドを作る力Thread 継承や Runnable 実装を理解する
スレッドを起動する力start で新しい処理の流れを作る
スレッドを制御する力sleep や join で動きを調整する
共有データを守る力synchronized で安全性を保つ
オブジェクト指向で整理する力クラス、オブジェクト、メソッドの役割を見分ける

15章は、Javaをより実践的に使うための大切な入口です。

スレッドを知ることで、時間のかかる処理を別の流れに分けたり、複数の処理を並行して進めたりできるようになります。

そして同時に、複数の流れがあるからこそ、待つ、終了を確認する、共有データを守る、といった考え方も必要になります。

鬼滅の刃風に言えば、強い隊士が増えるだけでは任務は成功しません。

水月、蒼真、記録係、支援隊士がそれぞれ動きながら、必要なところで待ち、順番を守り、共通の記録を正しく扱うことで、任務全体が安全に進みます。

Javaのスレッドも同じです。

複数の流れを作る力と、それらを安全に協調させる力。
この2つがそろうことで、スレッドは本当の意味で実用的な技術として使えるようになります。