在本课中,我们将大体讨论使用java.lang.ThreadLocal<>类以及如何在多线程环境中使用它。

ThreadLocal类用于存储变量这个类的一个显着特征是它为每个使用它的线程保留一个单独的独立值副本。

深入研究类的操作,我们可以想象一个映射,将线程映射到值,当前线程需要使用时从中取合适的值。

ThreadLocal类构造器

构造器 行动
线程局部() 在 Java 中创建一个空变量

方法

方法 行动
得到() 返回当前线程局部变量的值
放() 为当前线程设置局部变量的值
消除() 移除当前线程局部变量的值
ThreadLocal.withInitial() 设置初始值的附加工厂方法

获取()和设置()

让我们写一个例子,我们创建两个计数器。第一个是普通变量,用于计算线程数。第二个我们将包装在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

在这里我们很容易看到第二个线程中的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类来获取随机数。但它在多线程环境中是否同样有效?实际上,不。Random不适合多线程环境,因为当多个线程同时访问一个类时,性能会受到影响。

为了解决这个问题,JDK 7 引入了java.util.concurrent.ThreadLocalRandom类来在多线程环境中生成随机数。它由两个类组成:ThreadLocalRandom

一个线程接收到的随机数是独立于其他线程的,而java.util.Random提供的是全局随机数。此外,与Random不同,ThreadLocalRandom不支持显式播种。相反,它覆盖了从Random继承的setSeed()方法,因此它在调用时总是抛出UnsupportedOperationException 。

我们看一下ThreadLocalRandom类的方法:

方法 行动
ThreadLocalRandom current() 返回当前线程的 ThreadLocalRandom。
下一个(整数位) 生成下一个伪随机数。
double nextDouble(double least, double bound) 从最小(包含)和绑定(排除)之间的均匀分布返回伪随机数。
int nextInt(int least, int bound) 从最小(包含)和绑定(排除)之间的均匀分布返回伪随机数。
长 nextLong(long n) 从 0(含)和指定值(不含)之间的均匀分布返回一个伪随机数。
long nextLong(最短,长界) 从最小(包含)和绑定(排除)之间的均匀分布返回伪随机数。
void setSeed(长种子) 抛出UnsupportedOperationException。此生成器不支持播种。

使用 ThreadLocalRandom.current() 获取随机数

ThreadLocalRandom是ThreadLocalRandom类的组合。它通过简单地避免对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类时性能受到的影响就越大。

总结并重申RandomThreadLocalRandom类之间的区别:

随机的 线程本地随机
如果不同的线程使用同一个Random实例,就会发生冲突并且性能会受到影响。 没有冲突或问题,因为生成的随机数是当前线程本地的。
使用线性同余公式更改初始值。 随机数生成器使用内部生成的种子进行初始化。
在每个线程都使用自己的一组Random对象的应用程序中很有用。 在多个线程在线程池中并行使用随机数的应用程序中很有用。
这是一个父类。 这是一个儿童班。