Java

Javaスレッド入門|買物プログラムで学ぶ並行処理の基礎

勉強ちゃん

Java学習のステップとして、前回までは「入力」「ファイル読み書き」「クラス化」などを取り上げてきました。
今回は、より実用的なアプリケーション開発に欠かせない 「スレッド(並行処理)」 について解説していきます。


スレッドとは

スレッドとは、プログラムの処理を「並行して実行する仕組み」です。
通常のJavaプログラムは 1つのメインスレッド 上で順番に実行されますが、スレッドを使うことで 複数の処理を同時に進める ことができます。

これらを同時進行できると、アプリの動作がスムーズになります。


なぜスレッドが必要?

スレッドを導入するメリットは大きく4つあります。

  1. 効率化:複数な処理を同時並行で進められます。
  2. 応答性向上:複雑な処理結果を待つ間に、別の処理を進められます。
  3. スケーラビリティ:マルチコアCPUを活用して高速化できます。

スレッドの種類

Javaでスレッドを扱う方法はいくつかあります。

1. Threadクラスを継承する方法

Threadクラスを拡張し、run() メソッドをオーバーライドするシンプルな方法です。

class TestThread extends Thread {
    public void run() {
        System.out.println("スレッドが実行されました: " + Thread.currentThread().getName());
    }
}

public class Main {
    public static void main(String[] args) {
        TestThread testThread = new TestThread();
        testThread.start();  // 新しいスレッドを開始
    }
}

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

クラスの多重継承ができないJavaでは、こちらの方がよく使われます。

class TestRunnable implements Runnable {
    public void run() {
        System.out.println("Runnableを使ったスレッド実行: " + Thread.currentThread().getName());
    }
}

public class Main {
    public static void main(String[] args) {
        Thread thread = new Thread(new TestRunnable());
        thread.start();
    }
}

3. 匿名クラスやラムダ式を使う方法(モダンJava)

コードを短く書きたい場合、匿名クラスやラムダ式でスレッドを生成できます。

public class Main {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            System.out.println("ラムダ式でスレッド実行");
        });
        thread.start();
    }
}

買物プログラムにスレッドを

前回の買い物アプリの主な処理は次の4つでした。

  1. 価格表の読み込み(ファイル→Map)
  2. ユーザー入力(商品名・数量)
  3. 計算(小計・合計)
  4. ファイル出力(明細+合計)

このうち (2) 入力 と (3) 計算+(4) 出力 を 別スレッド に分け、
安全なキューでデータを受け渡し(プロデューサ/コンシューマ)します。

設計の要点

  • 入力スレッド:ユーザーの入力を読み取り、キューへ投入
  • 計算スレッド:キューから取り出し計算→最後にまとめて書き出し
  • データの受け渡しは BlockingQueue(待機と通知を自動でやってくれる安全なキュー)
  • キューの終了合図は ポイズンピル(特別な終了メッセージ)で明確化

1 価格表の読み込みをクラスはそのまま利用(ProductLoader)

class ProductLoader {
    // ファイル形式例: リンゴ,100 のような「商品名,単価」
    public Map<String, Integer> loadPrices(String fileName) throws IOException {
        Map<String, Integer> priceMap = new HashMap<>();
        try (BufferedReader br = new BufferedReader(new FileReader(fileName))) {
            String line;
            int row = 0;
            while ((line = br.readLine()) != null) {
                row++;
                if (line.isBlank()) continue;
                String[] data = line.split(",");
                if (data.length != 2) {
                    throw new IOException("価格ファイルの形式エラー(" + row + "行目): " + line);
                }
                String name = data[0].trim();
                int price = Integer.parseInt(data[1].trim());
                priceMap.put(name, price);
            }
        }
        return priceMap;
    }
}

2 並行処理の「パイプ」を作る(PurchaseCommand)

class PurchaseCommand {
    public static final PurchaseCommand POISON = new PurchaseCommand("__END__", 0);
    private final String productName;
    private final int quantity;
    public PurchaseCommand(String productName, int quantity) {
        this.productName = productName; this.quantity = quantity;
    }
    public String getProductName() { return productName; }
    public int getQuantity() { return quantity; }
}
  • BlockingQueue は put()(空きを待つ)と take()(データを待つ)で、待ち合わせを安全にやってくれます。
  • 終了時は POISON をキューに入れて、コンシューマ側に「終わり」を伝えます。

3 入力スレッド(InputProducer)

class InputProducer implements Runnable {
    private final BlockingQueue<PurchaseCommand> queue;
    private final Map<String, Integer> priceMap;

    public InputProducer(BlockingQueue<PurchaseCommand> queue, Map<String, Integer> priceMap) {
        this.queue = queue; this.priceMap = priceMap;
    }

    @Override
    public void run() {
        try (Scanner sc = new Scanner(System.in)){
            while (true) {
                System.out.println("商品名を入力してください(終了は end):");
                String name = sc.nextLine().trim();
                if ("end".equalsIgnoreCase(name)) {
                    queue.put(PurchaseCommand.POISON);
                    break;
                }
                if (!priceMap.containsKey(name)) {
                    System.out.println("指定された商品は存在しません。");
                    continue;
                }
                System.out.println("数量を入力してください: ");
                while (!sc.hasNextInt()) {
                    System.out.println("整数で入力してください: ");
                    sc.next();
                }
                int q = sc.nextInt();
                sc.nextLine(); 
                queue.put(new PurchaseCommand(name, q));
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

4 計算&保存スレッド(PurchaseConsumer)

class PurchaseConsumer implements Runnable {
    private final BlockingQueue<PurchaseCommand> queue;
    private final Map<String, Integer> priceMap;
    private final ResultWriter writer;
    private final String outputFile;

    private final List<Purchase> lines = new ArrayList<>();
    private final AtomicInteger total = new AtomicInteger(0);

    public PurchaseConsumer(BlockingQueue<PurchaseCommand> queue,
                            Map<String, Integer> priceMap,
                            ResultWriter writer,
                            String outputFile) {
        this.queue = queue;
        this.priceMap = priceMap;
        this.writer = writer;
        this.outputFile = outputFile;
    }

    @Override
    public void run() {
        try {
            while (true) {
                PurchaseCommand cmd = queue.take(); // データ到着まで待機
                if (cmd == PurchaseCommand.POISON) break;

                int unitPrice = priceMap.get(cmd.getProductName());
                Item item = new Item(cmd.getProductName(), unitPrice);
                Purchase line = new Purchase(item, cmd.getQuantity());
                lines.add(line);
                total.addAndGet(line.getSubtotal());
            }

            // 全投入完了後にまとめて出力
            writer.write(outputFile, lines, total.get());

        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } catch (IOException io) {
            System.out.println("書き込みエラー: " + io.getMessage());
        }
    }
}
  • 合計や明細は別のクラスで利用していない(=他スレッドと共有しない)ので同期不要
  • 合計はサンプルとして AtomicInteger を使っていますが、この例では単一スレッドだけが更新するため必須ではありません(実戦では並列更新時に有効)。

5 結果の書き込み(I/O)

class ResultWriter {
    public void write(String fileName, List<Purchase> lines, int total) throws IOException {
        try (BufferedWriter bw = new BufferedWriter(new FileWriter(fileName, true))) {
            for (Purchase line : lines) {
                bw.write(line.toLine());
                bw.newLine();
            }
            bw.write("合計金額:" + total);
            bw.newLine();
        }
    }
}

6 全体の流れ:Main

import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.List;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        try {
            // 価格表読み込み
            ProductLoader loader = new ProductLoader();
            Map<String, Integer> priceMap = loader.loadPrices("productUnitPrice");

            // 並行パイプ(安全なキュー)
            BlockingQueue<PurchaseCommand> queue = new LinkedBlockingQueue<>();

            // スレッド起動
            Thread producer = new Thread(new InputProducer(queue, priceMap), "InputProducer");
            Thread consumer = new Thread(new PurchaseConsumer(queue, priceMap, new ResultWriter(), "productInfo"),
                                         "PurchaseConsumer");
            producer.start();
            consumer.start();

            // 終了待ち
            producer.join();
            consumer.join();

            System.out.println("計算結果を productInfo に保存しました。");
        } catch (Exception e) {
            System.out.println("エラー: " + e.getMessage());
        }
    }
}

実行の流れ
入力スレッドがコマンド(商品名・数量)をキューに積む → 計算スレッドが取り出して小計・合計を更新 → 最後にまとめてファイル出力します。

まとめ

  • Java スレッド/並行処理は、I/O と計算を重ねて効率化できます。
  • 買い物アプリを「入力→キュー→計算&保存」のプロデューサ/コンシューマに分割すると安全で拡張もしやすいです。

次回

次は JFrame(GUI)入門。今回の並行処理の発想を活かし、UI スレッドワーカースレッドを分けて、安全に画面更新する基本を学びます。

ABOUT ME
自己紹介
自己紹介
職業:Web開発エンジニア
こんにちは!
このブログでは、ITのお仕事で学んだ知識や、 日本での生活で学んだ知識を紹介しています。
お役に立てればうれしいです 😊
記事URLをコピーしました