1.概览
在这篇文章中,我们将看一下java.lang包下的ThreadLocal类。ThreadLocal能让我们有能力单独地为当前线程(current thread)存储数据-----并且简单地把它包装在一个对象的指定类型中。
2.ThreadLocal API
ThreadLocal的概念允许我们去存储那些只能被一个特定线程访问的数据.例如,我们想有一个和指定线程绑定的Integer值。
ThreadLocal threadLocalValue = new ThreadLocal<>();
下一步,当我们在一个线程中使用该值得时候,我们只需调用get()或set()方法,更简单地说,就是,我们可以这样认为:ThreadLocal是把数据存储在一个map中----只不过是使用当前线程作为key键。
由于这样的事实,当我们调用get()方法获取一threadLocalValue时,我们得到一个针对当前线程的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数据:
publicclassContext {
privateString userName;
publicContext(String userName) {
this.userName = userName;
}
}
我们想为每一个用户id生成一个thread,我们将创建一个SharedMapWithUserContext类,这个类会实现Runnable接口。 run方法会通过UserReposity类来调用某一个数据库,它会为每一个指定的userId返回一个上下文对象。紧接着,我们把这个context对象存储到ConcurrentHashMap中,并以userId作为key:
publicclassSharedMapWithUserContext implementsRunnable {
publicstaticMap userContextPerUserId = newConcurrentHashMap<>();
privateInteger userId;
privateUserRepository userRepository = newUserRepository();
@Override
publicvoidrun() {
String userName = userRepository.getUserNameForUserId(userId);
userContextPerUserId.put(userId, newContext(userName));
}
// standard constructor
}
通过针对俩个不同的userId创建并启动俩个线程,我们就能够测试我们的代码,并断言我们在userContextPerUserId 这个map中有俩个入口:
SharedMapWithUserContext firstUser = newSharedMapWithUserContext(1);
SharedMapWithUserContext secondUser = newSharedMapWithUserContext(2);
newThread(firstUser).start();
newThread(secondUser).start();
assertEquals(SharedMapWithUserContext.userContextPerUserId.size(), 2);
4.把用户数据存储到ThreadLocal中
我们可以重写我们的案例,把用户context实例存入ThreadLocal中,每一个线程都将有它自己的ThreadLocal实例。
在使用ThreadLocal时,我们需要格外小心,因为每一个ThreadLocal实例都和一个特定的线程关联。在我们的例子中,针对每一个用户id,我们都有一个专门的线程,并且这个线程是由我们创建的,因此,我们对该线程拥有完全的控制权。
run()方法会抓取用户context并使用set方法把它存储到ThreadLocal变量中:
public class ThreadLocalWithUserContext implementsRunnable {
privatestaticThreadLocal userContext = newThreadLocal<>();
privateInteger userId;
privateUserRepository userRepository = newUserRepository();
@Override
publicvoidrun() {
String userName = userRepository.getUserNameForUserId(userId);
userContext.set(newContext(userName));
System.out.println("thread context for given userId: "
+ userId + " is: "+ userContext.get());
}
// standard constructor
}
我们可以测试一下,开启俩个线程,它们会为给定的userId执行动作:
ThreadLocalWithUserContext firstUser = newThreadLocalWithUserContext(1);
ThreadLocalWithUserContext secondUser = newThreadLocalWithUserContext(2);
newThread(firstUser).start();
newThread(secondUser).start();
在运行完这段代码之后,我们将会看到在控制台看到,针对每一个给定的线程,ThreadLocal都被设置进值了:
thread context forgiven userId: 1is: Context{userNameSecret='18a78f8e-24d2-4abf-91d6-79eaa198123f'}
thread context forgiven userId: 2is: Context{userNameSecret='e19f6a0a-253e-423e-8b2b-bca1f471ae5c'}
根据上面的运行结果,我们能够看到,每一个用户都有它自己的Context。
5.不要把ThreadLocal和ExecutorService一块使用
如果我们想使用ExecutorService并且提交了一个Runnable给它,使用ThreadLocal将会产生非确定的结果---因为我们无法保证,每一个Runnable动作在它每次执行时都能被同一个线程处理。
由于这样的原因,我们的ThreadLocal会被不同的userId共享,这也就是为什么我们不应该把ThreadLocal和ExecutorService一起使用。只有当我们能完全控制让哪个线程执行哪个runnable动作时,我们才能使用ThreadLocal.
6.结论
在这篇文章中,我们看了ThreadLocal的构造,我们实现了使用了在线程间共享的ConcurrentHashMap去存储和指定userId相关联的context。
之后,我们使用ThreadLocal去重写了我们的案例。
上面所有案例的代码都可以在GitHub链接地址上找到-----这是一个maven项目,所以非常容易导入并运行。