java中的ThreadLocal

阅读更多

目录

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中获取,如下图所示:

 


java中的ThreadLocal_第1张图片

可以看到使用起来非常方便,但也不要把什么数据都往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 ThreadLocal actionContext = 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 ThreadLocal actionContext = 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就总结到这里。如有不当之处,欢迎留言指正!

 

 

出处:

http://moon-walker.iteye.com/blog/2397926

  • java中的ThreadLocal_第2张图片
  • 大小: 34.3 KB
  • 查看图片附件

你可能感兴趣的:(ThreadLocal,ThreadLocalMap)