Posted on 2010-01-25 22:10
周舒阳 阅读(3442)
评论(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 }
这里提供一个示意图来展示注册与重置的流程:
这里给大家提供一些建议:
- 不管你如何使用ThreadLocal,请不要忘记重置它。
- 当你的ThreadLocal对象的有效期局限在一次请求中(或者是其他的周期性时间段中),你可以尝试使用AutoResetThreadLocal和ThreadLocalRegistry来简化你的代码。
- 请注意!你还是需要在什么地方调用一下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