美文网首页
简述Java中的ThreadLocal使用方法

简述Java中的ThreadLocal使用方法

作者: HelloMin | 来源:发表于2018-12-10 16:37 被阅读9次

本文为译文,原文链接:https://www.baeldung.com/java-threadlocal。原文简单易懂,推荐阅读。

1. 引言

在这篇文章中,我们主要关注的是java.lang包中的ThreadLocal 类,它可以用来为当前的线程独立的保存数据,而我们只需要用一个ThreadLocal对象把数据封装起来就好了。

2. ThreadLocal API

TheadLocal的构造使得我们可以存储只有特定线程才能访问的数据。
比如,如果我们想要存储一个和特定线程相绑定的整数,可以这么做:

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

之后,当我们想要使用这个线程的数据,只需要调用get()或者set()方法。其机制可以简单理解为,ThreadLocal将数据保存进了一张以线程作为查找键key的Map表格中。
基于以上的描述,当我们对变量threadLocalValue调用get()函数,就可以获得该线程下的一个整数。

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

我们还可以通过ThreadLocal的静态方法withInitial() 构造一个ThreadLocal对象,同时传给它一个初值:

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

除了获取,如果需要删除一个值,我们可以使用remove函数:

threadLocal.remove();

为了了解如何恰当的使用ThreadLocal,首先,我们需要看一个没有使用ThreadLocal的例子,然后重写这个例子,从而更好的理解ThreadLocal的结构。

3. 用Map保存用户数据

假设我们需要一个程序,用于保存与特定用户相关的上下文数据,并且使用用户的id来访问这些数据

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

我们希望每个用户id对应着一个线程。为此,我们创建了SharedMapWithUserContext类,这是一个实现了Runnable接口的类,其中run()函数的实现调用了UserRepository类中的数据库,从而返回一个特定用户id的上下文对象。
之后,我们将用户的上下文保存在ConcurentHashMap中,并且以用户id作为查找的键。

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
}

我们可以简单测试一下我们的代码:只需创建并且运行两个线程,分别赋予它们不同的用户id,然后判断我们已经在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. 使用ThreadLocal保存用户数据

我们可以使用ThreadLocal来保存用户上下文对象,重写上面的例子。每个线程都有自己独立的ThreadLocal对象。
使用ThreadLocal需要我们加倍细心,因为每个ThreadLocal对象都是与一个特定的线程相关联的。在我们的例子里,我们给每个特定用户id创建了一个专有的线程,我们创建了这些线程,也因此对它们有着完整的权限。
我们在run( )函数中获取用户上下文,并使用ThreadLocal的set()函数把它保存起来:

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

为了测试,我们可以创建两个线程,它们都会对自己的用户id执行上文的操作:

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

运行以上代码之后,我们会在标准输出中看到,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'}

可以看出,每个用户都有其特定的上下文。

5. 不要在ExecutorService中使用ThreadLocal

如果我们想使用一个ExecutorService,并提交一个Runnable任务给它,同时使用ThreadLocal可能会导致无法预料的结果。因为我们无法确定,是否特定userId对应的所有Runnable任务,每次被执行的时候,都是同一个线程在执行。

也因为每个线程都对应着不同的用户id,我们的ThreadLocal会被不同的用户id共享。因此,我们不应同时使用TheadLocal和ExecutorService。只有在我们确定哪个线程会执行哪个runnable任务的时候,才可以使用ThreadLocal。

6. 小结

在本文中,我们学习了ThreadLocal的构造,并实现了一个使用共享的ConcurrentHashMap来为多个线程保存特定用户id上下文的逻辑。
之后,我们重写了这段代码,使用ThreadLocal来保存数据,从而保证数据只为特定的线程,特定的用户id使用。

以上所有的示例代码都可以在 GitHub project中找到– 这是一个Maven项目,所以可以简单的被引用和运行。

相关文章

网友评论

      本文标题:简述Java中的ThreadLocal使用方法

      本文链接:https://www.haomeiwen.com/subject/otkrhqtx.html