An Introduction to ThreadLocal in Java – Java中的ThreadLocal简介

最后修改: 2017年 4月 16日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

In this tutorial, we’ll be looking at the ThreadLocal construct from the java.lang package. This gives us the ability to store data individually for the current thread and simply wrap it within a special type of object.

在本教程中,我们将研究ThreadLocal结构,它来自java.lang包。这使我们有能力为当前线程单独存储数据,并简单地将其包裹在一个特殊类型的对象中。

2. ThreadLocal API

2.ThreadLocal API

The TheadLocal construct allows us to store data that will be accessible only by a specific thread.

TheadLocal 结构允许我们存储只有特定线程才能访问的数据。

Let’s say that we want to have an Integer value that will be bundled with the specific thread:

比方说,我们想有一个Integer值,它将与特定的线程捆绑在一起。

ThreadLocal<Integer> threadLocalValue = new ThreadLocal<>();

Next, when we want to use this value from a thread, we only need to call a get() or set() method. Simply put, we can imagine that ThreadLocal stores data inside of a map with the thread as the key.

接下来,当我们想使用线程的这个值时,我们只需要调用get()set()方法。简单地说,我们可以想象,ThreadLocal在一个以线程为键的映射中存储数据。

As a result, when we call a get() method on the threadLocalValue, we’ll get an Integer value for the requesting thread:

因此,当我们对threadLocalValue调用get()方法时,我们将得到一个请求线程的Integer值。

threadLocalValue.set(1);
Integer result = threadLocalValue.get();

We can construct an instance of the ThreadLocal by using the withInitial() static method and passing a supplier to it:

我们可以通过使用withInitial()静态方法并向其传递一个供应商来构造一个ThreadLocal的实例。

ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 1);

To remove the value from the ThreadLocal, we can call the remove() method:

要从ThreadLocal中移除值,我们可以调用remove()方法。

threadLocal.remove();

To see how to use the ThreadLocal properly, we’ll first look at an example that doesn’t use a ThreadLocal, and then we’ll rewrite our example to leverage that construct.

为了了解如何正确使用ThreadLocal,我们首先看一个没有使用ThreadLocal的例子,然后我们将重写我们的例子以利用该结构。

3. Storing User Data in a Map

3.将用户数据存储在地图中

Let’s consider a program that needs to store the user-specific Context data per given user id:

让我们考虑一个程序,它需要为每个给定的用户ID存储特定用户的Context数据。

public class Context {
    private String userName;

    public Context(String userName) {
        this.userName = userName;
    }
}

We want to have one thread per user id. We’ll create a SharedMapWithUserContext class that implements the Runnable interface. The implementation in the run() method calls some database through the UserRepository class that returns a Context object for a given userId.

我们希望每个用户ID有一个线程。我们将创建一个SharedMapWithUserContext类,实现Runnable接口。run()方法中的实现通过UserRepository类调用一些数据库,返回给定userIdContext对象。

Next, we store that context in the ConcurentHashMap keyed by userId:

接下来,我们将该上下文存储在ConcurentHashMap中,以userId为关键。

public class SharedMapWithUserContext implements Runnable {
 
    public static Map<Integer, Context> userContextPerUserId
      = new ConcurrentHashMap<>();
    private Integer userId;
    private UserRepository userRepository = new UserRepository();

    @Override
    public void run() {
        String userName = userRepository.getUserNameForUserId(userId);
        userContextPerUserId.put(userId, new Context(userName));
    }

    // standard constructor
}

We can easily test our code by creating and starting two threads for two different userIds, and asserting that we have two entries in the userContextPerUserId map:

我们可以通过为两个不同的userIds创建并启动两个线程,并断言我们在userContextPerUserId映射中有两个条目,来轻松测试我们的代码。

SharedMapWithUserContext firstUser = new SharedMapWithUserContext(1);
SharedMapWithUserContext secondUser = new SharedMapWithUserContext(2);
new Thread(firstUser).start();
new Thread(secondUser).start();

assertEquals(SharedMapWithUserContext.userContextPerUserId.size(), 2);

4. Storing User Data in ThreadLocal

4.将用户数据存储在ThreadLocal

We can rewrite our example to store the user Context instance using a ThreadLocal. Each thread will have its own ThreadLocal instance.

我们可以重写我们的例子,用一个ThreadLocal来存储用户Context实例。每个线程将有自己的ThreadLocal实例。

When using ThreadLocal, we need to be very careful because every ThreadLocal instance is associated with a particular thread. In our example, we have a dedicated thread for each particular userId, and this thread is created by us, so we have full control over it.

当使用ThreadLocal时,我们需要非常小心,因为每个ThreadLocal实例都与一个特定的线程有关。在我们的例子中,我们为每个特定的userId设置了一个专用线程,这个线程是由我们创建的,所以我们可以完全控制它。

The run() method will fetch the user context and store it into the ThreadLocal variable using the set() method:

run()方法将获取用户上下文并使用set()方法将其存储到ThreadLocal变量中。

public class ThreadLocalWithUserContext implements Runnable {
 
    private static ThreadLocal<Context> userContext 
      = new ThreadLocal<>();
    private Integer userId;
    private UserRepository userRepository = new UserRepository();

    @Override
    public void run() {
        String userName = userRepository.getUserNameForUserId(userId);
        userContext.set(new Context(userName));
        System.out.println("thread context for given userId: " 
          + userId + " is: " + userContext.get());
    }
    
    // standard constructor
}

We can test it by starting two threads that will execute the action for a given userId:

我们可以通过启动两个线程来测试它,这两个线程将为给定的userId执行动作。

ThreadLocalWithUserContext firstUser 
  = new ThreadLocalWithUserContext(1);
ThreadLocalWithUserContext secondUser 
  = new ThreadLocalWithUserContext(2);
new Thread(firstUser).start();
new Thread(secondUser).start();

After running this code, we’ll see on the standard output that ThreadLocal was set per given thread:

运行这段代码后,我们会在标准输出上看到, ThreadLocal被设置在每个给定的线程中。

thread context for given userId: 1 is: Context{userNameSecret='18a78f8e-24d2-4abf-91d6-79eaa198123f'}
thread context for given userId: 2 is: Context{userNameSecret='e19f6a0a-253e-423e-8b2b-bca1f471ae5c'}

We can see that each of the users has its own Context.

我们可以看到,每个用户都有自己的Context

5. ThreadLocals and Thread Pools

5.ThreadLocals和线程池

ThreadLocal provides an easy-to-use API to confine some values to each thread. This is a reasonable way of achieving thread-safety in Java. However, we should be extra careful when we’re using ThreadLocals and thread pools together.

ThreadLocal提供了一个易于使用的API,将一些值限制在每个线程中。这是一种在Java中实现线程安全的合理方式。然而,当我们在使用ThreadLocals和线程池时,我们应该格外小心。

In order to better understand this possible caveat, let’s consider the following scenario:

为了更好地理解这一可能的注意事项,让我们考虑以下情况。

  1. First, the application borrows a thread from the pool.
  2. Then it stores some thread-confined values into the current thread’s ThreadLocal.
  3. Once the current execution finishes, the application returns the borrowed thread to the pool.
  4. After a while, the application borrows the same thread to process another request.
  5. Since the application didn’t perform the necessary cleanups last time, it may re-use the same ThreadLocal data for the new request.

This may cause surprising consequences in highly concurrent applications.

这可能会在高并发的应用程序中造成令人惊讶的后果。

One way to solve this problem is to manually remove each ThreadLocal once we’re done using it. Because this approach needs rigorous code reviews, it can be error-prone.

解决这个问题的一个方法是,一旦我们使用完每个ThreadLocal,就手动删除它。因为这种方法需要严格的代码审查,它可能很容易出错。

5.1. Extending the ThreadPoolExecutor

5.1.对ThreadPoolExecutor的扩展

As it turns out, it’s possible to extend the ThreadPoolExecutor class and provide a custom hook implementation for the beforeExecute() and afterExecute() methods. The thread pool will call the beforeExecute() method before running anything using the borrowed thread. On the other hand, it’ll call the afterExecute() method after executing our logic.

事实证明,可以扩展ThreadPoolExecutor类,并为beforeExecute()a>和afterExecute()a>方法提供自定义挂钩实现。线程池将在使用借来的线程运行任何东西之前调用beforeExecute()方法。另一方面,它将在执行我们的逻辑后调用afterExecute()方法。

Therefore, we can extend the ThreadPoolExecutor class and remove the ThreadLocal data in the afterExecute() method:

因此,我们可以扩展ThreadPoolExecutor类,在afterExecute()方法中删除ThreadLocal数据。

public class ThreadLocalAwareThreadPool extends ThreadPoolExecutor {

    @Override
    protected void afterExecute(Runnable r, Throwable t) {
        // Call remove on each ThreadLocal
    }
}

If we submit our requests to this implementation of ExecutorService, then we can be sure that using ThreadLocal and thread pools won’t introduce safety hazards for our application.

如果我们将请求提交给这个ExecutorService的实现,那么我们可以确信,使用ThreadLocal和线程池不会为我们的应用程序带来安全隐患。

6. Conclusion

6.结论

In this brief article, we examined the ThreadLocal construct. We implemented the logic that uses ConcurrentHashMap that was shared between threads to store the context associated with a particular userId. Then we rewrote our example to leverage ThreadLocal to store data associated with a particular userId and a particular thread.

在这篇简短的文章中,我们研究了ThreadLocal结构。我们实现了使用线程间共享的ConcurrentHashMap的逻辑,以存储与特定userId相关的上下文。然后我们重写了我们的例子,以利用ThreadLocal来存储与特定userId和特定线程相关的数据。

The implementation of all these examples and code snippets can be found over on GitHub.

所有这些例子和代码片断的实现都可以在GitHub上找到over