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();@Overridepublic 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);
new Thread(firstUser).start();
new Thread(secondUser).start();assertEquals(SharedMapWithUserContext.userContextPerUserId(), 2);
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();@Overridepublic 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的标准输出:
可以得出结论,每个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 {@overrideprotected void afterExecute(Runnable r, Throwable t) {// Call remove on each ThreadLocal}
}
如果我们将请求提交到ExecutorService这个实现中,使用ThreadLocal和线程池不会给我们的应用程序带来任何安全隐患。
6. 结论
通过测试ThreadLocal构建函数,我们发现了ThreadLocal的特性,即一个线程绑定了一个ThreadLocal,ThreadLocal和线程池的组合使用存在安全隐患,并介绍了如何规避这种安全隐患。