前言
之前写过一篇关于[SimpleDateFormat线程安全问题](link: http://zhengyk.cn/2017/10/31/java/Thread_sdf/)的记录博客,里面提到了使用 ThreadLocal 来保证 SimpleDateFormat 线程安全,这是一种比在每个方法里都 new SimpleDateFormat更优雅的方式,当然现在操作时间推荐使用 joda-time ,joda-time线程安全且功能丰富、性能高。
其实网上介绍 ThreadLocal的文章也是很多很多,但我觉得自己还是得总结下来,一是记录自己的理解,二是方便以后查漏补缺。
那么ThreadLocal 是什么呢?
ThreadLocal
ThreadLocal官话解释是线程本地存储,它为变量在每个线程中都创建了一个副本,那么每个线程访问自己的变量副本,自然实现了变量的线程隔离。
代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public class ThreadLocalTest {
private static final ThreadLocal<SimpleDateFormat> THREAD_LOCAL_A = new ThreadLocal<SimpleDateFormat>(){ @Override protected SimpleDateFormat initialValue() { return new SimpleDateFormat("yyyyMMdd"); } };
private static final ThreadLocal<SimpleDateFormat> THREAD_LOCAL_B = new ThreadLocal<SimpleDateFormat>(){ @Override protected SimpleDateFormat initialValue() { return new SimpleDateFormat("yyyyMMdd HH:mm:ss"); } };
}
|
结合 ThreadLocal 和 Thread 的源码,草画了一张ThreadLocal 的结构图:

每个Thread内有一个 ThreadLocalMap,这个 ThreadLocalMap 的 每个 key 就是 每个ThreadLocal 对象(THREAD_LOCAL_A、THREAD_LOCAL_B),value 是变量副本。
1 2
| ThreadLocal.ThreadLocalMap threadLocals = null;
|
ThreadLocalMap 是 ThreadLocal 里的静态内部类,ThreadLocalMap由 ThreadLocal 来操作。
这样,每个线程都有属于自己的变量副本,实现了变量线程隔离。
一些常用方法
void set(T value)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); }
ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
|
T get()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { T result = (T)e.value; return result; } } return setInitialValue(); }
private T setInitialValue() { T value = initialValue(); Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); return value; }
protected T initialValue() { return null; }
|
ThreadLocalMap
前面提到 ThreadLocalMap 是 ThreadLocal 的静态内部类,key 是 ThreadLocal对象本身,value 是变量副本
1 2 3 4 5 6 7 8 9 10 11 12
| static class ThreadLocalMap { static class Entry extends WeakReference<ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } .... }
|
可以看到 ThreadLocalMap使用 Entry 来存储 K-V,而且这个 Entry 的 key 是弱引用,下次 GC 时要被回收,那就可能存在内存泄露问题。
**ThreadLocal存在的内存泄露问题:**由于ThreadLocalMap 存在于 Thread 中,和线程的生命周期一样,但是 ThreadLocal 在类里,往往 ThreadLocal 的生命周期比ThreadLocalMap要长(ThreadLocal 声明为 static 后生命周期会更长),由于Entry 使用ThreadLocal的弱引用,当 ThreadLocal 没有外部的强引用引用它时,下次 GC ThreadLocal 弱引用会被回收,而 value 是强引用,所以如果当前线程一直不结束,那么ThreadLocalMap 中就会存在key 为 null的数据,造成内存泄露。
**避免内存泄露:**用完后及时调用 remove 方法。
ThreadLocal 实践
众所周知,可以利用Spring 的 AOP 来记录日志,有环绕通知、前置通知、后置通知、异常通知等,加入我想实现一个记录某个方法执行的时间,使用环绕通知是可以解决的,那如果使用前置通知和后置通知呢?
前置通知和后置通知是不在同一个方法里的,所以不能像环绕通知那样在执行方法前后记录时间。
这时候可以利用 ThreadLocal 来实现。
代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public class RecordTime {
private static final Logger logger = LoggerFactory.getLogger(RecordTime.class);
private static final ThreadLocal<Long> recordTime = new ThreadLocal<>();
@Before("within(com.*..*)") public void addBeforeLogger(JoinPoint joinPoint) { recordTime.set(System.currentTimeMillis()); }
@AfterReturning(returning = "rnt", pointcut = "within(com.*..*)") public void addAfterReturningLogger(JoinPoint joinPoint, Object rnt) { Long time = System.currentTimeMillis() - recordTime.get(); logger.info("耗时:"+ time +"毫秒\r\n"); recordTime.remove(); } }
|
ThreadLocal 在框架中的应用
在 Spring 框架中,很多地方使用到了 ThreadLocal,如 **RequestContextHolder **请求上下文类,当我们在 service 层用到 request 或 controller 时,可以直接从 controller 层传递request、response 到 service 层,但是这样的话方法参数变多,代码不简洁。
其实 Spring 提供的RequestContextHolder类,可以让我们直接在service 层中获取 request 和 response
代码实例:
1 2 3 4 5
| RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
HttpServletRequest request = ((ServletRequestAttributes)requestAttributes).getRequest();
HttpServletResponse response = ((ServletRequestAttributes)requestAttributes).getResponse();
|
我们只要封装一个获取 request 和response 的方法抽取去 BaseService 中即可,这样使用更为优雅、简洁。
RequestContextHolder 就是利用 ThreadLocal 与当前请求的线程绑定来实现的:
1 2 3 4 5 6
| public abstract class RequestContextHolder { private static final ThreadLocal<RequestAttributes> requestAttributesHolder = new NamedThreadLocal("Request attributes"); private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder = new NamedInheritableThreadLocal("Request context"); ... }
|