目录
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<ThreadLocal<?>> { 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 ThreadLocal<ActionContext> actionContext = new ThreadLocal(); //省略一堆static常量 private Map<String, Object> context; //构造方法 初始化map public ActionContext(Map<String, Object> 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 ThreadLocal<ActionContext> actionContext = new ThreadLocal<>(); private Map<String, Object> context; public ActionContext(Map<String, Object> 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<String, Object> 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就总结到这里。如有不当之处,欢迎留言指正!
出处:
相关推荐
### Java中ThreadLocal详解 #### 一、ThreadLocal概述 在Java多线程编程中,`ThreadLocal`是一个非常重要的工具类,它提供了一种在每个线程内部存储线程私有实例的方法。通常情况下,当多个线程共享某个变量时,...
Java中的ThreadLocal是一个非常重要的工具类,它在多线程编程中扮演着独特角色,用于为每个线程提供独立的变量副本。理解ThreadLocal的工作原理和使用方法对于编写高效、安全的多线程程序至关重要。 ### ...
Java中的`ThreadLocal`类是一个非常实用的工具,它提供了线程局部变量的功能。线程局部变量意味着每个线程都拥有自己独立的变量副本,互不干扰,这在多线程编程中尤其有用,可以避免数据共享带来的同步问题。下面...
java 中ThreadLocal 的正确用法 ThreadLocal 是 Java 中的一个特殊类,它可以让每个线程拥有自己独立的变量副本,避免了多线程之间的共享变量问题。下面我们将详细介绍 Java 中 ThreadLocal 的正确用法。 用法一...
"Java 中ThreadLocal实例分析" Java 中的 ThreadLocal 实例分析是指在多线程环境下,如何使用 ThreadLocal 来实现线程安全。ThreadLocal 是 Java 中的一种机制,用于在多个线程中实现变量的隔离。 在上面的代码中...
线程局部变量(ThreadLocal)其实的功用非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,是Java中一种较为特殊的线程绑定机制,是每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突...
Java事务和ThreadLocal是两种在Java编程中至关重要的概念,它们分别用于处理多线程环境下的数据一致性问题和提供线程局部变量。 首先,我们来深入理解Java事务。在数据库操作中,事务是一系列操作的集合,这些操作...
java ThreadLocal多线程专属的变量源码java ThreadLocal多线程专属的变量源码java ThreadLocal多线程专属的变量源码java ThreadLocal多线程专属的变量源码java ThreadLocal多线程专属的变量源码java ThreadLocal多...
Java中的ThreadLocal是一个非常重要的工具类,它在多线程编程中扮演着独特角色,尤其在处理线程间数据隔离和共享时。ThreadLocal不是线程本身,而是为每个线程提供一个独立的变量副本,使得每个线程都可以独立地改变...
Java中的ThreadLocal类是一个强大的工具,它允许程序员创建线程局部变量。这意味着每个线程都有其自己独立的、不可见的变量副本,从而避免了线程间的数据共享问题,简化了多线程环境下的编程复杂性。ThreadLocal并不...
在Java编程中,ThreadLocal是一个强大的工具,它允许线程拥有自己的局部变量副本,从而避免了多线程环境下的数据共享问题。然而,如果不正确地使用ThreadLocal,可能会导致内存泄露,尤其是在Java EE容器如Tomcat中...
Java中的ThreadLocal是一种特殊的技术,它为每个线程提供了一个独立的变量副本,避免了多线程间的直接共享,从而简化了并发编程的复杂性。ThreadLocal不是一种同步机制,而是设计来解决线程间数据隔离问题的工具。 ...
总之,ThreadLocal是Java中一种强大的工具,它可以提供线程安全的局部变量,帮助开发者在多线程编程中简化数据管理,同时避免了不必要的同步开销。但使用时需要注意内存泄漏问题,确保及时清理不再使用的ThreadLocal...
彻底理解Java中的ThreadLocal ThreadLocal翻译成中文比较准确的叫法应该是:线程局部变量。使用这个工具类可以很简洁地编写出优美的多线程程序。ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这...
ThreadLocal是Java中的一种机制,可以将变量与线程关联起来,使得每个线程都可以拥有自己的变量副本。 ThreadLocal的出现是为了解决多线程编程中的线程安全问题。 从本质上说,ThreadLocal是一种存储机制,它可以在...
ThreadLocal是Java中用于线程局部变量的一个工具类,它的工作原理主要体现在如何在不同的线程之间隔离变量的副本,确保每个线程拥有自己的独立数据。这个设计模式在多线程编程中尤其有用,因为它避免了传统的同步...
Java中的ThreadLocal是一个非常有用的工具类,它提供了一种线程局部变量的机制。线程局部变量(ThreadLocal)的特点是每个线程都有其独立的副本,这些副本之间互不干扰,即使它们共享同一个ThreadLocal实例。这使得...
在这个例子中,getSession()方法会检查ThreadLocal中是否存在Session,若不存在则创建并设置,而closeSession()方法则负责关闭并移除当前线程的Session副本。 总结来说,ThreadLocal是Java中解决多线程数据隔离问题...
ThreadLocal是Java中一个非常重要的线程局部变量工具类,它的设计目的是为了解决多线程环境下各个线程之间的数据隔离问题。通过ThreadLocal,每个线程都可以拥有自己独立的变量副本,避免了多线程间的数据冲突和同步...