管理好你的ThreadLocal

Posted on 2010-01-25 22:10 周舒阳 阅读(3443) 评论(4)  编辑  收藏
本期Blog原文参见:
http://www.liferay.com/web/shuyang.zhou/blog/-/blogs/master-your-threadlocals

      ThreadLocal不是解决并发问题的"银弹", 实际上许多关于并发的最佳实践并不鼓励使用它。

      但有些时候它确实是必须的,或者它能够极大程度的简化你的设计。因此我们必须正视它的存在。由于它非常容易被误用,我们必须找到一种方法来避免它导致麻烦。今天我们不是要讲该在什么时候以及如何使用ThreadLocal,而是要谈一谈当你必须要使用它时,如果能够确保它不惹大麻烦。

      开发者使用ThreadLocal时最容易犯的也是最严重的错误就是忘记重置它。假如你使用ThreadLocal来缓存用户的认证信息,用户A通过Worker Thread1登录系统,你将认证信息缓存在ThreadLocal中以提升性能。但在Worker Thread1完成对用户A的服务后你忘记了重置ThreadLocal(清空缓存)。就在这时,用户B在没有登录的情况下访问你的系统,凑巧的是它也接受了来自Worker Thread1的服务,Worker Thread1检查了一下它的缓存发现了认证信息,因此它会将用户B当作用户A来服务。你应该会想象到接下来将要发生什么。

      对于这一问题,一个立即就会想到的解决方案是在结束一个request的服务后重置ThreadLocal。但问题的难点在于一个Worker Thread可能会拥有多个ThreadLocal对象,它们散落在你程序的各个角落,如何才能轻松的将它们全部重置呢?你需要为每一个Worker Thread的所有ThreadLocal对象提供一个ThreadLocal的注册表。请注意!这个注册表本身也必须是一个ThreadLocal对象(但它不注册自身的引用),因此当一个Worker Thread重置注册表中的ThreadLocal对象时,它只会重置属于自己的ThreadLocal对象,而不是其他线程的。一旦你有了这样一个注册表,你就可以在一个request的处理结束后重置全部ThreadLocal对象了,通常是在一个filter中执行重置。现在你应该马上想到的一个问题是:我们该如何将一个ThreadLocal对象添加到注册表中呢?你当然可以在每次使用ThreadLocal后添加一行注册代码,但这样会让你的代码很丑,而且这种做法有着和原来一样的问题:如果你忘了一行注册代码怎么办?解决办法是创建一个ThreadLocal的子类,重写set()和initialValue()方法,每当这些方法被调用时,它们会将自身注册到注册表中。这样整个注册和重置的过程对于开发者而言就是透明的了,你所要做的只是使用我创建的ThreadLocal子类。

      这里列出ThreadLocal子类和注册表的代码:
 1 public class AutoResetThreadLocal<T> extends InitialThreadLocal<T> {
 2 
 3     public AutoResetThreadLocal() {
 4         this(null);
 5     }
 6 
 7     public AutoResetThreadLocal(T initialValue) {
 8         super(initialValue);
 9     }
10 
11     public void set(T value) {
12         ThreadLocalRegistry.registerThreadLocal(this);
13 
14         super.set(value);
15     }
16 
17     protected T initialValue() {
18         ThreadLocalRegistry.registerThreadLocal(this);
19 
20         return super.initialValue();
21     }
22 
23 }

 1 public class ThreadLocalRegistry {
 2 
 3     public static ThreadLocal<?>[] captureSnapshot() {
 4         Set<ThreadLocal<?>> threadLocalSet = _threadLocalSet.get();
 5 
 6         return threadLocalSet.toArray(
 7             new ThreadLocal<?>[threadLocalSet.size()]);
 8     }
 9 
10     public static void registerThreadLocal(ThreadLocal<?> threadLocal) {
11         Set<ThreadLocal<?>> threadLocalSet = _threadLocalSet.get();
12 
13         threadLocalSet.add(threadLocal);
14     }
15 
16     public static void resetThreadLocals() {
17         Set<ThreadLocal<?>> threadLocalSet = _threadLocalSet.get();
18 
19         for (ThreadLocal<?> threadLocal : threadLocalSet) {
20             threadLocal.remove();
21         }
22     }
23 
24     private static ThreadLocal<Set<ThreadLocal<?>>> _threadLocalSet =
25         new InitialThreadLocal<Set<ThreadLocal<?>>>(
26             new HashSet<ThreadLocal<?>>());
27 
28 }

      这里提供一个示意图来展示注册与重置的流程:

     
      这里给大家提供一些建议:
  1. 不管你如何使用ThreadLocal,请不要忘记重置它。
  2. 当你的ThreadLocal对象的有效期局限在一次请求中(或者是其他的周期性时间段中),你可以尝试使用AutoResetThreadLocal和ThreadLocalRegistry来简化你的代码。
  3. 请注意!你还是需要在什么地方调用一下ThreadLocalRegistry.resetThreadLocals()的(通常是在一个filter中)。

补充说明!
      细心的读者可能已经发现了,ThreadLocalRegistry.resetThreadLocals(),只是重置已注册的ThreadLocal对象,并没有将它们从注册表中移除。你可能会担心这样的注册表只会越长越大,最终导致内存泄漏。
      本文开篇时我就有说明,这里不讲该如果使用ThreadLocal,但为了解释这一问题还是要说明一个ThreadLocal的最佳实践的。在Liferay中,所有的ThreadLocal对象都是static的,也就是说一旦使用ThreadLocal的类的数量确定了,一个线程可能使用到的最大ThreadLocal对象数量也就确定了。而且这个数字在Liferay中是相对比较小的,因此这个注册表不存在无限增长的问题。
我确实见过有人不将ThreadLocal设置为static,大部分情况是打字漏掉了。如果你是存心这样使用,建议你该重新思考一下你的设计了。
总之,推荐大家始终将ThreadLocal设置为static的。如果你确实有需要使用非static的ThreadLocal,你可以在ThreadLocalRegistry.resetThreadLocals() 的最后填上一行语句_threadLocalSet.get().clear();这样可以确保不会产生内存泄漏,但也增加了一些开销。
      这里我提供了一个消除了对Liferay其他类文件依赖的ThreadLocalRegistry供大家下载使用。
      http://www.blogjava.net/Files/ShuyangZhou/ThreadLocalRegistry/src.zip

Feedback

# re: 管理好你的ThreadLocal  回复  更多评论   

2010-01-26 13:43 by JiangMin
我就喜欢看楼主这样的文章!

# re: 管理好你的ThreadLocal  回复  更多评论   

2010-01-27 20:08 by john locke
写的不错

# re: 管理好你的ThreadLocal  回复  更多评论   

2010-02-01 16:06 by yefeng
我想问个问题,ThreadLocal是线程安全的呀,应该不会有你这样问题啊

# re: 管理好你的ThreadLocal  回复  更多评论   

2010-02-01 16:17 by 周舒阳
@yefeng
这跟线程安全与否无关,这里描述的是当你的ThreadLocal变量逃离了它的作用域时会引起的问题,你仍然是在同一个线程的上下文下,但作用域已经改变了。你可以将ThreadLocal理解为一个线程内的全局变量,但你的应用规定这个ThreadLocal存在一定的逻辑作用域(比如一个request的处理),当你跨作用域传递它而又不进行重置操作的话就可能会引起问题。ThreadLocalRegistry的目的是提供集中的重置处理,以防止由于“马虎”引起的问题。

只有注册用户登录后才能发表评论。


网站导航:
 

posts - 3, comments - 15, trackbacks - 0, articles - 0

Copyright © 周舒阳