使用ThreadLocal

对于多任务,Java标准库提供的线程池可以方便地执行这些任务,同时复用线程。Web应用程序就是典型的多任务应用,每个用户请求页面时,我们都会创建一个任务,类似:

然后,通过线程池去执行这些任务。

观察process()方法,它内部需要调用若干其他方法,同时,我们遇到一个问题:如何在一个线程内传递状态?

process()方法需要传递的状态就是User实例。有的童鞋会想,简单地传入User就可以了:

  1. public void process(User user) {
  2. checkPermission(user);
  3. doWork(user);
  4. saveStatus(user);
  5. sendResponse(user);
  6. }

但是往往一个方法又会调用其他很多方法,这样会导致User传递到所有地方:

  1. void doWork(User user) {
  2. queryStatus(user);
  3. checkStatus();
  4. setNewStatus(user);
  5. log();
  6. }

这种在一个线程中,横跨若干方法调用,需要传递的对象,我们通常称之为上下文(Context),它是一种状态,可以是用户身份、任务信息等。

Java标准库提供了一个特殊的ThreadLocal,它可以在一个线程中传递同一个对象。

ThreadLocal实例通常总是以静态字段初始化如下:

它的典型使用方式如下:

  1. void processUser(user) {
  2. try {
  3. threadLocalUser.set(user);
  4. step1();
  5. step2();
  6. threadLocalUser.remove();
  7. }
  8. }

通过设置一个User实例关联到ThreadLocal中,在移除之前,所有方法都可以随时获取到该User实例:

  1. User u = threadLocalUser.get();
  2. log();
  3. printUser();
  4. }
  5. void log() {
  6. User u = threadLocalUser.get();
  7. println(u.name);
  8. }
  9. void step2() {
  10. User u = threadLocalUser.get();
  11. checkUser(u.id);
  12. }

注意到普通的方法调用一定是同一个线程执行的,所以,step1()step2()以及log()方法内,threadLocalUser.get()获取的User对象是同一个实例。

实际上,可以把ThreadLocal看成一个全局Map<Thread, Object>:每个线程获取ThreadLocal变量时,总是使用Thread自身作为key:

因此,ThreadLocal相当于给每个线程都开辟了一个独立的存储空间,各个线程的ThreadLocal关联的实例互不干扰。

  1. try {
  2. threadLocalUser.set(user);
  3. } finally {
  4. threadLocalUser.remove();
  5. }

这是因为当前线程执行完相关代码后,很可能会被重新放入线程池中,如果没有被清除,该线程执行其他代码时,会把上一次的状态带进去。

为了保证能释放ThreadLocal关联的实例,我们可以通过AutoCloseable接口配合try (resource) {…}结构,让编译器自动为我们关闭。例如,一个保存了当前用户名的ThreadLocal可以封装为一个UserContext对象:

  1. public class UserContext implements AutoCloseable {
  2. static final ThreadLocal<String> ctx = new ThreadLocal<>();
  3. public UserContext(String user) {
  4. ctx.set(user);
  5. }
  6. public static String currentUser() {
  7. return ctx.get();
  8. }
  9. @Override
  10. public void close() {
  11. ctx.remove();
  12. }
  13. }

使用的时候,我们借助try (resource) {…}结构,可以这么写:

这样就在UserContext中完全封装了ThreadLocal,外部代码在try (resource) {…}内部可以随时调用UserContext.currentUser()获取当前线程绑定的用户名。

下载练习:ThreadLocal练习 (推荐使用快速下载)

ThreadLocal表示线程的“局部变量”,它确保每个线程的ThreadLocal变量都是各自独立的;

ThreadLocal适合在一个线程的处理流程中保持上下文(避免了同一参数在所有方法中传递);

使用ThreadLocal - 图1