关于ThreadLocal
2019獨角獸企業重金招聘Python工程師標準>>>
一、認識ThreadLocal
1.ThreadLocal概念
????為了防止任務在共享資源上產生沖突,我們可以使用同步機制,還可以選擇根除對變量的共享來防止沖突。線程本地的存儲是一種自動化的機制,可以為使用相同的變量的每個不同的線程都創建不同的存儲。當有5個線程都要使用變量x所表示的對象,那線程本地存儲就會生成5個用于x的不同的存儲塊,這樣使得你可以將狀態和線程關聯起來。創建和管理線程本地存儲可以用java.lang.ThreadLocal類來實現。
????ThreadLocal,很多地方叫做線程本地變量,也有些地方叫做線程本地存儲,其實意思差不多。ThreadLocal為變量在每個線程中都創建了一個副本,那么每個線程可以訪問自己內部的副本變量。
ThreadLocal的實現是這樣的:每個Thread 維護一個 ThreadLocalMap 映射表,這個映射表的 key 是 ThreadLocal 實例本身,value 是真正需要存儲的 Object。也就是說 ,ThreadLocal 本身并不存儲值,它只是作為一個 key 來讓線程從 ThreadLocalMap 獲取 value。
ThreadLocal的應用:
class ConnectionManager {private static Connection connect = null;public static Connection openConnection() {if(connect == null){connect = DriverManager.getConnection();}return connect;}public static void closeConnection() {if(connect!=null)connect.close();} }????????假設有這樣一個數據庫鏈接管理類,這段代碼在單線程中使用是沒有任何問題的,但是在多線程中使用會存在線程安全問 題:第一,這里面的2個方法都沒有進行同步,很可能在openConnection方法中會多次創建connect;第二,由于connect是共享變 量,那么必然在調用connect的地方需要使用到同步來保障線程安全,因為很可能一個線程在使用connect進行數據庫操作,而另外一個線程調用 closeConnection關閉鏈接。
所以出于線程安全的考慮,必須將這段代碼的兩個方法進行同步處理,并且在調用connect的地方需要進行同步處理。但是,這樣將會大大影響程序執行效率,因為一個線程在使用connect進行數據庫操作的時候,其他線程只有等待。
那到底需不需要將connect變量進行共享?事實上,是不需要的。假如每個線程中都有一個 connect變量,各個線程之間對connect變量的訪問實際上是沒有依賴關系的,即一個線程不需要關心其他線程是否對這個connect進行了修改 的。
有一種可行的方法是,在每個需要使用數據庫連接的方法中具體使用時才創建數據庫鏈接,然后在方法調用完畢再釋放這個連接。比如下面這樣:
class ConnectionManager {private Connection connect = null;public Connection openConnection() {if(connect == null){connect = DriverManager.getConnection();}return connect;}public void closeConnection() {if(connect!=null)connect.close();} }class Dao{public void insert() {ConnectionManager connectionManager = new ConnectionManager();Connection connection = connectionManager.openConnection();//使用connection進行操作connectionManager.closeConnection();} }這樣處理確實也沒有任何問題,由于每次都是在方法內部創建的連接,那么線程之間自然不存在線程安全問題。但是這樣會有一個致命的影響:由于在方法中需要頻繁地開啟和關閉數據庫連接,這樣不盡嚴重影響程序執行效率,還可能導致服務器壓力巨大,并且嚴重影響程序執行性能。
那么這種情況下使用ThreadLocal是再適合不過的了,因為ThreadLocal在每個線程中對該變量會創建一個副本,即每個線程內部 都會有一個該變量,且在線程內部任何地方都可以使用,線程之間互不影響,這樣一來就不存在線程安全問題,也不會嚴重影響程序執行性能。另一方面,由于在每個線程中都創建了副本,所以要考慮它對資源的消耗,比如內存的占用會比不使用ThreadLocal要大。
使用示例:
public class DAOFactory {private static class ConnectionContext {private static ThreadLocal<ConnectionContext > context = new ThreadLocal<ConnectionContext >();public static void setContext(ConnectionContext cc) {context.set(cc);}public static ConnectionContext getContext() {return context.get();}public static void removeContext() {context.remove();}public ResultSet query(String sql) throws SQLException {Statement st = null;ResultSet rs = null;Connection conn = null;try {conn = ConnectionContext .getConnection();st = conn.createStatement();rs = st.executeQuery(sql);return rs;}catch (SQLException e) {throw e;}finally {ConnectionContext cc = new DAOThreadContext(rs, conn, st);ConnectionContext.setContext(cc);}}//Other operation ....public static Connection getConnection() throws SQLException {Connection con = null;try {synchronized (ds) { //其中ds為自定義的數據源con = ds.getConnection();}}catch (SQLException e) {throw e;}return con;}} }2.多線程下ThreadLocal實現線程自增序號
首先看生成序號的接口
public interface Sequence {public int getNumber();}自定義線程類
public class ThreadClient extends Thread {private Sequence sequence;public ThreadClient(Sequence sequence){this.sequence = sequence;}@Overridepublic void run(){for(int i = 0; i < 3; i++){System.out.println(Thread.currentThread().getName() + "=>" + sequence.getNumber());}}}without ThreadLocal情況
public class SequenceA implements Sequence {private static int number = 0;@Overridepublic int getNumber(){number++;return number;}public static void main(String[] args) {// TODO Auto-generated method stubSequence sequence = new SequenceA();ThreadClient tc1 = new ThreadClient(sequence);ThreadClient tc2 = new ThreadClient(sequence);ThreadClient tc3 = new ThreadClient(sequence);tc1.start();tc2.start();tc3.start();}}輸出結果:
Thread-0=>1
Thread-0=>4
Thread-0=>5
Thread-2=>3
Thread-1=>2
Thread-1=>7
Thread-1=>8
Thread-2=>6
Thread-2=>9
從輸出結果可以看出,線程之間共享了static變量,所以造成同一個線程輸出的生成序號不連續的情況。
Use ThreadLocal情況
public class SequenceB implements Sequence {private static ThreadLocal<Integer> numberContainer = new ThreadLocal<Integer>(){@Overridepublic Integer initialValue(){return 0;}};@Overridepublic int getNumber(){numberContainer.set(numberContainer.get() + 1);return numberContainer.get(); }public static void main(String[] args) {Sequence sequence = new SequenceB();ThreadClient tc1 = new ThreadClient(sequence);ThreadClient tc2 = new ThreadClient(sequence);ThreadClient tc3 = new ThreadClient(sequence);tc1.start();tc2.start();tc3.start();} }輸出結果:
Thread-0=>1
Thread-0=>2
Thread-0=>3
Thread-2=>1
Thread-2=>2
Thread-1=>1
Thread-1=>2
Thread-2=>3
Thread-1=>3
使用了ThreadLocal后, 每個線程相互獨立了,同樣是 static 變量,對于不同的線程而言,它沒有被共享,而是每個線程各一份,這樣也就保證了線程安全。 也就是說,TheadLocal 為每一個線程提供了一個獨立的副本,即便只有一個Sequence對象。
二、深入解析ThreadLocal類
1.ThreadLocal類方法
先了解一下ThreadLocal類提供的幾個方法:
public T get() { } public void set(T value) { } public void remove() { } protected T initialValue() { }get()方法是用來獲取ThreadLocal在當前線程中保存的變量副本,set()用來設置當前線程中變量的副本,remove()用來移除 當前線程中變量的副本,initialValue()是一個protected方法,一般是用來在使用時進行重寫的,它是一個延遲加載方法。
首先我們來看一下ThreadLocal類是如何為每個線程創建一個變量的副本的。先看下get方法的實現:
/** * Returns the value in the current thread's copy of this * thread-local variable. If the variable has no value for the * current thread, it is first initialized to the value returned * by an invocation of the {@link #initialValue} method. * * @return the current thread's value of this thread-local */ public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) return (T)e.value; } return setInitialValue(); }第一句是取得當前線程,然后通過getMap(t)方法獲取到一個map,map的類型為ThreadLocalMap。然后接著下面獲取到<key,value>鍵值對,注意這里獲取鍵值對傳進去的是? this,而不是當前線程t。
如果獲取成功,則返回value值。
如果map為空,則調用setInitialValue方法返回value。
我們上面的每一句來仔細分析:
首先看一下getMap方法中做了什么:
/** * Get the map associated with a ThreadLocal. Overridden in * InheritableThreadLocal. * * @param t the current thread * @return the map */ ThreadLocalMap getMap(Thread t) { return t.threadLocals; }在getMap中,是調用當期線程t,返回當前線程t中的一個成員變量threadLocals。
那么我們繼續取Thread類中取看一下成員變量threadLocals是什么:
/* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals = null;實際上就是一個ThreadLocalMap,這個類型是ThreadLocal類的一個內部類,我們繼續取看ThreadLocalMap的實現:
static class ThreadLocalMap {/*** The entries in this hash map extend WeakReference, using* its main ref field as the key (which is always a* ThreadLocal object). Note that null keys (i.e. entry.get()* == null) mean that the key is no longer referenced, so the* entry can be expunged from table. Such entries are referred to* as "stale entries" in the code that follows.*/static class Entry extends WeakReference<ThreadLocal> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal k, Object v) {super(k);value = v;}}可以看到ThreadLocalMap的Entry繼承了WeakReference,并且使用ThreadLocal作為鍵值。然后再繼續看setInitialValue方法的具體實現:
/** * Variant of set() to establish initialValue. Used instead * of set() in case user has overridden the set() method. * * @return the initial value */ private T setInitialValue() { T value = initialValue(); Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); return value; }很容易了解,就是如果map不為空,就設置鍵值對,為空,再創建Map,看一下createMap的實現:
/** * Create the map associated with a ThreadLocal. Overridden in * InheritableThreadLocal. * * @param t the current thread * @param firstValue value for the initial entry of the map * @param map the map to store. */ void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }現在,應該對ThreadLocal如何創建變量副本有個了解了。首先,在每個線程Thread內部有一個ThreadLocal.ThreadLocalMap類型的成員變量threadLocals,這個 threadLocals就是用來存儲實際的變量副本的,鍵值為當前ThreadLocal變量,value為變量副本(即T類型的變量)。
初始時,在Thread里面,threadLocals為空,當通過ThreadLocal變量調用get()方法或者set()方法,就會對 Thread類中的threadLocals進行初始化,并且以當前ThreadLocal變量為鍵值,以ThreadLocal要保存的副本變量為 value,存到threadLocals。
然后在當前線程里面,如果要使用副本變量,就可以通過get方法在threadLocals里面查找。
2.舉例: 通過ThreadLocal能達到在每個線程中創建變量副本的效果
public class Test {ThreadLocal<Long> longLocal = new ThreadLocal<Long>();ThreadLocal<String> stringLocal = new ThreadLocal<String>();public void set() {longLocal.set(Thread.currentThread().getId());stringLocal.set(Thread.currentThread().getName());}public long getLong() {return longLocal.get();}public String getString() {return stringLocal.get();}public static void main(String[] args) throws InterruptedException {final Test test = new Test();test.set();System.out.println(test.getLong());System.out.println(test.getString());Thread thread1 = new Thread(){public void run() {test.set();System.out.println(test.getLong());System.out.println(test.getString());};};thread1.start();thread1.join();System.out.println(test.getLong());System.out.println(test.getString());}}輸出結果:
1
main
9
Thread-0
1
main
從這段代碼的輸出結果可以看出,在main線程中和thread1線程中,longLocal保存的副本值和stringLocal保存的副本值都不一樣。最后一次在main線程再次打印副本值是為了證明在main線程中和thread1線程中的副本值確實是不同的。
總結一下:
1)實際的通過ThreadLocal創建的副本是存儲在每個線程自己的threadLocals中的;
2)為何threadLocals的類型ThreadLocalMap的鍵值為ThreadLocal對象,因為每個線程中可有多個threadLocal變量,就像上面代碼中的longLocal和stringLocal;
3)在進行get之前,必須先set,否則會報空指針異常;
??? 如果想在get之前不需要調用set就能正常訪問的話,必須重寫initialValue()方法。
因為在上面的代碼分析過程中,我們發現如果沒有先set的話,即在map中查找不到對應的存儲,則會通過調用setInitialValue方法返回i, 而在setInitialValue方法中,有一個語句是T value = initialValue(), 而默認情況下,initialValue方法返回的是null。
public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) return (T)e.value; } return setInitialValue(); } private T setInitialValue() { T value = initialValue(); Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); return value; }3.在set()之前get()會拋出異常
public class Test {ThreadLocal<Long> longLocal = new ThreadLocal<Long>();ThreadLocal<String> stringLocal = new ThreadLocal<String>();public void set() {longLocal.set(Thread.currentThread().getId());stringLocal.set(Thread.currentThread().getName());}public long getLong() {return longLocal.get();}public String getString() {return stringLocal.get();}public static void main(String[] args) throws InterruptedException {final Test test = new Test();System.out.println(test.getLong());System.out.println(test.getString());Thread thread1 = new Thread(){public void run() {test.set();System.out.println(test.getLong());System.out.println(test.getString());};};thread1.start();thread1.join();System.out.println(test.getLong());System.out.println(test.getString());}}輸出結果:
Exception in thread "main" java.lang.NullPointerException
?? ?at com.threadlocal.test2.Test.getLong(Test.java:14)
?? ?at com.threadlocal.test2.Test.main(Test.java:24)
重寫了initialValue方法之后,可以在沒有set之前就先get
ThreadLocal<Long> longLocal = new ThreadLocal<Long>(){protected Long initialValue() {return Thread.currentThread().getId();};};ThreadLocal<String> stringLocal = new ThreadLocal<String>(){;protected String initialValue() {return Thread.currentThread().getName();};};三、Thread同步機制的比較
ThreadLocal和線程同步機制相比有什么優勢呢?ThreadLocal和線程同步機制都是為了解決多線程中相同變量的訪問沖突問題。
在同步機制中,通過對象的鎖機制保證同一時間只有一個線程訪問變量。這時該變量是多個線程共享的,使用同步機制要求程序慎密地分析什么時候對變量進行讀寫,什么時候需要鎖定某個對象,什么時候釋放對象鎖等繁雜的問題,程序設計和編寫難度相對較大。
而ThreadLocal則從另一個角度來解決多線程的并發訪問。ThreadLocal會為每一個線程提供一個獨立的變量副本,從而隔離了多個線程 對數據的訪問沖突。因為每一個線程都擁有自己的變量副本,從而也就沒有必要對該變量進行同步了。ThreadLocal提供了線程安全的共享對象,在編寫 多線程代碼時,可以把不安全的變量封裝進ThreadLocal。
由于ThreadLocal中可以持有任何類型的對象,低版本JDK所提供的get()返回的是Object對象,需要強制類型轉換。但JDK 5.0通過泛型很好的解決了這個問題,在一定程度地簡化ThreadLocal的使用,代碼清單 9 2就使用了JDK 5.0新的ThreadLocal<T>版本。
概括起來說,對于多線程資源共享的問題,同步機制采用了“以時間換空間”的方式,而ThreadLocal采用了“以空間換時間”的方式。前者僅提供一份變量,讓不同的線程排隊訪問,而后者為每一個線程都提供了一份變量,因此可以同時訪問而互不影響。
Spring使用ThreadLocal解決線程安全問題我們知道在一般情況下,只有無狀態的Bean才可以在多線程環境下共享,在Spring中, 絕大部分Bean都可以聲明為singleton作用域。就是因為Spring對一些Bean(如RequestContextHolder、 TransactionSynchronizationManager、LocaleContextHolder等)中非線程安全狀態采用 ThreadLocal進行處理,讓它們也成為線程安全的狀態,因為有狀態的Bean就可以在多線程中共享了。
一般的Web應用劃分為展現層、服務層和持久層三個層次,在不同的層中編寫對應的邏輯,下層通過接口向上層開放功能調用。在一般情況下,從接收請求到返回響應所經過的所有程序調用都同屬于一個線程。
同一線程貫通Action、Service、Dao這三層,這樣需要將一些非線程安全的變量以ThreadLocal存放,在同一次請求響應的調用線程中,所有關聯的對象引用到的都是同一個變量。
下面的實例能夠體現Spring對有狀態Bean的改造思路:
1.TestDao類,非線程安全
import java.sql.Connection; import java.sql.SQLException; import java.sql.Statement; public class TestDao { private Connection conn;// ①一個非線程安全的變量 public void addTopic() throws SQLException { Statement stat = conn.createStatement();// ②引用非線程安全變量 // … } }由于①處的conn是成員變量,因為addTopic()方法是非線程安全的,必須在使用時創建一個新TopicDao實例(非singleton)。下面使用ThreadLocal對conn這個非線程安全的“狀態”進行改造:
2.TestDao類,線程安全
import java.sql.Connection; import java.sql.SQLException; import java.sql.Statement; public class TestDaoNew { // ①使用ThreadLocal保存Connection變量 private static ThreadLocal<Connection> connThreadLocal = new ThreadLocal<Connection>(); public static Connection getConnection() { // ②如果connThreadLocal沒有本線程對應的Connection創建一個新的Connection, // 并將其保存到線程本地變量中。 if (connThreadLocal.get() == null) { Connection conn = datasource.getConnection(); connThreadLocal.set(conn); return conn; } else { return connThreadLocal.get();// ③直接返回線程本地變量 } } public void addTopic() throws SQLException { // ④從ThreadLocal中獲取線程對應的Connection Statement stat = getConnection().createStatement(); } }不同的線程在使用TopicDao時,先判斷connThreadLocal.get()是否是null,如果是null,則說明當前線程還沒有對應的 Connection對象,這時創建一個Connection對象并添加到本地線程變量中;如果不為null,則說明當前的線程已經擁有了 Connection對象,直接使用就可以了。這樣,就保證了不同的線程使用線程相關的Connection,而不會使用其它線程的 Connection。因此,這個TopicDao就可以做到singleton共享了。
當然,這個例子本身很粗糙,將Connection的ThreadLocal直接放在DAO只能做到本DAO的多個方法共享Connection時不發生 線程安全問題,但無法和其它DAO共用同一個Connection,要做到同一事務多DAO共享同一Connection,必須在一個共同的外部類使用 ThreadLocal保存Connection。
3. ConnectionManager.java
import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class ConnectionManager { private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() { @Override protected Connection initialValue() { Connection conn = null; try { conn = DriverManager.getConnection( "jdbc:mysql://localhost:3306/test", "username", "password"); } catch (SQLException e) { e.printStackTrace(); } return conn; } }; public static Connection getConnection() { return connectionHolder.get(); } public static void setConnection(Connection conn) { connectionHolder.set(conn); } }四、ThreadLocal的使用場景
ThreadLocal的應用場景,最適合的是按線程多實例(每個線程對應一個實例)的對象的訪問,并且這個對象很多地方都要用到。 最常見的ThreadLocal使用場景為 用來解決 數據庫連接、Session管理等。,如下:
private static ThreadLocal<Connection> connectionHolder= new ThreadLocal<Connection>() {public Connection initialValue() {return DriverManager.getConnection(DB_URL);}};public static Connection getConnection() {return connectionHolder.get();} private static final ThreadLocal threadSession = new ThreadLocal();public static Session getSession() throws InfrastructureException {Session s = (Session) threadSession.get();try {if (s == null) {s = getSessionFactory().openSession();threadSession.set(s);}} catch (HibernateException ex) {throw new InfrastructureException(ex);}return s; }對于多線程資源共享的問題,同步機制采用了“以時間換空間”的方式,比如定義一個static變量,同步訪問,而ThreadLocal采用了“以 空間換時間”的方式。前者僅提供一份變量,讓不同的線程排隊訪問,而后者為每一個線程都提供了一份變量,因此可以同時訪問而互不影響。
在多線程的開發中,經常會考慮到的策略是對一些需要公開訪問的屬性通過設置同步的方式來訪問。這樣每次能保證只有一個線程訪問它,不會有沖突。但是 這樣做的結果會使得性能和對高并發的支持不夠。在某些情況下,如果我們不一定非要對一個變量共享不可,而是給每個線程一個這樣的資源副本,讓他們可以獨立 都各自跑各自的,這樣不是可以大幅度的提高并行度和性能了嗎?
還有的情況是有的數據本身不是線程安全的,或者說它只能被一個線程使用,不能被其它線程同時使用。如果等一個線程使用完了再給另一個線程使用就根本不現實。這樣的情況下,我們也可以考慮ThreadLocal。
五、ThreadLocal內存泄漏?
ThreadLocal使得各線程能夠保持各自獨立的一個對象,并不是通過ThreadLocal.set()來實現的,而是通過每個線程中的new對象 的操作來創建的對象,每個線程創建一個,不是什么對象的拷貝或副本。通過ThreadLocal.set()將這個新創建的對象的引用保存到各線程的自己 的一個map(Thread類中的ThreadLocal.ThreadLocalMap的變量)中,每個線程都有這樣一個map,執行 ThreadLocal.get()時,各線程從自己的map中取出放進去的對象,因此取出來的是各自自己線程中的對象,ThreadLocal實例是作 為map的key來使用的。
代碼1:
/* ThreadLocal values pertaining to this thread. This map is maintained* by the ThreadLocal class. */ThreadLocal.ThreadLocalMap threadLocals = null;/** InheritableThreadLocal values pertaining to this thread. This map is* maintained by the InheritableThreadLocal class.*/ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;很多人會有這樣的無解:感覺這個ThreadLocal對象建立了一個類似于全局的map,然后每個線程作為map的key來存取對應的線程本地的 value。其實是ThreadLocal類中有一個ThreadLocalMap靜態內部類,可以簡單的理解為一個map,這個map為每個線程復制一 個變量的“拷貝”存儲其中。下面是ThreadLocalMap的部分源碼:
代碼2:
static class ThreadLocalMap {static class Entry extends WeakReference<ThreadLocal> {Object value;Entry(ThreadLocal k, Object v) {super(k);value = v;}} private static final int INITIAL_CAPACITY = 16;private Entry[] table;private int size = 0;private int threshold; // Default to 0//部分省略 }通過代碼1和代碼2的片段可以看出,在Thread類中保有ThreadLocal.ThreadLocalMap的引用,即在一個Java線程棧 中指向了堆內存中的一個ThreadLocal.ThreadLocalMap的對象,此對象中保存了若干個Entry,每個Entry的 key(ThreadLocal實例)是弱引用,value是強引用(這點類似于WeakHashMap)。
用到弱引用的只是key,每個key都弱引用指向threadLocal,當把threadLocal實例置為null以后,沒有任何強引用指向 threadLocal實例,所以threadLocal將會被GC回收,但是value卻不能被回收,因為其還存在于 ThreadLocal.ThreadLocalMap的對象的Entry之中。只有當前Thread結束之后,所有與當前線程有關的資源才會被GC回 收。所以,如果在線程池中使用ThreadLocal,由于線程會復用,而又沒有顯示的調用remove的話的確是會有可能發生內存泄露的問題。
????????其實,ThreadLocalMap的設計中已經考慮到這種情況,也加上了一些防護措施:在ThreadLocal的get(),set(),remove()的時候都會清除線程ThreadLocalMap里所有key為null的value。????????????????????? ThreadLocal.ThreadLocalMap的get或者set方法中會探測其中的key是否被回收(調用 expungeStaleEntry方法),然后將其value設置為null,這個功能幾乎和WeakHashMap中的 expungeStaleEntries()方法一樣。因此value在key被gc后可能還會存活一段時間,但最終也會被回收,但是若不再調用get或 者set方法時,那么這個value就在線程存活期間無法被釋放。
?為什么使用弱引用
從表面上看內存泄漏的根源在于使用了弱引用。網上的文章大多著重分析ThreadLocal使用了弱引用會導致內存泄漏,但是另一個問題也同樣值得思考:為什么使用弱引用而不是強引用?
我們先來看看官方文檔的說法:
??? To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.
??? 為了應對非常大和長時間的用途,哈希表使用弱引用的 key。
下面我們分兩種情況討論:
??? key 使用強引用:引用的ThreadLocal的對象被回收了,但是ThreadLocalMap還持有ThreadLocal的強引用,如果沒有手動刪除,ThreadLocal不會被回收,導致Entry內存泄漏。
??? key 使用弱引用:引用的ThreadLocal的對象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使沒有手動刪除,ThreadLocal也會被回收。value在下一次ThreadLocalMap調用set,get,remove的時候會被清除。
比較兩種情況,我們可以發現:由于ThreadLocalMap的生命周期跟Thread一樣長,如果都沒有手動刪除對應key,都會導致內存泄漏,但是使用弱引用可以多一層保障:弱引用ThreadLocal不會內存泄漏,對應的value在下一次ThreadLocalMap調用set,get,remove的時候會被清除。
因此,ThreadLocal內存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一樣長,如果沒有手動刪除對應key就會導致內存泄漏,而不是因為弱引用。
ThreadLocal 最佳實踐
綜合上面的分析,我們可以理解ThreadLocal內存泄漏的前因后果,那么怎么避免內存泄漏呢?
??? 每次使用完ThreadLocal,都調用它的remove()方法,清除數據。
在使用線程池的情況下,沒有及時清理ThreadLocal,不僅是內存泄漏的問題,更嚴重的是可能導致業務邏輯出現問題。所以,使用ThreadLocal就跟加鎖完要解鎖一樣,用完就清理。
?
參考文章:
http://www.cnblogs.com/dolphin0520/p/3920407.html
http://blog.csdn.net/lufeng20/article/details/24314381
http://my.oschina.net/huangyong/blog/159489
http://www.importnew.com/21043.html
http://blog.xiaohansong.com/2016/08/06/ThreadLocal-memory-leak/
轉載于:https://my.oschina.net/cain1507/blog/742275
總結
以上是生活随笔為你收集整理的关于ThreadLocal的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 如何在Linux上制作一个屏幕录像视频教
- 下一篇: 基于gulp编写的一个简单实用的前端开发