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
类结合使用的问题。
因为在绝大部分使用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根路径的配置复制一份作为项目启动的配置。相关分析可以看这里:
然后写一个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,证实了本篇文章前部分的分析,如有不同意见,请指出。