Java中ThreadLocal简要介绍

1. 简介

在这篇文章中,我们将会介绍ThreadLocal在java.lang这个包中是如何被构造的。ThreadLocal提供了在当前线程中单独存储数据,并能将特殊类型的对象保存在ThreadLocal里面的能力。

2. ThreadLocal API

ThreadLocal构造函数允许存储一个只能在特定线程中才能获取的数据。
比如我们想让一个Integer类型的数据和一个特定线程绑定在一起:

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

当我们想在这个线程中使用这个数据时,可以调用set方法或者get方法,简单来说,ThreadLocal存储数据就是将线程作为map中的key。正是因为这个原因,我们可以在threadLocal中调用get方法获取Integer值。

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

ThreadLocal可以使用withInitial()静态方法通过传递supplier方法的lambda方式来实现初始化,写法如下:

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

我们可以调用remove方法来移除ThreadLocal中的值

threadLocal.remove()

为了充分的了解使用ThreadLocal,我们首先需要看一下不使用ThreadLocal的例子,然后我们将会利用ThreadLocal的构造函数来重写我们的例子。

3. 在一个Map中存储用户的数据

考虑一个特定的程序,需要存储给定userId用户的上下文数据:

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

给每个userId都赋予一个线程,通过实现Runnable接口来创建一个SharedMapWithUserContext类,run函数的实现中,通过调用UserRepository类中方法,根据参数userId,返回一个Context的对象。
接着,我们将userId作为ConcurrentHashMap中的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
  }

我们可以为两个不同userId创建两个不同的线程,通过调用线程中的start方法,并且使用断言判断userContextPerUserId这个map中有两个条目。

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

4. 在ThreadLocal中存储用户数据

我们可以使用ThreadLocal重写我们的例子,每个线程都会有他们自身的ThreadLocal实例。
当使用ThreadLocal时,需要十分小心,因为每个ThreadLocal实例需要和一个特定的线程关联起来,在我们的例子中,我们为每一个userId设置特定的线程,这个线程是由我们自己产生的,所以我们能完全控制此线程。
在run方法中将会获取每个user的上下文,并且将上下文使用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: ThreadLocal.Context@327d19c0
thread context for given userId: 2 is: ThreadLocal.Context@7e3a5d26

可以得出结论,每个user都有他们自己的上下文。

5. ThreadLocals和Thread池

ThreadLocal提供了一系列很容易使用的API来限制某个线程中的某些值,目的是为了保证java线程中的安全。
我们在同时使用ThreadLocals和线程池大的过程中需要十分的小心。
为了能够更好的理解可能存在的场景,考虑一下如下的场景:
1、首先,应用从线程池中拿到一个线程。
2、然后存储一些线程限制的值到当前线程的ThreadLocal。
3、一旦当前线程的执行过程结束,应用需要将取到的线程返回到线程池。
4、过了一会,应用获取到相同的线程并且执行另外的请求。
5、如果应用没有执行cleanup方法,那么应用就会在新的请求中使用相同的ThreadLocal数据。
这种场景在高并发场景中将会造成令人意外的结果。
为了避免出现这种问题,我们需要在不使用ThreadLocal的时候,移除掉ThreadLocal中缓存的内容,实践中需要严格的代码审视,才能排查出这类问题。

5.1 扩展ThreadPoolExecutor

可以通过扩展ThreadPoolExecutor类并且提供beforeExecute和afterExecute方法的实现,来解决上述的问题。线程池将在使用获得的线程运行任何方法前执行beforeExecute方法,在执行完我们的逻辑后,就会调用afterExecute方法。
因此,我们将会扩展ThreadPoolExecutor类,并且在afterExecute方法中清除ThreadLocal数据:

public class ThreadLocalAwareThreadPool extends ThreadPoolExecutor {
    
    
  @override
  protected void afterExecute(Runnable r, Throwable t) {
    
    
    // Call remove on each ThreadLocal
  }
}

如果我们将请求提交到ExecutorService这个实现中,使用ThreadLocal和线程池不会给我们的应用程序带来任何安全隐患。

6. 结论

通过测试ThreadLocal构建函数,我们发现了ThreadLocal的特性,即一个线程绑定了一个ThreadLocal,ThreadLocal和线程池的组合使用存在安全隐患,并介绍了如何规避这种安全隐患。
相关代码见github

猜你喜欢

转载自blog.csdn.net/sinat_28199083/article/details/131616239