Skip to content

Part I: 並行処理の基礎

概要

本章では、並行処理を学ぶ前提として逐次処理(Sequential Processing)の基本を理解します。パスワードクラッキングを例に、逐次処理の特徴と限界を確認します。


第2章: 逐次処理

逐次処理とは

逐次処理は、タスクを一つずつ順番に実行する最も基本的な処理方式です。

uml diagram

パスワードクラッキングの例

ブルートフォース(総当たり)でパスワードを解読するプログラムを通じて、逐次処理の特徴を学びます。

実装の全体像

ソースファイル: apps/java/src/main/java/concurrency/ch02/PasswordCracker.java

package concurrency.ch02;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HexFormat;
import java.util.List;

/**
 * パスワードクラッカー(逐次処理版)
 * ブルートフォース(総当たり)でパスワードを解読する
 */
public class PasswordCracker {

    public static List<String> getCombinations(int length, int minNumber, int maxNumber) {
        List<String> combinations = new ArrayList<>();
        String format = "%0" + length + "d";

        for (int i = minNumber; i <= maxNumber; i++) {
            combinations.add(String.format(format, i));
        }
        return combinations;
    }

    public static String getCryptoHash(String password) {
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            byte[] hash = digest.digest(password.getBytes(StandardCharsets.UTF_8));
            return HexFormat.of().formatHex(hash);
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("SHA-256 algorithm not available", e);
        }
    }

    public static boolean checkPassword(String expectedCryptoHash, String possiblePassword) {
        String actualCryptoHash = getCryptoHash(possiblePassword);
        return expectedCryptoHash.equals(actualCryptoHash);
    }

    public static String crackPassword(String cryptoHash, int length) {
        int maxNumber = (int) Math.pow(10, length) - 1;
        List<String> combinations = getCombinations(length, 0, maxNumber);

        for (String combination : combinations) {
            if (checkPassword(cryptoHash, combination)) {
                return combination;
            }
        }
        return null;
    }

    public static void main(String[] args) {
        String cryptoHash = "ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f";
        int length = 8;

        System.out.println("Processing number combinations sequentially");
        long startTime = System.nanoTime();

        String result = crackPassword(cryptoHash, length);

        long processTime = System.nanoTime() - startTime;
        if (result != null) {
            System.out.println("PASSWORD CRACKED: " + result);
        }
        System.out.printf("PROCESS TIME: %.3f seconds%n", processTime / 1_000_000_000.0);
    }
}

コードの解説

1. パスワード組み合わせの生成

public static List<String> getCombinations(int length, int minNumber, int maxNumber) {
    List<String> combinations = new ArrayList<>();
    String format = "%0" + length + "d";  // 先頭ゼロ埋めフォーマット

    for (int i = minNumber; i <= maxNumber; i++) {
        combinations.add(String.format(format, i));
    }
    return combinations;
}

8桁の数字パスワードの場合、00000000 から 99999999 まで 1億通りの組み合わせを生成します。

2. ハッシュ関数

public static String getCryptoHash(String password) {
    MessageDigest digest = MessageDigest.getInstance("SHA-256");
    byte[] hash = digest.digest(password.getBytes(StandardCharsets.UTF_8));
    return HexFormat.of().formatHex(hash);  // Java 17+ の HexFormat
}

SHA-256 を使用してパスワードをハッシュ化します。HexFormat は Java 17 以降で利用可能な便利なクラスです。

3. パスワードチェック

public static boolean checkPassword(String expectedCryptoHash, String possiblePassword) {
    String actualCryptoHash = getCryptoHash(possiblePassword);
    return expectedCryptoHash.equals(actualCryptoHash);
}

候補パスワードのハッシュと、保存されているハッシュを比較します。

パフォーマンス測定

long startTime = System.nanoTime();
// ... 処理 ...
long processTime = System.nanoTime() - startTime;
System.out.printf("PROCESS TIME: %.3f seconds%n", processTime / 1_000_000_000.0);

System.nanoTime() は高精度のパフォーマンスカウンターで、経過時間の測定に適しています。


TDD によるテスト

ソースファイル: apps/java/src/test/java/concurrency/ch02/PasswordCrackerTest.java

@Test
@DisplayName("4桁の組み合わせを生成できる")
void shouldGenerateFourDigitCombinations() {
    List<String> combinations = PasswordCracker.getCombinations(4, 0, 9);

    assertThat(combinations)
        .hasSize(10)
        .containsExactly(
            "0000", "0001", "0002", "0003", "0004",
            "0005", "0006", "0007", "0008", "0009"
        );
}

@Test
@DisplayName("SHA-256ハッシュを計算できる")
void shouldCalculateSha256Hash() {
    String hash = PasswordCracker.getCryptoHash("12345678");

    assertThat(hash)
        .isEqualTo("ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f");
}

@Test
@DisplayName("パスワードをクラックできる")
void shouldCrackPassword() {
    String cryptoHash = PasswordCracker.getCryptoHash("1234");

    String result = PasswordCracker.crackPassword(cryptoHash, 4);

    assertThat(result).isEqualTo("1234");
}

逐次処理の特徴

利点

利点 説明
シンプル 実装が簡単で理解しやすい
予測可能 実行順序が決定的
デバッグ容易 問題の特定が容易
競合なし 共有リソースの競合が発生しない

限界

限界 説明
遅い 1億通りを1つずつ処理するため時間がかかる
リソース非効率 マルチコア CPU を活用できない
スケールしない データ量に比例して処理時間が増加

なぜ並行処理が必要か

8桁のパスワードクラッキングでは、1億通りの組み合わせを順番にチェックします。

uml diagram

4コア CPU があっても、逐次処理では1コアしか使用されません。並行処理を使えば、複数コアに作業を分散できます。

uml diagram


実行方法

テストの実行

cd apps/java
./gradlew test

サンプルコードの実行

cd apps/java
./gradlew run

次のステップ

Part II では、スレッドとプロセスを使って並行処理を実装します。パスワードクラッキングを並列化し、処理時間を大幅に短縮する方法を学びます。


参考コード