java 中 ThreadLocal 介绍

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/neweastsun/article/details/83547953

java 中 ThreadLocal 介绍

本文我们介绍java.lang包中提供的ThreadLocal类。其能够在当前线程中存储数据——简单地在特定类型对象中包装数据。

ThreadLocal API

ThreadLocal对象允许我们存储只能被特定线程访问的数据。加入有integer型值需要被绑定至特定线程:

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

接下来当我们从前线程使用该值,仅需要调用get()或set()方法。简言之,ThreadLocal存储数据在内部map中,并使用线程作为key。基于此,当在threadLocalValue上调用get()方法,获得当前请求对象的Integer值。

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

我们也可以通过withInitial方法定义ThreadLocal实例,参数为supplier接口函数:

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

从ThreadLocal中删除值,需调用remove方法:

threadLocal.remove();

为了更好地使用ThreadLocal,我们首先不使用ThreadLocal实现一个示例,然后在利用其进行重构,通过对比了解其优势。

在map 中存储数据

假设程序需要存储每个给定user id对应的用户特定类型的Context数据:

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

每个user id对应一个线程。我们创建SharedMapWithUserContext类实现Runnable接口。run方法通过调用UserRepository类获取给定userId的Context对象,然后在ConcurentHashMap 存储context对象,使用userId作为key:

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
}

我们很容易通过创建并启动两个不同线程进行测试,测试代码如下:

SharedMapWithUserContext firstUser = new SharedMapWithUserContext(1);
SharedMapWithUserContext secondUser = new SharedMapWithUserContext(2);
new Thread(firstUser).start();
new Thread(secondUser).start();
 
assertEquals(SharedMapWithUserContext.userContextPerUserId.size(), 2);

在ThreadLocal中存储数据

下面使用ThreadLocal重写存储用Context实例数据。每个线程都拥有自己的ThreadLocal对象。
使用ThreadLocal时需注意,因为每个ThreadLocal实例仅与一个特定的线程进行关联。在我们的示例中,每个userId都有一个专门的线程,这个线程是由我们创建的,所以我们可以完全控制它。
run方法中获取用户context对象并通过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
}

下面开启两个线程进行测试,每个线程执行给定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。

不要在ExecutorService中使用ThreadLocal

如果我们想使用ExecutorService 并提及一个Runnabel任务,ThreadLocal将获得不确定的结果——因为我们不能保证每个给定userId的Runnable任务每次都被相同的线程处理。

因此,ThreadLocal将在不同userId直接共享。所以不要在ExecutorService中使用ThreadLocal,仅在我们能够完全控制线程执行具体任务时使用。

总结

本文我们看了如何使用ThreadLocal类。使用两种不同的方式实现相同的示例,通过对比进行说明。

猜你喜欢

转载自blog.csdn.net/neweastsun/article/details/83547953