Java超|15章のまとめ

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

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

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

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

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

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

ドラゴンボール風にたとえると、戦士本部の任務で、1人の戦士だけがすべてを順番にこなすのではなく、GokuWarrior、VegetaWarrior、GohanAnalyzer、BulmaSupport がそれぞれ別の役割を持って同時に動くような状態です。

GokuWarrior は重力修行を進める。
VegetaWarrior は戦闘フォームを確認する。
GohanAnalyzer は修行記録を分析する。
BulmaSupport は装置や回復カプセルを準備する。

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

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

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

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

15章は、スレッドの基本から、起動、制御、作り方の違い、共有データの安全な扱い方までを学ぶ章でした。

ここでは、15章で学んだ内容を、ドラゴンボールの世界観に置き換えながら整理していきます。

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

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

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

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

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

戦士本部で考えてみましょう。

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

修行場を準備
  ↓
重力装置を確認
  ↓
修行記録を整理
  ↓
回復カプセルを片付ける

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

main の流れ             :BulmaSupport が修行場を準備
GokuWarriorスレッド     :GokuWarrior が重力修行を進める
VegetaWarriorスレッド   :VegetaWarrior が戦闘フォームを確認する
GohanAnalyzerスレッド   :GohanAnalyzer が修行記録を分析する

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

ただし、スレッドを使ったからといって、必ず処理が単純になるわけではありません。

複数の流れがあるからこそ、実行順序や共有データへのアクセスに注意が必要になります。

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

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

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

さらにもう1つスレッドを起動すれば、流れはさらに増えます。

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

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

流れ内容
main スレッドBulmaSupport が本部の準備を進める
GokuWarrior スレッドGokuWarrior が重力修行を進める
VegetaWarrior スレッドVegetaWarrior が戦闘フォーム確認を進める

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

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

ドラゴンボール風に言えば、GokuWarrior が修行へ向かっても、VegetaWarrior がフォーム確認を始めても、本部の BulmaSupport は自分の準備を続けています。

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

この図が示していること

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

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

右側では、main スレッドに加えて、GokuWarrior スレッド、VegetaWarrior スレッド、GohanAnalyzer スレッドがそれぞれ別々に動いています。

図の要素意味
main最初から存在する処理の流れ
1本の矢印順番に進む基本的な処理
複数のレーン複数スレッドによる並行した処理
GokuWarriorスレッド重力修行を進める流れ
VegetaWarriorスレッド戦闘フォームを確認する流れ
GohanAnalyzerスレッド修行記録を分析する流れ

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

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

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

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

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

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

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

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

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

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

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

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

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

たとえば、GokuWarrior スレッドなら、GokuWarrior が重力修行を進める処理を run に書きます。

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

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

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

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

GokuWarrior の表示が先に出ることもあります。
VegetaWarrior の表示が先に出ることもあります。
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 を実行したスレッドだけだという点です。

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

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

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

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

ドラゴンボール風に言えば、GokuWarrior が一呼吸置いて気を整えていても、VegetaWarrior や BulmaSupport まで一緒に止まるわけではありません。

止まるのは、その場で気を整えている戦士だけです。

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

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

worker1.join();

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

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

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

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

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

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

ドラゴンボール風に言えば、BulmaSupport が GokuWarrior に「止まれ」と命じているのではありません。

BulmaSupport が「GokuWarrior の修行が終わるまで、こちらは最終確認を待とう」としている状態です。

sleep と join の違い

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

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

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

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

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

ドラゴンボール風に言えば、sleep は「一呼吸置く」ことです。

join は「仲間の任務完了を確認するまで待つ」ことです。

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

この図が示していること

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

WarriorTask task1 = new WarriorTask();
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()
    {
        // 記録整理の処理
    }
}

ドラゴンボール風に言えば、GohanAnalyzer がすでに「記録解析部隊の親クラス」を受け継いでいる場合でも、「別任務で動く約束」である Runnable は身につけられるということです。

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

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

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

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

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

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

GokuWarrior も VegetaWarrior も、その修行記録帳に自分の修行ポイントを加算します。

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

total = total + 50;

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

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

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

GokuWarriorスレッドVegetaWarriorスレッド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で排他制御を行うための指定

ドラゴンボール風に言えば、修行記録帳に青白い結界を張るようなものです。

GokuWarrior が書いている間は、VegetaWarrior は待つ。
GokuWarrior が書き終わったあと、VegetaWarrior が更新後の合計を見て書く。

このように、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のオブジェクト指向とつながっています。

ドラゴンボール風に整理すると、次のようになります。

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

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

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

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

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

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

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

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

戦士本部の任務で考えると、ただ「誰かが動いている」と見るだけでは不十分です。

今、GokuWarrior は何をしているのか。
VegetaWarrior はどの流れで動いているのか。
BulmaSupport は待っているのか。
GohanAnalyzer は共有の修行記録帳を安全に使えているのか。

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

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

15章で身についた力

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

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

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

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

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

ドラゴンボール風に言えば、強い戦士が増えるだけでは任務は成功しません。

GokuWarrior、VegetaWarrior、GohanAnalyzer、BulmaSupport がそれぞれ動きながら、必要なところで待ち、順番を守り、共通の記録を正しく扱うことで、任務全体が安全に進みます。

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

複数の流れを作る力と、それらを安全に協調させる力。

この2つがそろうことで、スレッドは本当の意味で実用的な技術として使えるようになります。