【納得Java】ThreadLocalでスレッド毎に値を保持する方法

スレッドが2つ以上複数あり同時進行で処理されることを「マルチスレッド」といいます。

「マルチスレッド」で複数同時並行で処理する場合に、別のスレッドから意図せずに値が変更できるとしたら、これってとても恐いことですよね。

例えば、アマゾンのようなショッピングサイトで自分の買い物データガ他人の買い物データで変更されるなんてことが起きてしまったらショッピングなんてできなくなります。

そんなことにならないために使われるのがThreadLocalクラスです。

この記事では、そんなThreadLocalクラスについて以下の内容で解説していきます。

  • ThreadLocalクラスとは
  • スレッド毎に値を保持する方法
  • ThreadLocalクラスの注意点

今回はThreadLocalクラスについて、使い方をわかりやすく解説します!

なお、Javaの記事については、こちらにまとめています。

目次

ThreadLocalクラスとは

ThreadLocalクラスについて解説する前に、Javaの変数とそのメモリ管理について解説しておきましょう。

Javaでは「クラス変数」は共有メモリ領域に保存されます。この領域を「ヒープ領域」といいます。

ちなみに、「クラス変数」とはクラス内の静的な(static)メンバ変数のことです。この領域は複数のスレッドで共有することができて、他のスレッドによって書き換えることができます。

これに対してプリミティブ型のローカル変数はスレッドごとの固有のスタック領域に格納されます。この領域はスレッド固有の領域なので、他のスレッドによって書き換えられることはありません。

これを「スレッドセーフ」といいます。

ThreadLocalクラスを使うことで「クラス変数」についてもスレッド固有で管理することができます。

つまり「スレッドセーフ」を実現することができるのです。

ただし、オブジェクトを示すローカル変数(参照型のローカル変数)の場合、プリミティブ型同様スタック領域に格納されますが、ヒープ領域にオブジェクトの実体が存在するためその限りではありません。

スレッド毎に値を保持する方法

マルチスレッドでスレッド毎に値を保持する場合に、ただのTreadクラスを使用した場合とThreadLocalクラスを使用した場合で、それぞれクラス変数の値がどのようになるかみていきましょう。

Threadクラスでクラス変数を扱う場合の注意点

まずはThreadLocalクラスを使用せずにただのThreadクラスを使用した場合に、クラス変数の値がどのようになるのかみていきましょう。

class Int {
    // クラス変数の宣言
    private static int i;
 
    // getter
    public static int getInt() {
        return i;
    }
    // setter
    public static void setInt(int i) {
        Int.i = i;
    }
}
 
class ThreadTest extends Thread {
    int val = 0;
 
    // コンストラクタ
    public ThreadTest(int val){
        this.val = val;
    }
 
    public void run() {
        for (int i = 0; i < 5; i++){
 
            // ThreadTestクラスのメンバ変数valの値を
            // Intクラスのクラス変数にセット
            Int.setInt(val);
 
            // 1秒(1000ミリ秒)待機
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {}
 
            // 処理結果の表示
            // valの値とIntクラスのクラス変数の値が異なる場合は異常でエラーを表示"
            String message = getName() + ": val = " + val + ", i = " + Int.getInt();
            if (val == Int.getInt()) {
                System.out.println(message);
            } else {
                System.out.println(message + " エラー");
            }
        }
    }
}
 
 
public class Main {
 
    public static void main(String[] args) {
        ThreadTest tt1 = new ThreadTest(0);
        ThreadTest tt2 = new ThreadTest(1);
 
        tt1.start();
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) { }
        tt2.start();
    }
 
}

実行結果:

Thread-0: val = 0, i = 0
Thread-0: val = 0, i = 0
Thread-0: val = 0, i = 1 エラー
Thread-1: val = 1, i = 0 エラー
Thread-0: val = 0, i = 1 エラー
Thread-1: val = 1, i = 0 エラー
Thread-0: val = 0, i = 1 エラー
Thread-1: val = 1, i = 1
Thread-1: val = 1, i = 1
Thread-1: val = 1, i = 1

このサンプルコードでは、Intクラスでクラス変数「i」を宣言しています。

Intクラスにはクラス変数「i」のゲットメソッドであるgetIntメソッドとセットメソッドであるsetIntメソッドがあります。

ThreadTestクラスではコンストラクタで取得した変数「val」の値をIntクラスのクラス変数「i」に代入しています。

「val」と「i」の値が同じ値となっているか表示し、約1秒おきに5回繰り返すメソッドをrunメソッド内で定義しています。

これをスレッドで動かせるようにしています。

これらをMainクラスのmainメソッド内でインスタンス化しています。

「val」の値が0(ゼロ)のThread-0と「val」の値が1のThread-1を作り、Thread-0がスタートしてから約3秒後にThread-1がスタートするようになっています。

この場合Thread-0とThread-1の両方からクラス変数「i」にアクセスできます。

Thread-1がスタートしてThread-0が終了するまでの約2秒間は2つのスレッドが並列で処理を行っているので、クラス変数「i」の値が意図しない値となっています。

このような問題を解決するためにThreadLocalクラスを使用します。

ThreadLocalでクラス変数を扱う方法

それではサンプルコードを使ってThreadLocalクラスの使い方をみていきましょう。

class Int {
    // インスタンス変数の宣言
    private int i;
 
    // getter
    public int getInt() {
        return i;
    }
    // setter
    public void setInt(int i) {
        this.i = i;
    }
}
 
class NewInt {
 
    // 現行スレッドの初期値を取得
    private static ThreadLocal<Int> tl = new ThreadLocal<Int>(){
        @Override
        protected Int initialValue(){
            return new Int();
        }
    };
 
    // 現行スレッドの値を取得
    private static Int getNewInt(){
        return tl.get();
    }
 
    // 現行スレッドでのgetter
    public static int getInt(){
        return getNewInt().getInt();
    }
 
    // 現行スレッドでのsetter
    public static void setInt(int i){
        getNewInt().setInt(i);
    }
}
 
class ThreadTest extends Thread {
 
    int val = 0;
 
    // コンストラクタ
    public ThreadTest(int val){
        this.val = val;
    }
 
    public void run() {
        for (int i = 0; i < 5; i++){
 
            // ThreadTestクラスのメンバ変数valの値を
            // Intクラスのインスタンス変数にセット
            NewInt.setInt(val);
 
            // 1秒(1000ミリ秒)待機
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) { }
 
            // 処理結果の表示
            // valの値とIntクラスのクラス変数の値が異なる場合は異常でエラーを表示"
            String message = getName() + ": val = " + val + ", i = " + NewInt.getInt();
            if (val == NewInt.getInt()) {
                System.out.println(message);
            } else {
                System.out.println(message + " エラー");
            }
        }
    }
}
 
 
public class Main {
 
    public static void main(String[] args) {
        ThreadTest tt1 = new ThreadTest(0);
        ThreadTest tt2 = new ThreadTest(1);
 
        tt1.start();
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) { }
        tt2.start();
    }
 
}

実行結果:

Thread-0: val = 0, i = 0
Thread-0: val = 0, i = 0
Thread-0: val = 0, i = 0
Thread-1: val = 1, i = 1
Thread-0: val = 0, i = 0
Thread-1: val = 1, i = 1 
Thread-0: val = 0, i = 0 
Thread-1: val = 1, i = 1
Thread-1: val = 1, i = 1
Thread-1: val = 1, i = 1

このサンプルコードでは、先ほどの例に加えてNewIntクラスを作成しています。

Intクラスのメンバは「非static」に変更しています。

NewIntクラス内でThreadLocalクラスを使用します。

ThreadLocalクラスの使い方として、まずThreadLocalクラスをインスタンス化し、initialValueメソッドを使ってスレッドの初期値を取得します。

その次にこの例ではgetNewIntメソッドを定義して、スレッドの値を取得できるようにしています。

NewIntクラスのgetIntメソッドでIntクラスのgetIntメソッドを、setIntメソッドでIntクラスのsetIntメソッドを代替しています。

TreadTestクラスのrunメソッド内のIntクラスのクラスメソッドをNewIntクラスのクラスメソッドで代替することで、スレッド固有のクラス変数として意図通りに扱えています。

ThreadLocalRandomで乱数を扱う方法

ThreadLocalクラスを使ってスレッド固有の変数を使用しながら、乱数を使用する場合は注意が必要となります。

乱数を使用する場合、通常Math.Randomメソッドなどを使用しますが、この場合も同様にMath.Randomメソッドを使用すると処理速度が極端に遅くなる可能性があるからです。

ですのでこの場合はThreadLocalRandomクラスのThreadLocalRandom.current().nextIntメソッドを使用します。

実際に使用する例をみてみましょう。

import java.util.concurrent.ThreadLocalRandom;
 
class Int {
    // インスタンス変数の宣言
    private int i;
 
    // getter
    public int getInt() {
        return i;
    }
    // setter
    public void setInt(int i) {
        this.i = i;
    }
}
 
class NewInt {
 
    // 現行スレッドの初期値を取得
    private static ThreadLocal<Int> tl = new ThreadLocal<Int>(){
        @Override
        protected Int initialValue(){
            return new Int();
        }
    };
 
    // 現行スレッドの値を取得
    private static Int getNewInt(){
        return tl.get();
    }
 
    // 現行スレッドでのgetter
    public static int getInt(){
        return getNewInt().getInt();
    }
 
    // 現行スレッドでのsetter
    public static void setInt(int i){
        getNewInt().setInt(i);
    }
}
 
class ThreadTest extends Thread {
    int val = 0;
 
    // コンストラクタ
    public ThreadTest(int val){
        this.val = val;
    }
 
    public void run() {
 
        // ThreadTestクラスのメンバ変数valの値を
        // Intクラスのインスタンス変数にセット
        NewInt.setInt(val);
 
        ThreadLocalRandom.current().nextInt();
    }
}
 
 
public class Main {
 
    public static void main(String[] args) {
        long t0 = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++){
            ThreadTest tt1 = new ThreadTest(0);
            tt1.start();
        }
        long processTime1 = System.currentTimeMillis() - t0;
        System.out.println("ThreadLocalRandomクラスでの処理時間は" + processTime1 + "ミリ秒");
    }
 
}

実行結果:

ThreadLocalRandomクラスでの処理時間は557ミリ秒

ThreadTestクラスではこれまでの例と同様にNewInt.setIntメソッドとそれに加えて乱数を発生させるThreadLocalRandom.current().nextIntメソッドの2つのメソッドを定義しています。

これらのメソッドはスレッドで実行するrunメソッド内で定義されています。

Mainクラスのmainメソッドでは1万個のスレッドを作成し、並列処理を行っています。

その処理時間がこの場合は557ミリ秒でした。

ちなみに、ThreadLocalRandom.current().nextIntメソッドの代わりにMath.Randomメソッドを用いると、55730ミリ秒処理時間がかかり約100倍の差が確認されました。

このように処理時間に大きなロスが発生するので、乱数が必要な場合はThreadLocalRandom.current().nextIntメソッドを使用しましょう!

removeを使ったメモリリークへの対応

ThreadLocalクラスを使用する場合の注意点があります。

それはメモリリークが発生する可能性があることです。

メモリリークを発生させないためには処理が終わったスレッドはremoveメソッドを使って削除するようにしましょう。

removeメソッドの使い方は下記のサンプルコードのとおりです。

import java.util.concurrent.ThreadLocalRandom;
 
class Int {
    // インスタンス変数の宣言
    private int i;
 
    // getter
    public int getInt() {
        return i;
    }
    // setter
    public void setInt(int i) {
        this.i = i;
    }
}
 
class NewInt {
 
    // 現行スレッドの初期値を取得
    private static ThreadLocal<Int> tl = new ThreadLocal<Int>(){
        @Override
        protected Int initialValue(){
            return new Int();
        }
    };
 
    // 現行スレッドの値を取得
    private static Int getNewInt(){
        return tl.get();
    }
 
    // 現行スレッドでのgetter
    public static int getInt(){
        return getNewInt().getInt();
    }
 
    // 現行スレッドでのsetter
    public static void setInt(int i){
        getNewInt().setInt(i);
    }
 
    // 現在のスレッドの値を削除
    public static void remove(){
        tl.remove();
    }
}
 
class ThreadTest extends Thread {
    int val = 0;
 
    // コンストラクタ
    public ThreadTest(int val){
        this.val = val;
    }
 
    public void run() {
 
        try {
            // ThreadTestクラスのメンバ変数valの値を
            // Intクラスのクラス変数にセット
            NewInt.setInt(val);
 
            ThreadLocalRandom.current().nextInt();
        } finally {
            NewInt.remove();
        }
    }
}
 
 
public class Main {
 
    public static void main(String[] args) {
        long t0 = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++){
            ThreadTest tt1 = new ThreadTest(0);
            tt1.start();
        }
        long processTime1 = System.currentTimeMillis() - t0;
        System.out.println("ThreadLocalRandomクラスでの処理時間は" + processTime1 + "ミリ秒");
    }
 
}

実行結果:

    
ThreadLocalRandomクラスでの処理時間は574ミリ秒

このサンプルコードでは、ThreadLocalクラスを使用しているNewIntクラス内でThreadLocalクラスのインスタンス「tl」のremoveメソッドを実行するremoveメソッドを作成しています。

これをThreadTest クラスのrunメソッドにおいてfinallyブロックの中で実行されるようにしています。

このようにすると処理が終了する度にスレッドは削除され、メモリリークの発生を防止することができます。

コンストラクタについて詳しく知りたい方へ

この記事では初期値を設定するためにコンストラクタを使用しています。

コンストラクタのさまざまな使い方については、以下の記事にまとめていますので、ぜひ参考にしてくださいね!

Threadの使い方(sleepで停止、joinで同期)

Threadクラスを使用してsleepメソッドで一時的に処理を停止したり、joinメソッドを使って別のスレッドの処理が完了するまで待機して同期させるといったことができます。

この記事では紹介しきれなかったThreadクラスのいろいろな使い方を次の記事にまとめているので、ぜひ確認してください!

まとめ

ここでは、ThreadLocalについて使い方や使う上での注意点について説明しました。

スレッド固有の値を取り扱う場合は他のスレッドから値にアクセスできないように、また乱数を一緒に使う場合やメモリリークなど注意する点がいくつかあります。

マルチスレッドの記述は最初は複雑だと感じるかもしれませんが、慣れて使いこなすことができるようにこの記事を何度も参考にして下さいね!

この記事を書いた人

【プロフィール】
DX認定取得事業者に選定されている株式会社SAMURAIのマーケティング・コミュニケーション部が運営。「質の高いIT教育を、すべての人に」をミッションに、IT・プログラミングを学び始めた初学者の方に向け記事を執筆。
累計指導者数4万5,000名以上のプログラミングスクール「侍エンジニア」、累計登録者数1万8,000人以上のオンライン学習サービス「侍テラコヤ」で扱う教材開発のノウハウ、2013年の創業から運営で得た知見に基づき、記事の執筆だけでなく編集・監修も担当しています。
【専門分野】
IT/Web開発/AI・ロボット開発/インフラ開発/ゲーム開発/AI/Webデザイン

目次