Java中ThreadLocal那点事

1.概述

在本文中,我们将从java.lang包中查看ThreadLocal 构造。我们能够用它为当前线程单独存储数据 - 并将其简单地包装在特殊类型的对象中。

2. ThreadLocal API

该TheadLocal结构使我们能够存储数据,这将是访问只通过一个特定的线程。

假设我们想要一个与特定线程捆绑在一起的Integer值:

ThreadLocal threadLocalValue = new ThreadLocal<>();

接下来,当我们想要从线程中使用此值时,我们只需要调用get()或set()方法。简而言之,我们可以认为ThreadLocal将数据存储在Map内部 - 以线程为键。

由于这个事实,当我们在threadLocalValue上调用get()方法时,我们将获得请求线程的Integer值:

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

我们可以使用withInitial()静态方法并将supplier传递给它来构造ThreadLocal的实例:

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

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

threadLocal.remove();

要了解如何正确使用ThreadLocal,首先,我们将看一个不使用ThreadLocal的示例,然后我们将利用该对象来重写我们的示例。

3.在Map中存储用户数据

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

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

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

接下来,我们将该上下文存储在userId键入的ConcurentHashMap中:

public class SharedMapWithUserContext implements Runnable {
  
    public static Map 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
}

我们可以通过为两个不同的userId创建和启动两个线程并声明我们在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重写我们的示例以存储用户Context实例。每个线程都有自己的ThreadLocal实例。

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

该 run() 方法将获取的用户上下文,并使用set() 方法将其存储到所述的ThreadLocal变量中:

public class ThreadLocalWithUserContext implements Runnable {
  
    private static ThreadLocal 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
}

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

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'}

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

5.不要将ThreadLocal与ExecutorService一起使用

如果我们想要使用ExecutorService并向其提交Runnable,则使用ThreadLocal将产生非确定性结果 - 因为我们无法保证每次执行时给定userId的每个Runnable操作都将由同一个线程处理。

因此,我们的ThreadLocal将在不同的userId之间共享。这就是为什么我们不应该将TheadLocal与ExecutorService一起使用。它应该仅在我们完全控制哪个线程将选择要执行哪个可运行动作时使用。

6.结论

在这篇快速文章中,我们查看了ThreadLocal构造。我们实现了使用在线程之间共享的ConcurrentHashMap的逻辑来存储与特定userId相关联的上下文。接下来,我们重写了我们的示例,以利用ThreadLocal存储与特定userId和特定线程相关联的数据。


关注公众号:「Java知己」,每天更新Java知识哦,期待你的到来!

  • 发送「1024」,免费领取 30 本经典编程书籍。
  • 发送「Group」,与 10 万程序员一起进步。
  • 发送「JavaEE实战」,领取《JavaEE实战》系列视频教程。
  • 发送「玩转算法」,领取《玩转算法》系列视频教程。
    Java中ThreadLocal那点事_第1张图片

你可能感兴趣的:(Java并发那些事(基础篇),Java知己)