このレッスンでは、 java.lang.ThreadLocal<>クラスの操作と、それをマルチスレッド環境で使用する方法について概説します。

ThreadLocalクラスは変数を格納するため使用されます。このクラスの特徴は、それを使用するスレッドごとに値の独立したコピーを保持することです。

クラスの操作をさらに詳しく調べると、スレッドを値にマップするMapが想像できます。現在のスレッドは、その値を使用する必要があるときに、そこから適切な値を取得します。

ThreadLocal クラス コンストラクター

コンストラクタ アクション
ThreadLocal() Javaで空の変数を作成します。

メソッド

方法 アクション
得る() 現在のスレッドのローカル変数の値を返します。
設定() 現在のスレッドのローカル変数の値を設定します。
削除() 現在のスレッドのローカル変数の値を削除します
ThreadLocal.withInitial() 初期値を設定する追加のファクトリ メソッド

get() & set()

2 つのカウンターを作成する例を書いてみましょう。1 つ目は通常の変数で、スレッドの数をカウントするためのものです。2 つ目はThreadLocalでラップします。そして、それらがどのように連携するか見てみましょう。まず、 Runnable を継承し、データと非常に重要なrun()メソッドを含むThreadDemoクラスを作成しましょう。画面にカウンターを表示するメソッドも追加します。


class ThreadDemo implements Runnable {

    int counter;
    ThreadLocal<Integer> threadLocalCounter = new ThreadLocal<>();

    public void run() {
        counter++;

        if(threadLocalCounter.get() != null) {
            threadLocalCounter.set(threadLocalCounter.get() + 1);
        } else {
            threadLocalCounter.set(0);
        }
        printCounters();
    }

    public void printCounters(){
        System.out.println("Counter: " + counter);
        System.out.println("threadLocalCounter: " + threadLocalCounter.get());
    }
}

クラスを実行するたびに、カウンター変数を使用してget()メソッドを呼び出して、 ThreadLocal変数からデータを取得します。新しいスレッドにデータがない場合は 0 に設定します。データがある場合は 1 ずつ増やします。そしてメインメソッドを書いてみましょう:


public static void main(String[] args) {
    ThreadDemo threadDemo = new ThreadDemo();

    Thread t1 = new Thread(threadDemo);
    Thread t2 = new Thread(threadDemo);
    Thread t3 = new Thread(threadDemo);

    t1.start();
    t2.start();
    t3.start();

}

クラスを実行すると、ThreadLocal変数はアクセスするスレッドに関係なく同じままですが、スレッドの数は増加していることがわかります。

カウンタ: 1
カウンタ: 2
カウンタ: 3
threadLocalCounter: 0
threadLocalCounter: 0
threadLocalCounter: 0

プロセスは終了コード 0 で終了しました

削除()

Removeメソッドがどのように機能するかを理解するために、 ThreadDemoクラスのコードを少し変更します。


if(threadLocalCounter.get() != null) {
      threadLocalCounter.set(threadLocalCounter.get() + 1);
  } else {
      if (counter % 2 == 0) {
          threadLocalCounter.remove();
      } else {
          threadLocalCounter.set(0);
      }
  }

このコードでは、スレッド カウンタが偶数の場合、ThreadLocal変数のRemove()メソッドを呼び出します。結果:

カウンタ: 3
threadLocalCounter: 0
カウンタ: 2
threadLocalCounter: null
カウンタ: 1
threadLocalCounter: 0

プロセスは終了コード 0 で終了しました

ここで、2 番目のスレッドのThreadLocal変数がnullであることが簡単にわかります。

ThreadLocal.withInitial()

このメソッドはスレッドローカル変数を作成します。

ThreadDemoクラスの実装:


class ThreadDemo implements Runnable {

    int counter;
    ThreadLocal<Integer> threadLocalCounter = ThreadLocal.withInitial(() -> 1);

    public void run() {
        counter++;
        printCounters();
    }

    public void printCounters(){
        System.out.println("Counter: " + counter);
        System.out.println("threadLocalCounter: " + threadLocalCounter.get());
    }
}

コードの結果を見てみましょう。

カウンタ: 1
カウンタ: 2
カウンタ: 3
threadLocalCounter: 1
threadLocalCounter: 1
threadLocalCounter: 1

プロセスは終了コード 0 で終了しました

なぜそのような変数を使用する必要があるのでしょうか?

ThreadLocal は、java.lang.Threadの実行スレッドに関連するローカル変数の抽象化を提供します。

ThreadLocal変数は、各スレッドがget()およびset()メソッドを介してアクセスされる、個別に初期化された独自の変数インスタンスを持っているという点で通常の変数とは異なります。

各スレッド、つまりThreadクラスのインスタンスには、それに関連付けられたThreadLocal変数のマップがあります。マップのキーはThreadLocalオブジェクトへの参照であり、値は「取得された」ThreadLocal変数への参照です。

Random クラスがマルチスレッド アプリケーションでの乱数の生成に適していないのはなぜですか?

Randomクラスを使用して乱数を取得します。しかし、それはマルチスレッド環境でも同様に機能するのでしょうか? 実は違う。複数のスレッドが同時にクラスにアクセスするとパフォーマンスが低下するため、ランダムはマルチスレッド環境には適していません。

この問題に対処するために、JDK 7 では、マルチスレッド環境で乱数を生成するjava.util.concurrent.ThreadLocalRandomクラスが導入されました。これは、 ThreadLocalRandomの 2 つのクラスで構成されます。

1 つのスレッドが受け取る乱数は他のスレッドから独立していますが、java.util.Random はグローバルな乱数を提供します。また、Randomとは異なり、ThreadLocalRandom は明示的なシードをサポートしません。代わりに、Randomから継承したsetSeed()メソッドをオーバーライドして、呼び出されたときに常に UnsupportedOperationException をスローします

ThreadLocalRandomクラスのメソッドを見てみましょう。

方法 アクション
ThreadLocalRandom current() 現在のスレッドの ThreadLocalRandom を返します。
int next(int ビット) 次の疑似乱数を生成します。
double nextDouble(二重最小、二重境界) 最小(これを含む) と限界(これを含まない)の間の一様分布から擬似乱数を返します。
int nextInt(int 最小、int 境界) 最小 (これを含む) と限界 (これを含まない) の間の一様分布から擬似乱数を返します。
long nextLong(long n) 0 (両端を含む) から指定された値 (両端を含まない) までの一様分布から擬似乱数を返します。
long nextLong(最小ロング、ロングバウンド) 最小 (これを含む) と限界 (これを含まない) の間の一様分布から擬似乱数を返します。
void setSeed(ロングシード) UnsupportedOperationExceptionをスローします。このジェネレーターはシードをサポートしていません。

ThreadLocalRandom.current() を使用して乱数を取得する

ThreadLocalRandom は、 ThreadLocalクラスとRandomクラスを組み合わせたものです。Randomクラスのインスタンスへの同時アクセスを回避するだけで、マルチスレッド環境でのパフォーマンスが向上します。

複数のスレッドを含む例を実装し、アプリケーションがThreadLocalRandomクラスを使用して行うことを確認してみましょう。


import java.util.concurrent.ThreadLocalRandom;

class RandomNumbers extends Thread {

    public void run() {
        try {
            int bound = 100;
            int result = ThreadLocalRandom.current().nextInt(bound);
            System.out.println("Thread " + Thread.currentThread().getId() + " generated " + result);
        }
        catch (Exception e) {
            System.out.println("Exception");
        }
    }

    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();

				for (int i = 0; i < 10; i++) {
            RandomNumbers randomNumbers = new RandomNumbers();
            randomNumbers.start();
        }

        long endTime = System.currentTimeMillis();

        System.out.println("Time taken: " + (endTime - startTime));
    }
}

私たちのプログラムの結果:

所要時間: 1
スレッド 17 が生成されました 13
スレッド 18 が生成されました 41
スレッド 16 が生成されました 99
スレッド 19 が生成されました 25
スレッド 23 が生成されました 33
スレッド 24 が生成されました 21 スレッド 15 が生成されました
15 スレッド 21 が生成されました 28
スレッド 22 が生成されました 97
スレッド 20 が
生成されました 33

次に、RandomNumbersクラスを変更して、その中でRandom を使用しましょう。


int result = new Random().nextInt(bound);
所要時間: 5
スレッド 20 生成 48
スレッド 19 生成 57
スレッド 18 生成 90
スレッド 22 生成 43
スレッド 24 生成 7
スレッド 23 生成 63スレッド 15
生成 2
スレッド 16 生成 40
スレッド 17 生成 29
スレッド 21 生成 12

メモを取る!私たちのテストでは、結果が同じになる場合もあれば、異なる場合もありました。ただし、さらに多くのスレッド (たとえば 100) を使用すると、結果は次のようになります。

ランダム — 19 ~ 25 ミリ秒
ThreadLocalRandom — 17 ~ 19 ミリ秒

したがって、アプリケーション内のスレッドが多いほど、マルチスレッド環境でRandomクラスを使用する場合のパフォーマンスへの影響が大きくなります。

RandomクラスとThreadLocalRandomクラスの違いを要約して繰り返すと、次のようになります

ランダム スレッドローカルランダム
異なるスレッドがRandomの同じインスタンスを使用すると、競合が発生し、パフォーマンスが低下します。 生成された乱数は現在のスレッドに対してローカルであるため、競合や問題は発生しません。
線形合同式を使用して初期値を変更します。 乱数発生器は、内部生成されたシードを使用して初期化されます。
各スレッドが独自のRandomオブジェクトのセットを使用するアプリケーションで役立ちます。 複数のスレッドがスレッド プールで乱数を並行して使用するアプリケーションで役立ちます。
これは親クラスです。 こちらは子供クラスです。