目录
ThreadLocal使用场景
ThreadLocal实现详解
关于内存泄漏
Strust2中的ActionContext
在Spring MVC中使用ThreadLocal
ThreadLocal使用场景
ThreadLocal的一个典型使用场景,其实就是在“同一个线程内”为了避免多个方法调用过程中传递参数的麻烦,把一些上下文数据放到线程的ThreadLocalMap中,在需要使用这些参数的地方,直接从ThreadLocalMap中获取即可。看个例子:
假设某个线程内有三个方法依次被调用,methodA()-->methodB(int b)-->methodC(int b,int c),这样设计当然没有。这里只有三个方法和三个参数,假设有十几个方法和参数呢。每个方法调用都带上这些参数,看起来很臃肿不说,写起来也麻烦。
这时就可以使用ThreadLocal,把方法定义中的公共参数部分去掉,最终变为:methodA()-->methodB()-->methodC(),在方法执行过程中 如果有需要参数的地方,直接从ThreadLocal中获取,如下图所示:
可以看到使用起来非常方便,但也不要把什么数据都往ThreadLocalMap中放,在java web应用中,一般会在过滤器或者拦截器中 收集用户(查询数据库)的上下文信息,比如用户id、用户类型等信息,并把这些信息放入ThreadLocalMap中,在后续的业务处理方法中 如果有需要用户信息的地方,直接从ThreadLocalMap中获取,而无需再去查询数据库。最后记得在返回web请求之前,调用ThreadLocal的remove方法清空ThreadLocalMap。至于为什么要这么做,下面再来详细讲解。
ThreadLocal实现详解
上面提到了Thread、ThreadLocal、ThreadLocalMap,可能会有点晕,下面来简单梳理下。
1、首先需要明确的是Thread中有一个成员变量:
ThreadLocal.ThreadLocalMap threadLocals = null;
并且可以看到在Thread类中并没有对该成员进行实例化。
2、再来看下ThreadLocalMap,ThreadLocalMap是ThreadLocal的内部类。跟普通的HashMap没什么两样,唯一需要关注的是这个map的key是ThreadLocal的弱引用类型:
static class ThreadLocalMap { //map中的节点Entry, key是ThreadLocal的弱引用类型 static class Entry extends WeakReference> { Object value; Entry(ThreadLocal> k, Object v) { super(k); value = v; } //构造方法 ThreadLocalMap(ThreadLocal> firstKey, Object firstValue) { table = new Entry[INITIAL_CAPACITY]; int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); table[i] = new Entry(firstKey, firstValue); size = 1; setThreshold(INITIAL_CAPACITY); } //省略其他方法 }
顺便提一下,对象的引用类型有强引用、软引用、弱应用、虚引用,这里不细讲,我们一般使用的都是强引用;如果一个对象是弱引用可到达,那么这个对象会被垃圾回收器接下来的回收周期销毁;但是如果是软引用可以到达,那么这个对象会停留在内存更时间上长一些。当内存不足时垃圾回收器才会回收这些软引用可到达的对象;虚引用,也称为假引用,一般用于辅助垃圾回收。
ThreadLocalMap的key是弱引用,也就是说在下次垃圾回时,如果ThreadLocal类型的key没有其他强引用会被回收,这时ThreadLocalMap中的key就变为null,但value还在,ThreadLocalMap又是被Thread强引用,只有等这个Thread线程结束被销毁时,ThreadLocalMap才会被回收。ThreadLocalMap的生命周期就是线程的生命周期,现在的java tomcat web应用,都是采用的线程池技术,也就是说这些线程会被重复利用,而不是每次都销毁。这时如果不手动清除ThreadLocalMap就会出现内存泄漏。
3、最后来看下ThreadLocal,这里最重要的三个方法是set、get、remove方法。使用方式如下:
public static void main(String[] args) { ThreadLocal th = new ThreadLocal();//创建ThreadLocal th.set(123);//把123放入ThreadLocalMap System.out.println(th.get());//从ThreadLocalMap中取值 }
首先来看ThreadLocal的set方法的实现:
public void set(T value) { Thread t = Thread.currentThread();//获取当前线程 ThreadLocalMap map = getMap(t);//获取当前线程的ThreadLocalMap if (map != null) map.set(this, value);//把值放入map,key就是ThreadLocal对象自己 else createMap(t, value);//先实例化Thread中的ThreadLocalMap,再放入 } //还记得Thread中的ThreadLocalMap为什么没有实例化么?其实是在这里实例化 void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
可以看到,ThreadLocal本身不存放数据,真实存放数据的是ThreadLocalMap,ThreadLocal只是作为这个map的key。
再来看下get方法:
public T get() { Thread t = Thread.currentThread();//获取当前线程 ThreadLocalMap map = getMap(t);//获取当前线程的ThreadLocalMap if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue();//这个方法只是对ThreadLocalMap进行实例化。 }
最后看下remove方法,调用该方法可以手动清除ThreadLocalMap:
public void remove() { ThreadLocalMap m = getMap(Thread.currentThread());//获取当前线程中的ThreadLocalMap if (m != null) m.remove(this);//调用map的remove方法。 }
到这里,应该都Thread、ThreadLocal、ThreadLocalMap的关系应该就很清楚了。
关于内存泄漏
前面提到,在线程池环境下是用ThreadLocal,ThreadLocalMap中会存在key为null的情况,出现内存泄漏。最佳实践是在线程执行开始时新建TreadLocal执行set方法放入数据,在线程执行过程中执行get方法获取数据,在线程执行结束时执行remove方法清空数据(注意不是销毁)
其实即使不执行remove手动清除,ThreadLocal也不是那么容易出现内存泄漏,导致内存溢出,当执行ThreadLocal的get、set方法时,会调用ThreadLocalMap的方法cleanSomeSlots,清除中key为null的数据,具体实现如下:
private boolean cleanSomeSlots(int i, int n) { boolean removed = false; Entry[] tab = table; int len = tab.length; do {//遍历整个map i = nextIndex(i, len); Entry e = tab[i]; if (e != null && e.get() == null) {//如果key为空,就清理这个Entry节点 n = len; removed = true; i = expungeStaleEntry(i); } } while ( (n >>>= 1) != 0); return removed; }
另外,有种观点是:被static修饰ThreadLocal如果不及时remove 更容易引起内存泄漏,而我们在使用ThreadLocal的场景中,其实经常会用static修饰。ThreadLocal的set方法是把自己作为key把数据放入ThreadLocalMap,下一次执行set方法其实key没变(ThreadLocal是static的),只是数据会被覆盖,ThreadLocalMap中的数据量并不会有多大变化,除非创建大量的static ThreadLocal对象,否则程序是不会出现内存溢出。但如果使用不当,有可能出现数据错乱,比如 线程池取出一个线程执行任务,如果上次该线程执行时没有调用ThreadLocal的remove方法,这次执行时会获取到上次执行的数据,从而出现数据错乱。所以最佳实践,还是要在线程执行结束时,执行ThreadLocal的remove方法。
但也有网友发现在本地开发工具中使用static修饰ThreadLocal,由于没有remove,多次relaod程序引起的内存泄漏,这个笔者本人没有遇到过类似的问题,暂不做评论,可以参考下面三个链接进一步了解:
http://blog.xiaohansong.com/2016/08/09/ThreadLocal-leak-analyze/
https://www.tuicool.com/articles/6BJJzin
http://blog.xiaohansong.com/2016/08/06/ThreadLocal-memory-leak/
总的来说,在使用ThreadLocal过程中,记得及时remove就不会出现问题。
Strust2中的ActionContext
我记得以前参与过一个项目的开发,项目中使用的是Strust2(现在比较流行Spring MVC),为了方便程序的任意地方都可以使用 HttpServletRequest中的请求数据,以及用户的上下文信息,采用的是Strust2的com.opensymphony.xwork2.ActionContext进行存储,其实这个类的核心就是它有一个static的TheadLocal的成员:
public class ActionContext implements Serializable { static ThreadLocalactionContext = new ThreadLocal(); //省略一堆static常量 private Map context; //构造方法 初始化map public ActionContext(Map context) { this.context = context; } //静态方法,调用ThreadLocal的 set方法,把context放入ThreadLocalMap public static void setContext(ActionContext context) { actionContext.set(context); } //静态方法,调用ThreadLocal的 get方法,从ThreadLocalMap中获取值 public static ActionContext getContext() { return (ActionContext)actionContext.get(); } //获取map中的数据 public Object get(String key) { return this.context.get(key); } //想map中放入数据 public void put(String key, Object value) { this.context.put(key, value); } //省略其他代码 }
可以看到ActionContext的本质是,使用静态的ThreadLocal作为key,把ActionContext类型的实例对象放入ThreadLocalMap。而ActionContext中又包含一个Map成员,所有ActionContext的实例对象可以存放任意多个数据项到ThreadLocalMap。同时可以看到ActionContext提供了static的静态的setContext和getContext,典型的运用场景,就是在Strust2的拦截器中setContext放入数据,在后续的方法中通过getContext方法获取数据:
public class LoginInterceptor implements Interceptor { //省略其他方法内容 protected String doIntercept(ActionInvocation invocation) throws Exception { //获取到ActionContext实例对象 ActionContext actionContext = invocation.getInvocationContext(); HttpServletRequest request = (HttpServletRequest) actionContext.get(StrutsStatics.HTTP_REQUEST); HttpServletResponse response = (HttpServletResponse) actionContext.get(StrutsStatics.HTTP_RESPONSE); Integer userId = xxx//从cookei获取到userid //查询数据库获取到当前的User信息 UserInfo user = userDao.getByid(); //放入threadLocal actionContext.put("user",user); return invocation.invoke(); } }
其他业务方法中获取user对象:
ActionContext ac = ActionContext.getContext(); User user = (User)ac.get("user");
可以看到在拦截器中,可以通过ActionContext actionContext = invocation.getInvocationContext();直接获取到ActionContext对象,那这个对象是在什么时候创建的呢,通过阅读Struts2的源码发现,其实是在StrutsPrepareAndExecuteFilter中的doFilter创建的,并在这里把request和response等信息放入了ThreadLocalMap:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest)req; HttpServletResponse response = (HttpServletResponse)res; try { this.prepare.setEncodingAndLocale(request, response); this.prepare.createActionContext(request, response);//这一步创建ActionContext对象 this.prepare.assignDispatcherToThread(); if(this.excludedPatterns != null && this.prepare.isUrlExcluded(request, this.excludedPatterns)) { chain.doFilter(request, response); } else { request = this.prepare.wrapRequest(request); ActionMapping mapping = this.prepare.findActionMapping(request, response, true); if(mapping == null) { boolean handled = this.execute.executeStaticResourceRequest(request, response); if(!handled) { chain.doFilter(request, response); } } else { this.execute.executeAction(request, response, mapping); } } } finally { this.prepare.cleanupRequest(request);//这一步销毁ActionContext对象,并清空ThreadLocalMap } }
再来看下this.prepare.cleanupRequest(request)是怎么清空ThreadLocalMap的:
public void cleanupRequest(HttpServletRequest request) { //省略部分内容 try { this.dispatcher.cleanUpRequest(request); } finally { ActionContext.setContext((ActionContext)null);//这里虽然没有调用ThreadLocal的remove方法,但效果是一样的 Dispatcher.setInstance((Dispatcher)null); } }
在Spring MVC中使用ThreadLocal
在Spring MVC中没有类似Struts2中ActionContext的Api,但阅读完上述源码后,我们完全可以实现一个自己的ActionContext,并在Filter或者spring 拦截器中做类似StrutsPrepareAndExecuteFilter中的处理逻辑。
分为两步即可完成:
1、首先新建自己的ActionContext类:
public class ActionContext implements Serializable { public static final String HTTP_REQUEST = "com.jd.ejshop.web.HttpServletRequest"; public static final String HTTP_RESPONSE = "com.jd.ejshop.web.HttpServletResponse"; private static ThreadLocalactionContext = new ThreadLocal<>(); private Map context; public ActionContext(Map context) { this.context = context; setContext(this); } public static ActionContext getContext() { return actionContext.get(); } public static void setContext(ActionContext context) { actionContext.set(context); } public Object get(String key) { return context.get(key); } public void put(String key, Object value) { context.put(key, value); } public Map getContextMap() { return context; } public void remove() { context = null; actionContext.remove(); } public HttpServletRequest getRequest() { return (HttpServletRequest) ActionContext.getContext().get(HTTP_REQUEST); } public HttpServletResponse getResponse() { return (HttpServletResponse) ActionContext.getContext().get(HTTP_RESPONSE); } }
整体上跟Struts2版本的ActionContext很类似。
再来看下User类:
public class User implements Serializable { private static final long serialVersionUID = 1281237054612354L; private static final String LOGINCONTEXT = "com.jd.ejshop.web.common.LoginContext"; private Integer userId; private String username; /** * 把登陆信息放入ThreadLocal * @param user */ public static void setLoginContext(User user){ ActionContext.getContext().put(LOGINCONTEXT,user); } /** * 从ThreadLocal中取出登陆信息 * @return */ public static User getLoginContext(){ return (User)ActionContext.getContext().get(LOGINCONTEXT); } public Integer getUserId() { return userId; } public void setUserId(Integer userId) { this.userId = userId; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } }
2、在spring 拦截器中向ActionContext放入数据,并在后续业务方法中使用:
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { ActionContext ActionContext = new ActionContext(new HashMap<>()); ActionContext.put(ActionContext.HTTP_REQUEST, request); ActionContext.put(ActionContext.HTTP_RESPONSE, response); Integer userId = xxx//从cookei获取到userid //查询数据库获取到当前的User信息 User user = userDao.getByid(); User.setLoginContext(user);//放入ThreadLocalMap return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { //清除ThreadLocalMap ActionContext.remove(); } @Override public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception { } }
在后续任意业务方法中,都可以直接从ThreadLocalMap中获取到user对象:
User user = User.getLoginContext();
好了关于ThreadLocal就总结到这里。如有不当之处,欢迎留言指正!
出处: