1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Javaにおけるスレッドセーフの基礎と実装手法

Last updated at Posted at 2024-12-26

こんな人におすすめ

  • スレッドセーフが何か体系的かつ簡単に理解したい
  • Javaにおけるスレッドセーフの実現手段を知りたい

スレッドセーフとは何か

複数の同じ処理が走っても期待通りプログラムが動くように保証する性質のこと

  • 例) : 内部の数値が加算されるシステムにて、100万回非同期で呼び出して100万加算されることを保証する
    • スレッドセーフではない場合 : 結果が99万など一部加算されていない
    • スレッドセーフな場合 : 結果が100万となっている

複数のスレッドが同じリソースにアクセスすると、データの不整合やらが発生するよ

Javaシステムにおいてスレッドセーフは何を保証するのか

  • データの一貫性
  • 期待通りの動作
  • デッドロックや競合状態の回避 ..etc

Javaでスレッドセーフを実現するための実装

  • 同期処理にする
    • synchronized
    • Lock
    • Phases
      • フェーズという段階的な処理のステップを設けてフェーズごとにスレッドを同期させる
  • スレッドセーフなクラス
    • ConcurrentHashMap
    • AtomicInteger
  • 競合の発生しないイミュータブルな型宣言
    • String
    • Integer
  • スレッドごとに独立した値を持つ (スレッドローカル)
    • ThreadLocal

参考までにAIに使い分けを比較してもらった

手法 特徴 利点 注意点 主な用途
同期処理にする (synchronized, Lock) - 複数スレッドの同時アクセスを制限。
- synchronizedは簡潔で自動解放、Lockは柔軟な制御が可能。
- 容易に競合を回避。
- 柔軟な排他制御が可能(特にLock)。
- ロック待ちでデッドロック発生の可能性あり。
- パフォーマンス低下(特に高負荷環境)。
- 共有リソースの保護
- データの整合性確保
スレッドセーフなクラス (ConcurrentHashMap, AtomicInteger) - 内部で同期制御を行いスレッドセーフを保証。
- AtomicIntegerは単純な操作向け。
- 非同期アクセスが可能なためパフォーマンスが高い(特にConcurrent系)。
- 実装が簡単で確実。
- 高度な制御が必要な場合は対応できないこともある。 - マルチスレッド環境下での共有データの管理
イミュータブルな型 (String, Integer) - オブジェクトの状態が変化しないため競合が発生しない。
- 共有可能。
- 競合回避が不要。
- 実装がシンプルで安全。
- 再代入時に新しいインスタンスを生成するため、メモリ消費が増加する可能性あり。 - 共有データの定数化
- スレッドセーフなキャッシュの構築
スレッドローカル (ThreadLocal) - 各スレッドが独立したデータを保持。
- 他スレッドからアクセス不可。
- スレッドごとのデータ管理が簡単。
- 排他制御が不要。
- メモリリークのリスク(remove()の呼び忘れ)。
- 再利用スレッドで意図しないデータ残存の可能性。
- トランザクション情報
- スレッド単位の状態管理(例: リクエストコンテキストの保持)

スレッドセーフを考慮しない場合のリスク

データの競合が発生する

  • 概要 : 複数のスレッドが同じリソースにアクセス変更を加えることで、変更が競合する
  • 具体例 :
    • 100万のカウントアップにて同時にリソースへアクセス
    • 99万回目のカウントアップを複数スレッドが実行
    • 結果100万回のカウントアップで100万という数値にならない

DeadLockによって処理が停滞する

  • 概要 : 複数のスレッドが互いにロックしているリソースに対してロック要求を行い永遠に待機状態となる
  • 具体例 :
    • ThreadA -> リソースXをロック
    • ThreadB -> リソースYをロック
    • ThreadA -> リソースYのロック要求 -> ThreadBがロックしているため待機
    • ThreadB -> リソースXのロック要求 -> ThreadAがロックしているため待機

結果無限の待ち状態となる

ライブロックが発生する

  • 概要 : お互いにロックの解除とロックの同期タイミングが残念な一致をすることで、お互いにリソースを譲り合い続ける
  • 具体例 :
    • ThreadA -> リソースXをロック
    • ThreadB -> リソースYをロック
    • ThreadA -> リソースYに対してロックを要求 -> 失敗(リソースYはThreadBが保持)
    • ThreadA -> リソースXをアンロックし -> 再度リソースYのロックを試みる
    • ThreadB -> リソースXをロックしようとする -> 失敗(リソースXはThreadAが保持)
    • ThreadB はリソースYをアンロック -> 再度リソースXのロックを試みる
    • ThreadA と ThreadB はそれぞれリソースを取得しようとして、無限に譲り合い続け、進展しない状態が続く

次のリソースをロックする際は現在のリソースを解放すると回避できる可能性がある

スターベーション

  • 概要 :
    • 一部のスレッドが他のスレッドにリソースを奪われ続け、必要な処理が進まない状態
  • 具体例 :
    • 高い優先度を持つスレッドがリソースを占有し続け、低優先度のスレッドが実行されない

synchronized による同期実行制御

10000回のカウントを1000スレッドで実行する
カウント処理が正しければ 10000000 となるシナリオ
synchronized により Threadの処理を同期させ、修飾子のない非同期処理と比較する

メソッド単位で 同期/非同期を実装する

ThreadCounter.java
package com.example.demo.domain;

import org.springframework.stereotype.Component;

@Component
public class ThreadCounter {
    // アクセス対象のカウント変数
    int count = 0;

    // 同期させたいカウントアップ処理
    public synchronized void syncIncrement(){
        this.count++;
    }

    // 同期させたいカウント取得処理
    public synchronized int syncGetCount(){
        return this.count;
    }
    //検証のたびにカウントをリセットする
    public void init(){
        this.count = 0;
    }
    // 同期させたくないカウントアップ処理
    public void notSyncIncrement(){
        this.count++;
    }

    // 同期させたくないカウント取得処理
    public int notSyncGetCount(){
        return this.count;
    }
}

同期のThread処理を行うHelperクラス

ThreadCounterSyncHelper.java
package com.example.demo.helper;
import com.example.demo.domain.ThreadCounter;

public class ThreadCounterSyncHelper extends Thread {
    // counterオブジェクト
    private final ThreadCounter threadCounter;
    public ThreadCounterSyncHelper(ThreadCounter threadCounter){
        this.threadCounter = threadCounter;
    }
    // Threadで処理させたいcounter加算メソッド
    public void run(){
        for(int i=0;i<10000;i++){
            threadCounter.syncIncrement();
        }
    }
}

非同期のThread処理を行うHelperクラス

ThreadCounterNotSyncHelper.java
package com.example.demo.helper;
import com.example.demo.domain.ThreadCounter;

public class ThreadCounterNotSyncHelper extends Thread {
    // counterオブジェクト
    private final ThreadCounter threadCounter;
    public ThreadCounterNotSyncHelper(ThreadCounter threadCounter){
        this.threadCounter = threadCounter;
    }
    // Threadで処理させたいcounter加算メソッド
    public void run(){
        for(int i=0;i<10000;i++){
            threadCounter.notSyncIncrement();
        }
    }
}

同期/非同期のThread処理を行い結果を表示するController

SyncController.java
package com.example.demo.web;

import com.example.demo.domain.ThreadCounter;
import com.example.demo.helper.ThreadCounterNotSyncHelper;
import com.example.demo.helper.ThreadCounterSyncHelper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Controller
@RequestMapping("/sync/")
public class SyncController {
    @Autowired
    ThreadCounter threadCounter;
    /*
    * スレッドセーフの場合で数値加算をする
    * */
    @GetMapping("thread-safe")
    private String threadSafe(Model model){
        int count = 0;
        // カウントをリセットする
        threadCounter.init();
        // 100 Thread を同期して実行する
        for(int i=0;i<1000;i++){
            ThreadCounterSyncHelper threadCounterHelper = new ThreadCounterSyncHelper(threadCounter);
            threadCounterHelper.start();
        }
        try{
            // 5秒まつ
            Thread.sleep(5000);
            // 結果の取得
            count = threadCounter.syncGetCount();
        }catch(InterruptedException e){
            e.printStackTrace();
        }
        // 結果表示
        model.addAttribute("count",count);
        return "/thread-counter";
    }

    /*
     * スレッドセーフをしない場合で数値加算をする
     * */
    @GetMapping("thread-no-safe")
    private String threadNoSafe(Model model){
        int count = 0;
        // カウントをリセットする
        threadCounter.init();
        // 100 Thread を非同期で実行する
        for(int i=0;i<1000;i++){
            ThreadCounterNotSyncHelper threadCounterHelper = new ThreadCounterNotSyncHelper(threadCounter);
            threadCounterHelper.start();
        }
        try{
            // 5秒まつ
            Thread.sleep(5000);
            // 結果の取得
            count = threadCounter.notSyncGetCount();
        }catch(InterruptedException e){
            e.printStackTrace();
        }
        // 結果表示
        model.addAttribute("count",count);
        return "/thread-counter";
    }
}

結果の表示

thread-counter.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
	<p>thread count<p>
	<p th:text="${count}">count<p>
</body>
</html>

検証結果

同期処理

image.png

非同期処理

image.png

共通リソースに対してスレッドセーフとなっておらず変更が競合しており、
期待する結果が保証されていない

まとめ

  • 一言でThreadSafeと言っても様々なユースケースがある
  • ライブラリを使うことでスレッドセーフになるのではなく、ライブラリを利用し適切にロック制御をすることでThreadSafeが実現できる
  • ユースケースに応じて適切にライブラリを選定し、用途に応じてロック制御を調整する必要がある
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?