Java ThreadLocal

时间:2020-01-09 10:35:53  来源:igfitidea点击:

Java ThreadLocal类使我们可以创建只能由同一线程读取和写入的变量。因此,即使两个线程正在执行相同的代码,并且该代码引用了相同的" ThreadLocal"变量,两个线程也无法看到彼此的" ThreadLocal"变量。因此,Java ThreadLocal类提供了一种使代码线程安全的简单方法,而在其他情况下则不会。

创建一个ThreadLocal

我们可以创建一个ThreadLocal实例,就像通过new运算符创建任何其他Java对象一样。这是显示如何创建ThreadLocal变量的示例:

private ThreadLocal threadLocal = new ThreadLocal();

每个线程只需执行一次。现在,多个线程可以在此" ThreadLocal"中获取和设置值,并且每个线程将仅看到它自己设置的值。

设置ThreadLocal值

一旦创建了" ThreadLocal",就可以使用其" set()"方法设置要存储其中的值。

threadLocal.set("A thread local value");

获取ThreadLocal值

我们可以使用其get()方法读取存储在ThreadLocal中的值。这是获取存储在Java ThreadLocal内部的值的示例:

String threadLocalValue = (String) threadLocal.get();

删除ThreadLocal值

可以删除ThreadLocal变量中设置的值。我们可以通过调用ThreadLocal`remove()方法来删除值。下面是删除Java ThreadLocal上设置的值的示例:

threadLocal.remove();

通用ThreadLocal

我们可以创建一个具有泛型类型的ThreadLocal。使用泛型类型,只能在ThreadLocal上将泛型类型的对象设置为值。另外,我们不必强制转换get()返回的值。这是一个通用的" ThreadLocal"示例:

private ThreadLocal<String> myThreadLocal = new ThreadLocal<String>();

现在,我们只能将字符串存储在ThreadLocal实例中。另外,我们不需要强制转换从ThreadLocal获得的值:

myThreadLocal.set("Hello ThreadLocal");

String threadLocalValue = myThreadLocal.get();

初始ThreadLocal值

可以为Java的ThreadLocal设置一个初始值,该值将在使用新值调用set()之前第一次调用get()时使用。我们可以通过两个选项为ThreadLocal指定初始值:

  • 创建一个覆盖LocalitiValue()方法的ThreadLocal子类。
  • Supplier接口实现创建一个ThreadLocal。

我将在以下各节中向我们展示这两个选项。

覆盖initialValue()

为Java" ThreadLocal"变量指定初始值的第一种方法是创建" ThreadLocal"的子类,该子类将覆盖其" initialValue()"方法。创建ThreadLocal的子类的最简单方法是简单地创建一个匿名子类,就在我们创建ThreadLocal变量的位置。这是一个创建ThreadLocal的匿名子类的示例,该子类将覆盖initialValue()方法:

private ThreadLocal myThreadLocal = new ThreadLocal<String>() {
    @Override protected String initialValue() {
        return String.valueOf(System.currentTimeMillis());
    }
};

请注意,不同的线程仍将看到不同的初始值。每个线程将创建自己的初始值。只有从" initialValue()"方法返回完全相同的对象时,所有线程才会看到相同的对象。然而,首先使用ThreadLocal的全部目的是为了避免不同的线程看到相同的实例。

提供供应商实施

为JavaThreadLocal变量指定初始值的第二种方法是使用其静态工厂方法withInitial(Supplier)传递一个Supplier接口实现作为参数。该供应商实现为ThreadLocal提供了初始值。这是一个使用其静态工厂方法" withInitial()"创建" ThreadLocal"的示例,并将一个简单的" Supplier"实现作为参数传递:

ThreadLocal<String> threadLocal = ThreadLocal.withInitial(new Supplier<String>() {
    @Override
    public String get() {
        return String.valueOf(System.currentTimeMillis());
    }
});

由于"供应商"是一个功能性接口,因此可以使用Java Lambda表达式来实现。这是向withInitial()提供作为Lambda表达式的Supplier实现的样子:

ThreadLocal threadLocal = ThreadLocal.withInitial(
        () -> { return String.valueOf(System.currentTimeMillis()); } );

如我们所见,这比前面的示例要短一些。但是,使用最密集的lambda表达式语法,它甚至可以变得更短:

ThreadLocal threadLocal3 = ThreadLocal.withInitial(
        () -> String.valueOf(System.currentTimeMillis()) );

线程局部值的延迟设置

在某些情况下,我们无法使用设置初始值的标准方法。例如,也许我们需要一些配置信息,这些信息在创建ThreadLocal变量时不可用。在这种情况下,我们可以延迟设置初始值。这是一个如何在Java ThreadLocal上延迟设置初始值的示例:

public class MyDateFormatter {

    private ThreadLocal<SimpleDateFormat> simpleDateFormatThreadLocal = new ThreadLocal<>();

    public String format(Date date) {
        SimpleDateFormat simpleDateFormat = getThreadLocalSimpleDateFormat();
        return simpleDateFormat.format(date);
    }
    
    
    private SimpleDateFormat getThreadLocalSimpleDateFormat() {
        SimpleDateFormat simpleDateFormat = simpleDateFormatThreadLocal.get();
        if(simpleDateFormat == null) {
            simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            simpleDateFormatThreadLocal.set(simpleDateFormat);
        }
        return simpleDateFormat;
    }
}

注意format()方法如何调用getThreadLocalSimpleDateFormat()方法来获取Java SimpleDatFormat实例。如果尚未在" ThreadLocal"中设置" SimpleDateFormat"实例,则会创建一个新的" SimpleDateFormat"并在" ThreadLocal"变量中进行设置。一旦线程在ThreadLocal变量中设置了自己的SimpleDateFormat,就将相同的SimpleDateFormat对象用于该线程。但仅适用于该线程。每个线程都会创建自己的SimpleDateFormat实例,因为它们看不到在ThreadLocal变量上设置的其他实例。

SimpleDateFormat类不是线程安全的,因此多个线程不能同时使用它。为了解决这个问题,上面的MyDateFormatter类为每个线程创建一个SimpleDateFormat,因此每个调用format()方法的线程都将使用其自己的SimpleDateFormat实例。

将ThreadLocal与线程池或者ExecutorService一起使用

如果我们打算在传递给Java线程池或者Java ExecutorService的任务中使用JavaThreadLocal,请记住,我们无法保证哪个线程将执行任务。但是,如果我们只需要确保每个线程使用其自己的某个对象实例,那么这不是问题。然后,我们可以将Java ThreadLocal与线程池或者ExecutorService一起使用。

全线程本地示例

这是一个完全可运行的Java ThreadLocal示例:

public class ThreadLocalExample {

    public static void main(String[] args) {
        MyRunnable sharedRunnableInstance = new MyRunnable();

        Thread thread1 = new Thread(sharedRunnableInstance);
        Thread thread2 = new Thread(sharedRunnableInstance);

        thread1.start();
        thread2.start();

        thread1.join(); //wait for thread 1 to terminate
        thread2.join(); //wait for thread 2 to terminate
    }

}
public class MyRunnable implements Runnable {

    private ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>();

    @Override
    public void run() {
        threadLocal.set( (int) (Math.random() * 100D) );

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
        }

        System.out.println(threadLocal.get());
    }
}

这个例子创建了一个MyRunnable实例,该实例被传递给两个不同的线程。两个线程都执行run()方法,因此在ThreadLocal实例上设置了不同的值。如果对set()调用的访问已同步,并且不是ThreadLocal对象,则第二个线程将覆盖第一个线程设置的值。

但是,由于它是一个" ThreadLocal"对象,因此两个线程无法看到彼此的值。因此,它们设置并获得不同的值。

InheritableThreadLocal

InheritableThreadLocal类是ThreadLocal的子类。而不是每个线程在ThreadLocal内部都有自己的值,而是通过InheritableThreadLocal授予对线程以及该线程创建的所有子线程的值的访问权限。