正确理解Threadlocal类以及内存泄漏问题

2017.8.5更新:

tomcat低版本在reload或者stop一个web app时(一个tomcat可以运行多个web app),如果tomcat线程池中的线程中的threadlocalmap持有了由某个web app classloader加载的类,那么该web app classloader是无法被回收的,在tomcat7.0中,引入了ThreadLocalLeakPreventionListener , 该类需要配置在server.xml中,当reload的时候,会触发此listener,该listener的做法是,将线程池中存活的线程renew一遍,即把它们杀死,然后再新建数量相同的线程。怎么杀死呢,就是让线程抛出个运行时异常即可。

apache ThreadLocalLeakPreventionListener

https://wiki.apache.org/tomcat/MemoryLeakProtection

推荐阅读这篇文章

反驳:Threadlocal存在内存泄露

这篇文章分析的很不错,我也非常同意作者的观点,但是他并未分析到线程池与 Threadlocal 类结合使用的问题。

因为在绝大部分使用Java作为后端架构的互联网公司中,会将web项目部署到web容器中,最常见的就是tomcat了。而web容器中是有专门的线程池来管理线程的创建、执行以及销毁的。通常情况下,web容器会在启动时初始化一部分线程,放入线程池,当有请求到达时,会从线程池中取出一个线程来执行任务,执行完毕后再将线程回收至线程池,这样一来节省了线程频繁创建和销毁的时间,并且线程池一般会设置最大线程数量,从而避免了无限创建线程导致服务器崩溃的局面。

正是由于线程池大部分情况下是回收线程而不是销毁线程(在服务器十分空闲的状况下,线程池可能会销毁掉线程池内的部分线程),导致线程池总是持有一个线程的引用,而此线程一直持有自己内部私有的 threadLocals 变量(即ThreadLocal.ThreadLocalMap对象),导致 GC 无法对ThreadLocalMap对象内持有的内存空间进行回收(因为一直存在强引用)。这样的话,如果程序中在ThreadLocal对象中储存了一大块内存空间,那么这块内存空间几乎永远不会被回收,因为线程池几乎不会销毁线程。

而由于以上的内存泄漏还可能导致另外一个问题:当前请求有可能会拿到上一个请求保存在ThreadLocal对象里的值(如果分配给它们的是同一个线程的话)!

下面用代码证实一下结论:

为了方便,首先使用jdk自带的线程池进行验证,代码如下:

public class ThreadPoolWithThreadLocalTest {
    public static ThreadLocal objectThread = new ThreadLocal();

    public static void main(String args[]) {
        ExecutorService executor = Executors.newFixedThreadPool(3);//创建一个有三个线程的线程池
        for (int i = 0; i < 100; i++) {
            executor.execute(new Task());
        }
        executor.shutdown();
        /**
            表示线程池不再接收新的任务,当前任务执行完毕后程序结束
            一开始没有加这句话,任务执行完后,main()线程一直无法退出 - -
        */
    }
}

//如果没回收50m  那么最多只有3次 是 为空
//如果回收了50m  那么有很多次数不为空
class Task implements Runnable {
    public void run() {
        if (objectThread.get() == null) {
            objectThread.set(new byte[1024 * 1024 * 50]);
            System.out.println(Thread.currentThread() + " get 为空 放进去 放入的值为" + objectThread.get());
            sleepAwhile();
        } else {
            System.out.println(Thread.currentThread() + " get 不为空 为" + objectThread.get());
            sleepAwhile();
        }
    }

    public void sleepAwhile() {
        try {
            Thread.sleep(50L);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

一次部分执行结果如下:

Thread[pool-1-thread-3,5,main] get 为空 放进去 放入的值为[B@5f93f536
Thread[pool-1-thread-1,5,main] get 为空 放进去 放入的值为[B@2633e42b
Thread[pool-1-thread-2,5,main] get 为空 放进去 放入的值为[B@3d8fd87b
Thread[pool-1-thread-2,5,main] get 不为空 为[B@3d8fd87b
Thread[pool-1-thread-3,5,main] get 不为空 为[B@5f93f536
Thread[pool-1-thread-1,5,main] get 不为空 为[B@2633e42b
Thread[pool-1-thread-1,5,main] get 不为空 为[B@2633e42b
Thread[pool-1-thread-2,5,main] get 不为空 为[B@3d8fd87b
Thread[pool-1-thread-3,5,main] get 不为空 为[B@5f93f536
Thread[pool-1-thread-3,5,main] get 不为空 为[B@5f93f536
Thread[pool-1-thread-2,5,main] get 不为空 为[B@3d8fd87b
...

可以看到,在线程池中执行的任务只有前三次的threadlocalmap对象没有放入值,剩余的九十余次任务执行时,都是取出线程池中的三个线程之一进行执行,此时threadlocalmap对象中一直存在着值,GC完全没有回收这些分配了的内存空间,即使线程被线程池回收了。

因此,在web开发过程中,如果我们在ThreadLocal中set值了,在请求处理完毕后,记得remove()掉,这样会解除掉值的强引用,使GC能够回收它。

接下来看一下tomcat处理请求时的情况。

下图为dubug模式下,一次请求到达tomcat时的线程栈:

这里写图片描述

可见,tomcat内部处理为web请求分配线程时也使用了jdk自带的线程池,只是对其进行了必要的封装。

一开始我准备用 jemeter 起n个线程,访问web应用程序,来测试ThreadLocal。但是想了想有些麻烦,就放弃了。(主要是jemeter用的不熟练 233333)。

后来我就想有没有办法把tomcat的线程池中的线程数量固定为1个,哈哈,果然有方法:

修改tomcat路径下 conf/server.xml:

<Service name="Catalina">
    <Executor name="tomcatThreadPool" namePrefix="catalina-exec-" maxThreads="1" minSpareThreads="1"/>

    <Connector executor="tomcatThreadPool" port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />

......

在service中配置一个Executor,通过 maxThreads="1" minSpareThreads="1" 将其线程数量限制为1个,然后将其配置到处理http请求的connector中。

有关tomcat线程池配置分析可以看这里:

http://www.360doc.com/content/15/0603/14/20671606_475357473.shtml

因为我用idea启动的tomcat,idea会将tomcat根路径的配置复制一份作为项目启动的配置。相关分析可以看这里:

http://blog.csdn.net/joenqc/article/details/58044953

然后写一个controller:

@RequestMapping(value = "/index",method = {RequestMethod.GET,RequestMethod.POST})
    public String index(Model model){
        if(MyContext.objectThreadLocal.get() ==null){
            MyContext.objectThreadLocal.set(new byte[1024*1024*50]);
            model.addAttribute("msg","threadlocalmap为空,放进去 放进去的为"+MyContext.objectThreadLocal.get());
        }else{
            model.addAttribute("msg","threadlocalmap不为空,为 "+MyContext.objectThreadLocal.get());
        }
        return "index";
    }

启动项目,访问controller,无论访问多少次,或者换浏览器访问,执行结果总是如下:

第一次访问:

threadlocalmap为空,放进去 放进去的为[B@7b4e881

第二次及以后的访问:

threadlocalmap不为空,为 [B@7b4e881 

以上两次test,证实了本篇文章前部分的分析,如有不同意见,请指出。

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值