ThreadLocal深入研究

来源:互联网 发布:广州哪里学化妆知乎 编辑:程序博客网 时间:2024/05/16 10:06

不久前我写过一篇关于ThreadLocal用法的文章,但最近项目上出现了Memory Leak,调查后发现可能与ThreadLocal的使用有关,在此对ThreadLocal的使用作一些补充。


在ThreadLocal内部,其实是通过一个Map(类似Map<Thread, Object>)来保存各个线程独立的变量的,但是这个map有一点特殊,它对线程的引用是弱引用WeakReference(如果一个对象只被弱引用相联,那么GC就可以回收这个对象),这说明当线程执行结果后,即使没有显式的调用ThreadLocal.remove方法,GC也可以回收该线程在ThreadLocal中存放的独立对象了。

我们先看一个简单的例子:

public class App {public static void main(String[] args) throws Exception {final ThreadLocal<Obj> local = new ThreadLocal<Obj>();Thread t = new Thread() {public void run() {local.set(new Obj());}};t.start();while(true) {System.gc();TimeUnit.SECONDS.sleep(1);}}}class Obj {@Overrideprotected void finalize() throws Throwable {super.finalize();System.out.println(this + " finalized.");}}
线程t开始执行时创建了一个Obj对象,随后把该对象放入ThreadLocal中,之后线程t执行结束。之后便会输出Obj@721cdeff finalized,说明Obj对象被GC回收,这与我们上面的分析是一致的。

我们对程序稍作修改,再来看看:

public class App {public static void main(String[] args) throws Exception {final ThreadLocal<Obj> local = new ThreadLocal<Obj>();ExecutorService exec = Executors.newFixedThreadPool(2);Thread t = new Thread() {public void run() {local.set(new Obj());}};exec.execute(t);while(true) {System.gc();TimeUnit.SECONDS.sleep(1);}}}class Obj {@Overrideprotected void finalize() throws Throwable {super.finalize();System.out.println(this + "finalized.");}}
与之前的例子不同的地方是这里使用了线程池来执行线程,这样当线程执行完后并没有被销毁,而是还给了线程池。正因为此ThreadLocal Map中为该线程保存的entry不会被GC回收,也就是说上面这个例子不会有任何输出,Obj对象会在Heap中一直存在。

可以想象下在一个web server环境下,为了提高对请求的响应,大部分web server(比如tomcat)都是预先创建一个线程池。当有请求到来时,就从线程池中取出一个线程来处理请求,之后再将线程放回线程池,也就是说这些线程至始至终都不会被销毁。那如果像上面的例子一样在Web环境下错误地使用了ThreadLocal会带来什么后果呢?

我们再看一个例子:

public class App {public static void main(String[] args) throws Exception {final ThreadLocal<Object> local = new ThreadLocal<Object>();ExecutorService exec = Executors.newFixedThreadPool(2);Thread t = new Thread() {public void run() {local.set(App.createObj());}};exec.execute(t);while(true) {System.gc();TimeUnit.SECONDS.sleep(1);}}public static Object createObj()  {try {CustomClassLoader cl = new CustomClassLoader(new URL("file:///Users/ouyang/Develop/eclipse/workspace/Test/bin/"));Class<?> clazz = cl.loadClass("App$Obj");        return clazz.newInstance();} catch (Exception e) {e.printStackTrace();}return null;}public static class Obj {@Overrideprotected void finalize() throws Throwable {super.finalize();System.out.println(this + "finalized.");}}}class CustomClassLoader extends URLClassLoader {    public CustomClassLoader(URL... urls) {        super(urls, null);    }    @Override    protected void finalize() {        System.out.println("*** CustomClassLoader finalized!");    }}
这个例子在之前例子的基础上,修改了Obj对象的创建,这次我们使用一个自定义的ClassLoader来加载和创建Obj对象。同样的,这个例子不会有任何的输出,Obj对象不能被GC回收,从而导致加载他的CustomClassLoader对象不能被回收,更要命的是其它被CustomClassLoader加载的类啊、静态数据对象等等,都不能被GC回收,甚至是在undeploy应用的时候都不能被回收。只要web server不重启,每一次重新布暑应用都将加大这些无效类、静态数据所占用的空间。从而造成Permgen Leak和Memory Leak。


所以,必须在线程执行结束前,调用ThreadLocal的remove方法显式的删除对独立对象的强引用。

0 0
原创粉丝点击