Java ThreadLocal【译】

Java的ThreadLocal类使你可以创建一个只能被一个线程读取和写入的变量。因此,即使两个线程在执行相同的代码,并且引用了同一个ThreadLocal变量,两个线程也互相无法获取到对方的ThreadLocal变量。因此,Java的ThreadLocal类提供了一个简单的方法来实现代码的线程安全。

创建一个ThreadLocal

创建一个ThreadLocal实例和创建一个普通的Java对象相同 – 通过new操作。以下是一个创建一个ThreadLocal变量的示例:

private ThreadLocal threadLocal = new ThreadLocal();

每个线程只需要执行一次。多个线程可以在这个ThreadLocal内部get和set值,并且每个线程只能读取到它们自己set的值。

set ThreadLocal的值

一旦ThreadLocal被创建,你就可以通过set()方法将值保存在其中。

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

get ThreadLocal的值

你可以通过get()方法来读取存储在其中的值。以下是一个获取Java ThreadLocal中的值的示例:

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

移除ThreadLocal值

可以移除一个ThreadLocal中的变量集。你可以通过调用ThreadLocal.remove()方法来移除一个值。以下是移除一个Java ThreadLocal中数据集的示例:

threadLocal.remove();

通用ThreadLocal

你可以通过泛型来创建一个ThreadLocal,使用泛型后,只有指定类型的值可以被set到ThreadLocal。另外,你不需要对get()方法返回的值进行强转。以下是一个泛型ThreadLocal的示例:

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

现在你可以是保存字符串到这个ThreadLocal实例。另外,对于获取到的值你也不需要进行强转:

myThreadLocal.set("Hello ThreadLocal");

String threadLocalValue = myThreadLocal.get();

初始化ThreadLocal的值

可以为Java ThreadLocal设置一个初始化值,这个值是在调用set()方法前第一次调用get()方法时被使用到的一个新值。你有两个选项用来为一个ThreadLocal来指定一个初始化值:

  • 创建一个ThreadLocal子类,重写initiaValue()方法
  • 通过使用Supplier接口实现来创建ThreadLocal

我会在下面的章节介绍这两种方式。

重写initiaValue()方法

第一种给Java ThreadLocal指定初始值的方法是创建一个ThreadLocal的子类,重写initiaValue()方法。创建一个ThreadLocal子类最简单的方法就是在你创建ThreadLocal变量的地方简单地创建一个匿名子类。以下是一个示例:

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

注意,不同的线程会获取到不同的初始值。每个线程会创建它自有的初始值。只有在initiaValue()方法返回了相同的对象时,这时候素有线程读取到了相同的对象。然而,我们使用ThreadLocal的最初目的就是为了避免不同的线程读取到相同的实例。

提供一个Supplier实现

第二种给Java ThreadLocal指定初始值的方法是使用它的静态工厂方法withInitial(Supplier)通过将Supplier接口作为一个参数实现。这个Supplier实现为ThreadLocal提供初始值。以下是一个示例:

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

由于Supplier是一个functional interface,所以它可以使用Java Lambda Expression。以下是将Supplier实现作为lambda表达式提供给withInitial()的示例:

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

你可以看到,这比前一个例子短一些。还能更短,使用lambda表达式的密集语法:

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

Lazy Setting ThreadLocal的值

在有些情况下,你不能使用标准的方法来设置初始值。比如,你可能需要一些配置信息,这些配置信息在你创建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 SimpleDateFormat实例。如果一个SimpleDateFormat实例没有被设置到ThreadLocal中,那么一个新的SimpleDateFormat就会被创建并且设置到ThreadLocal中。一旦一个线程在ThreadLocal设置了它自己的SimpleDateFormat,那么接下来这个SimpleDateFormat就被这个线程使用,且仅有这个线程可用。每个线程都会创建自己的SimpleDateFormat实例,所以他们互相之间不可见对方在ThreadLocal上设置的变量。

SimpleDateFormat类是非线程安全的,所以多个线程不能同时使用SimpleDateFormat。为了解决这个问题,前文的MyDateFormatter类为每个线程创建了一个SimpleDateFormat,这样每个线程在调用format()方法时可以使用它们自己的SimpleDateFormat实例了。

在Thread Pool或者ExecutorService中使用ThreadLocal

如果你计划在Java线程池或者Java ExecutorService的任务中使用ThreadLocal,请牢记一点,你不能保证线程会执行你的任务。然而,如果你需要确保对于同一个对象每个线程都使用它们自己的实例,那么这不是问题。这种情况下你可以在Thread Pool或者ExecutorService中使用ThreadLocal。

Full ThreadLocal示例

以下是完全可运行的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对象,所以两个线程无法互相访问对方的值。因此,他们set和get的值不相同。

可继承的ThreadLocal

InheritableThreadLocal类是ThreadLocal类的一个子类。

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

发表评论

电子邮件地址不会被公开。 必填项已用*标注