本文對ThreadLocal的分析基於JDK 8。
本文大綱
1. ThreadLocal快速上手
2. ThreadLocal應用場景
3. TheadLocal set與get方法簡析
4. TheadLocal與內存泄漏
1. ThreadLocal快速上手
ThreadLocal是java.lang包下的一個類,它可以為每個線程維護一份獨立的變量副本。當線程運行結束后,線程內部的引用的指向的實例副本都會被回收。
對於初次接觸ThreadLocal的同學來說,看了上面這段話可能還是蒙的,下面我們通過簡單的例子快速上手ThreadLocal。
我們先看看不使用ThreadLocal的情況下,讓兩個線程共享一個打印Task進行打印輸出:
public class ThreadLocalTest1 { public static void main(String[] args) { Runnable task = new Task(); new Thread(task, "t1").start(); new Thread(task, "t2").start(); } static class Task implements Runnable { Integer counter = 0; // 多個線程共享的實例 @Override public void run() { while (true) { System.out.println(Thread.currentThread().getName() + " -> " + counter++); try { Thread.sleep(1000L); } catch (InterruptedException e) { e.printStackTrace(); } } } } }
毫無疑問,上面這段代碼對counter的操作不是線程安全的,因為counter是兩個線程間共享的,所以一個線程對counter的修改操作可能會影響另一個線程對counter的輸出,下面我節選了部分輸出結果:
t2 -> 0 t1 -> 0 // t1線程打印0 t2 -> 1 t1 -> 2 // t1線程打印couter從0直接跳到了2,因為t0線程對counter做了修改 t2 -> 3 t1 -> 3
可以從下圖看出兩個線程共享counter大致模型:
假設,現在有一個需求,要求t1和t2各自分別進行計數並打印,那么這時我們就可以使用ThreadLocal了,代碼如下:
public class ThreadLocalTest1 { public static void main(String[] args) { Runnable task = new Task(); new Thread(task, "t1").start(); new Thread(task, "t2").start(); } static class Task implements Runnable { ThreadLocal<Integer> cntTl = new ThreadLocal<Integer>() { protected Integer initialValue() { return 0; // 設置初始值為0 } }; @Override public void run() { while (true) { Integer counter = cntTl.get(); // 獲取值 System.out.println(Thread.currentThread().getName() + " -> " + counter++); cntTl.set(counter); // counter++后,將counter值設置回去 try { Thread.sleep(1000L); } catch (InterruptedException e) { e.printStackTrace(); } } } } }
運行上面代碼的部分輸出結果:
t1 -> 0 t2 -> 0 t1 -> 1 t2 -> 1 t1 -> 2 t2 -> 2 t1 -> 3 t2 -> 3
可以看到,t1和t2兩個線程分別按順序輸出了1、2、3......這就是因為上面提到過的ThreadLocal為每個線程都維護了一份數據的副本,在本例中的體現就是兩個線程t1、t2中都各自有一個counter,t1和t2線程各自操作自己的counter,因此對其中一個counter的數據進行修改不會對另一個counter產生影響。
使用ThreadLocal后的模型:
我們再理解深入一點,每個線程都有一個ThreadLocalMap對象,ThreadLocalMap是ThreadLocal的一個內部類:
/* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals = null; // Thread類中的threadLocals屬性
線程t1和t2各自都有一個ThreadLocalMap對象,暫且就把它看成一個Map就行,這個Map以當前ThreadLocal對象為key,value為我們要保存的值。當使用cntTl調用get方法時,其實是以當前ThreadLocal對象為key去獲取對應的value。
2. ThreadLocal應用場景
ThreadLocal主要有如下兩種應用場景:
1. 每個線程需要單獨維護一個對象實例,就像在快速上手提到的那樣;
2. 在同一線程執行的不同方法中共享對象實例。
下面將重點分析第2種應用場景。熟悉Web開發的同學都知道MVC模型,C(Controller)會調用Service,Service調用DAO,DAO會使用Connection去連接數據庫。在直接使用JDBC和數據庫通信的情況下,我們需要在Service中創建Connection對象,然后打開事務,並將Connection以參數的形式傳遞給DAO,DAO使用Connection對象與數據庫進行(開啟事務的Connection和執行SQL的Connection必須是同一個),交互完成后我們在Service層進行事務的提交或者回滾。在不使用ThreadLocal的情況下,我們可能會這樣寫代碼:
一個SqlRunner類用於執行SQL:
public class SqlRunner { public void save(Connection connection, String sql, Object data) { System.out.println("sql: " + sql + " executed successfully"); } }
Dao調用SqlRunner:
public class Dao { public void save(Connection connection, Object data) { // 接收Connection SqlRunner sqlRunner = new SqlRunner(); sqlRunner.save(connection, "insert into ...", data); } }
Service調用Dao:
public class Service { Dao dao = new Dao(); public void save(Object data) { Connection connection = new Connection(); // 創建Connection connection.beginTransaction(); // 開啟事務 dao.save(connection, data); // 傳入connection對象 connection.commit(); // 提交事務 } }
測試類:
public class ServiceTest { public static void main(String[] args) { Service service = new Service(); service.save("test data"); } }
控制台輸出:
transaction begin
data: test data, sql: insert into ... executed successfully
transaction commit
因為開啟事務的Connection和執行SQL的Connection必須是同一個,所以可以看到Service中將創建的Connection以參數的方式傳給了Dao,但是這種以傳參的方式共享Connection會導致每個調用Dao方法的Service都必須傳遞Connection,顯得太不優雅,下面我們將使用ThreadLocal來改變這種局面。
SqlRunner和上面的一樣,這里不再貼出代碼。
新增一個DataSource類:
public class DataSource { private static ThreadLocal<Connection> tl = new ThreadLocal<>(); // 使用ThreadLocal包裝Connection public static void beginTransaction() { getCurrentConnection().beginTransaction(); // 開啟事務 } public static void commit() { getCurrentConnection().commit(); // 提交事務 } public static Connection getCurrentConnection() { Connection connection = tl.get(); // 從ThreadLocal對象tl獲取connection if (connection == null) { connection = getConnection(); // 沒有和當前線程綁定的connection,則新建一個 tl.set(connection); // 將新建的connection與當前線程綁定 } return connection; } private static Connection getConnection() { return new Connection(); // 創建線程 } }
Dao:
public class Dao { public void save(Object data) { SqlRunner sqlRunner = new SqlRunner(); Connection connection = DataSource.getCurrentConnection(); // 獲取與當前線程綁定的connection System.out.println("connection in dao: " + connection); // 打印Dao中的connection對象 sqlRunner.save(connection, "insert into ...", data); } }
Service:
public class Service { Dao dao = new Dao(); public void save(Object data) { DataSource.beginTransaction(); // 使用Connection開啟事務 dao.save(data); System.out.println("connection in dao: " + DataSource.getCurrentConnection()); // 打印Service中connection對象 DataSource.commit(); // 提交事務 } }
測試類和上面的ServiceTest相同,這里不再貼出。
控制台輸出:
transaction begin
connection in Dao: com.andywooh.texplore.demo.concurrency.threadlocal.Connection@15db9742 data: test data, sql: insert into ... executed successfully
connection in Service: com.andywooh.texplore.demo.concurrency.threadlocal.Connection@15db9742 transaction commit
可以看到,在Service中的connection和Dao中的Connection是同一個對象。
簡單對ThreadLocal方式在同一個線程中、不同方法間共享connection對象做一個分析:調用Service的save方法,在開啟事務前會先使用DataSource的getCurrentConnection去獲得一個連接,由於是第一次獲取connection,此時還沒有和當前線程綁定的connection對象,所以會調用getConnection方法區創建一個connection對象,並將這個connection對象和當前線程進行綁定。當在同一個線程中在Dao里再一次調用getCurrentConnection時,由於已經有一個connection和當前線程綁定,所以就會直接返回該connection對象,這樣就實現了不傳參但是卻在Service和Dao中使用同一個Connectiond的功能。
3. TheadLocal set與get方法簡析
下面對ThreadLocal的set和get方法進行分析。再次說明一下,每個線程都包含一個ThreadLocalMap,我們先將其當成一個Map就行,ThreadLocalMap是ThreadLocal的一個內部類,這個Map中存儲了我們想要和當前線程綁定的值,其中key是當前ThreadLocal對象,value是我們想要保存的值。
set方法:
public void set(T value) { Thread t = Thread.currentThread(); // 獲取當前線程 ThreadLocalMap map = getMap(t); // 獲取當前線程內的map if (map != null) map.set(this, value); // map不為空,則以當前ThreadLocal對象為key,value為我們想要保存的值設置到map中 else createMap(t, value); // map為空,創建一個map來保存value,當然key還是當前ThreadLocal }
get方法:
public T get() { Thread t = Thread.currentThread(); // 獲取當前線程 ThreadLocalMap map = getMap(t); // 獲取當前線程內的map if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); // 以當前ThreadLocal對象為key取entry對象 if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; // 獲取entry中包裝的值,也就是我們之前設置進來的value return result; } } return setInitialValue(); // map為空,創建一個map並給map設置一個初始值entry;或者map中沒有entry,給已有的map添加一個初始值的entry }
4. TheadLocal與內存泄漏
前面提到過,當線程銷毀的時候,與線程綁定的相關的對象將會被GC。下面的代碼展示了Thread類中的exit方法,可以看到這里將threadLocals(就是ThreadLocalMap)進行了置空,方便虛擬機對ThreadLocalMap對象進行回收。
private void exit() { if (group != null) { group.threadTerminated(this); group = null; } /* Aggressively null out all reference fields: see bug 4006245 */ target = null; /* Speed the release of some of these resources */ threadLocals = null; // 把ThreadLocalMap引用置空 inheritableThreadLocals = null; inheritedAccessControlContext = null; blocker = null; uncaughtExceptionHandler = null; }
但是在一些線程不會死亡的場景,比如在線池,因為線程不會結束,如果處理的不好,那么和線程綁定的對象就會一直存在,從而造成內存泄漏。
因為這里涉及到強、弱引用的知識,這里簡單介紹一下:我們平常寫的Object obj = new Object()中的obj就是強引用,只要還有強引用指向一個對象,這個對象不會被回收。而對於弱引用,一旦發現只被弱引用引用的對象,不管當前內存空間足夠與否,這個對象都會被回收。
ThreadLocalMap中的Entry的key就是一個弱引用:
static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); // 創建一個弱引用的key value = v; } }
下圖展示了Thread對象、ThreadLocal對象、ThreadLocalMap對象以及Entry對象之間的聯系,其中虛線箭頭表示Entry中的弱引用key指向了ThreadLocal對象:
Entry對象中弱引用key指向了我們的ThreadLocal對象,當我們將ThreadLocal對象的引用置為null后,就沒有強用用指向它,只剩這個弱引用指向ThreadLocal對象,那么JVM會在GC的時候回收ThreadLocal對象。然而Entry對象中value引用指向的value對象還是存活的,這樣就會導致value對象一直得不到回收。但是,在我們調用ThreadLocal對象的get、set、remove方法時,會將上述提到的key為nul對應的value對象進行清除,從而避免了內存泄漏。值得注意的是,如果我們在創建一個ThreadLocal對象並set了一個value對象到ThreadLocalMap,然后不再調用前面提到的get、set、remove方法中的任意一個,此時就可能會導致這個value對象不能被回收。