管理好你的ThreadLocal

本期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

你可能感兴趣的:(管理好你的ThreadLocal)