1、概览
在本文中,我们将研究下ThreadLocal。ThreadLocal为我们提供了为当前线程单独保存数据的能力,并把它包装在一个特殊的对象类型里面。
2、ThreadLocal 的API
ThreadLocal这种结构体使得我们可以存储一些只能被特定线程访问的数据。
比如说,我们想存储一个Integer值,这个Integer值将和某个特定的线程绑定在一起。
ThreadLocal<Integer> threadLocalValue = new ThreadLocal<>();
当我们在某个线程中,要使用该值的时候,我们只需要调用get()或set()方法即可。简单点说,我们可以认为ThreadLocal是把数据存储到了一个Map中了,而键就是当前线程。
正是基于这样事实,当我们调用threadLocalValue的get()方法时,我们获取到的是所请求线程的ThreadLocal中存储的值。
threadLocalValue.set(1);
Integer result = threadLocalValue.get();
使用withInitial()这个静态方法并传递一个初始值,我们可以直接构造一个ThreadLocal实例。
要移除ThreadLocal中的值,我们可以调用remove方法:
threadLocal.remove();
要想知道如何正确地使用ThreadLocal,首先,我们先看一个例子,在这个例子中,我们先不使用ThreadLocal,之后,我们重写此案例,并利用ThreadLocal改写它。
3、把用户数据存储在Map中
我们设想有一个程序,它需要为每个给定的用户,存储该用户特定的上下文数据。
public class Context {
private String userName;
public Context(String userName) {
this.userName = userName;
}
}
我们想让每个用户都有一个线程。我们创建一个SharedMapWithUserContext类,这个类实现了Runnable接口。
在run方法的实现中,会调用UserRepository查询给定用户的上下文信息。
下一步,我们就把上下文数据存储到ConcurrentHashMap中。
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创建俩个线程,并判断userConotextPerUserId中有俩个entry。
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中
我们可以重写上面的案例,把用户的Context实例存储在ThreadLocal中。每一个线程都有它自己的ThreadLocal实例。
在使用ThreadLocal时,我们需要额外的小心。因为每一个ThreadLocal实例都和一个特定的线程相关。在我们的案例中,每一个userId都有一个专门的线程。run()方法会获取用户的上下文,并把它存储在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
}
我们启动来个线程,来检查程序的执行情况。
ThreadLocalWithUserContext firstUser
= new ThreadLocalWithUserContext(1);
ThreadLocalWithUserContext secondUser
= new ThreadLocalWithUserContext(2);
new Thread(firstUser).start();
new Thread(secondUser).start();
在运行完值,我们将在控制台看到如下输出:
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、不要把ThreadLocal和ExecutorService一块使用
如果我们想使用ExecutorService,并提交一个Runnable给它。使用ThreadLocal的话,可能产生不确定结果。因为我们无法保证,指定用户的Runnalbe行为在每次执行时,都是由相同的线程运行的。
由于这个原因,ThreadLocal就可能会在不同的userId间共享。这就是为什么我们不能把ThreadLocal和ExecutorService一起使用的原因。只有当我们可以完全控制,由哪一个线程运行我们的任务时,我们才能使用ThreadLocal。
6、总结
在这篇文章中,我们看了ThreadLocal的用法。我们先是使用在多个线程间共享的ConcurrentHashMap来存储特定用户的上下文数据。之后,我们重写了该案例,并使用ThreadLocal来存储数据,把特定的userId存储在特定线程的ThreadLocal中。
网友评论