ThreadLocal深入研究

不久前我寫過一篇關於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 {
	@Override
	protected 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 {
	@Override
	protected 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 {
		@Override
		protected 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方法顯式的刪除對獨立對象的強引用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章