史上最全ThreadLocal 详解(一)
目錄
一、ThreadLocal簡介
二、ThreadLocal與Synchronized的區別
三、ThreadLocal的簡單使用
四、ThreadLocal的原理
? ? ? ? 4.1 ThreadLocal的set()方法:
? ? ? ? ?4.2 ThreadLocal的get方法
4.3 ThreadLocal的remove方法
?4.4、ThreadLocal與Thread,ThreadLocalMap之間的關系??
五、ThreadLocal 常見使用場景
一、ThreadLocal簡介
ThreadLocal叫做線程變量,意思是ThreadLocal中填充的變量屬于當前線程,該變量對其他線程而言是隔離的,也就是說該變量是當前線程獨有的變量。ThreadLocal為變量在每個線程中都創建了一個副本,那么每個線程可以訪問自己內部的副本變量。
ThreadLoal 變量,線程局部變量,同一個 ThreadLocal 所包含的對象,在不同的 Thread 中有不同的副本。這里有幾點需要注意:
- 因為每個 Thread 內有自己的實例副本,且該副本只能由當前 Thread 使用。這是也是 ThreadLocal 命名的由來。
- 既然每個 Thread 有自己的實例副本,且其它 Thread 不可訪問,那就不存在多線程間共享的問題。
ThreadLocal 提供了線程本地的實例。它與普通變量的區別在于,每個使用該變量的線程都會初始化一個完全獨立的實例副本。ThreadLocal 變量通常被private static修飾。當一個線程結束時,它所使用的所有 ThreadLocal 相對的實例副本都可被回收。
總的來說,ThreadLocal 適用于每個線程需要自己獨立的實例且該實例需要在多個方法中被使用,也即變量在線程間隔離而在方法或類間共享的場景
下圖可以增強理解:
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?圖1-1? ThreadLocal在使用過程中狀態
二、ThreadLocal與Synchronized的區別
ThreadLocal<T>其實是與線程綁定的一個變量。ThreadLocal和Synchonized都用于解決多線程并發訪問。
但是ThreadLocal與synchronized有本質的區別:
1、Synchronized用于線程間的數據共享,而ThreadLocal則用于線程間的數據隔離。
2、Synchronized是利用鎖的機制,使變量或代碼塊在某一時該只能被一個線程訪問。而ThreadLocal為每一個線程都提供了變量的副本
,使得每個線程在某一時間訪問到的并不是同一個對象,這樣就隔離了多個線程對數據的數據共享。
而Synchronized卻正好相反,它用于在多個線程間通信時能夠獲得數據共享。
一句話理解ThreadLocal,threadlocl是作為當前線程中屬性ThreadLocalMap集合中的某一個Entry的key值Entry(threadlocl,value),雖然不同的線程之間threadlocal這個key值是一樣,但是不同的線程所擁有的ThreadLocalMap是獨一無二的,也就是不同的線程間同一個ThreadLocal(key)對應存儲的值(value)不一樣,從而到達了線程間變量隔離的目的,但是在同一個線程中這個value變量地址是一樣的。
三、ThreadLocal的簡單使用
直接上代碼:
public class ThreadLocaDemo {private static ThreadLocal<String> localVar = new ThreadLocal<String>();static void print(String str) {//打印當前線程中本地內存中本地變量的值System.out.println(str + " :" + localVar.get());//清除本地內存中的本地變量localVar.remove();}public static void main(String[] args) throws InterruptedException {new Thread(new Runnable() {public void run() {ThreadLocaDemo.localVar.set("local_A");print("A");//打印本地變量System.out.println("after remove : " + localVar.get());}},"A").start();Thread.sleep(1000);new Thread(new Runnable() {public void run() {ThreadLocaDemo.localVar.set("local_B");print("B");System.out.println("after remove : " + localVar.get());}},"B").start();} }A :local_A after remove : null B :local_B after remove : null從這個示例中我們可以看到,兩個線程分表獲取了自己線程存放的變量,他們之間變量的獲取并不會錯亂。這個的理解也可以結合圖1-1,相信會有一個更深刻的理解。
四、ThreadLocal的原理
要看原理那么就得從源碼看起。
? 4.1 ThreadLocal的set()方法:
public void set(T value) {//1、獲取當前線程Thread t = Thread.currentThread();//2、獲取線程中的屬性 threadLocalMap ,如果threadLocalMap 不為空,//則直接更新要保存的變量值,否則創建threadLocalMap,并賦值ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);else// 初始化thradLocalMap 并賦值createMap(t, value);}? ? ? ? ?從上面的代碼可以看出,ThreadLocal? set賦值的時候首先會獲取當前線程thread,并獲取thread線程中的ThreadLocalMap屬性。如果map屬性不為空,則直接更新value值,如果map為空,則實例化threadLocalMap,并將value值初始化。
那么ThreadLocalMap又是什么呢,還有createMap又是怎么做的,我們繼續往下看。大家最后自己再idea上跟下源碼,會有更深的認識。
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是ThreadLocal的內部靜態類,而它的構成主要是用Entry來保存數據 ,而且還是繼承的弱引用。在Entry內部使用ThreadLocal作為key,使用我們設置的value作為value。詳細內容要大家自己去跟。
//這個是threadlocal 的內部方法 void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);}//ThreadLocalMap 構造方法 ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {table = new Entry[INITIAL_CAPACITY];int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);table[i] = new Entry(firstKey, firstValue);size = 1;setThreshold(INITIAL_CAPACITY);}? ? ? ? ?4.2 ThreadLocal的get方法
? ? ??
public T get() {//1、獲取當前線程Thread t = Thread.currentThread();//2、獲取當前線程的ThreadLocalMapThreadLocalMap map = getMap(t);//3、如果map數據不為空,if (map != null) {//3.1、獲取threalLocalMap中存儲的值ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}//如果是數據為null,則初始化,初始化的結果,TheralLocalMap中存放key值為threadLocal,值為nullreturn setInitialValue();}private T setInitialValue() {T value = initialValue();Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);return value;}4.3 ThreadLocal的remove方法
public void remove() {ThreadLocalMap m = getMap(Thread.currentThread());if (m != null)m.remove(this);}?remove方法,直接將ThrealLocal 對應的值從當前相差Thread中的ThreadLocalMap中刪除。為什么要刪除,這涉及到內存泄露的問題。
實際上?ThreadLocalMap?中使用的 key 為 ThreadLocal 的弱引用,弱引用的特點是,如果這個對象只存在弱引用,那么在下一次垃圾回收的時候必然會被清理掉。
所以如果 ThreadLocal 沒有被外部強引用的情況下,在垃圾回收的時候會被清理掉的,這樣一來?ThreadLocalMap中使用這個 ThreadLocal 的 key 也會被清理掉。但是,value 是強引用,不會被清理,這樣一來就會出現 key 為 null 的 value。
ThreadLocal其實是與線程綁定的一個變量,如此就會出現一個問題:如果沒有將ThreadLocal內的變量刪除(remove)或替換,它的生命周期將會與線程共存。通常線程池中對線程管理都是采用線程復用的方法,在線程池中線程很難結束甚至于永遠不會結束,這將意味著線程持續的時間將不可預測,甚至與JVM的生命周期一致。舉個例字,如果ThreadLocal中直接或間接包裝了集合類或復雜對象,每次在同一個ThreadLocal中取出對象后,再對內容做操作,那么內部的集合類和復雜對象所占用的空間可能會開始持續膨脹。
?4.4、ThreadLocal與Thread,ThreadLocalMap之間的關系??
圖4-1 Thread、THreadLocal、ThreadLocalMap之間啊的數據關系圖
從這個圖中我們可以非常直觀的看出,ThreadLocalMap其實是Thread線程的一個屬性值,而ThreadLocal是維護ThreadLocalMap
這個屬性指的一個工具類。Thread線程可以擁有多個ThreadLocal維護的自己線程獨享的共享變量(這個共享變量只是針對自己線程里面共享)
五、ThreadLocal 常見使用場景
如上文所述,ThreadLocal 適用于如下兩種場景
- 1、每個線程需要有自己單獨的實例
- 2、實例需要在多個方法中共享,但不希望被多線程共享
對于第一點,每個線程擁有自己實例,實現它的方式很多。例如可以在線程內部構建一個單獨的實例。ThreadLoca 可以以非常方便的形式滿足該需求。
對于第二點,可以在滿足第一點(每個線程有自己的實例)的條件下,通過方法間引用傳遞的形式實現。ThreadLocal 使得代碼耦合度更低,且實現更優雅。
場景
1)存儲用戶Session
一個簡單的用ThreadLocal來存儲Session的例子:
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;}場景二、數據庫連接,處理數據庫事務
場景三、數據跨層傳遞(controller,service, dao)
? ? ? 每個線程內需要保存類似于全局變量的信息(例如在攔截器中獲取的用戶信息),可以讓不同方法直接使用,避免參數傳遞的麻煩卻不想被多線程共享(因為不同線程獲取到的用戶信息不一樣)。
例如,用 ThreadLocal 保存一些業務內容(用戶權限信息、從用戶系統獲取到的用戶名、用戶ID 等),這些信息在同一個線程內相同,但是不同的線程使用的業務內容是不相同的。
在線程生命周期內,都通過這個靜態 ThreadLocal 實例的 get() 方法取得自己 set 過的那個對象,避免了將這個對象(如 user 對象)作為參數傳遞的麻煩。
比如說我們是一個用戶系統,那么當一個請求進來的時候,一個線程會負責執行這個請求,然后這個請求就會依次調用service-1()、service-2()、service-3()、service-4(),這4個方法可能是分布在不同的類中的。這個例子和存儲session有些像。
package com.kong.threadlocal;public class ThreadLocalDemo05 {public static void main(String[] args) {User user = new User("jack");new Service1().service1(user);}}class Service1 {public void service1(User user){//給ThreadLocal賦值,后續的服務直接通過ThreadLocal獲取就行了。UserContextHolder.holder.set(user);new Service2().service2();} }class Service2 {public void service2(){User user = UserContextHolder.holder.get();System.out.println("service2拿到的用戶:"+user.name);new Service3().service3();} }class Service3 {public void service3(){User user = UserContextHolder.holder.get();System.out.println("service3拿到的用戶:"+user.name);//在整個流程執行完畢后,一定要執行removeUserContextHolder.holder.remove();} }class UserContextHolder {//創建ThreadLocal保存User對象public static ThreadLocal<User> holder = new ThreadLocal<>(); }class User {String name;public User(String name){this.name = name;} }執行的結果:service2拿到的用戶:jack service3拿到的用戶:jack場景四、Spring使用ThreadLocal解決線程安全問題?
我們知道在一般情況下,只有無狀態的Bean才可以在多線程環境下共享,在Spring中,絕大部分Bean都可以聲明為singleton作用域。就是因為Spring對一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非線程安全的“狀態性對象”采用ThreadLocal進行封裝,讓它們也成為線程安全的“狀態性對象”,因此有狀態的Bean就能夠以singleton的方式在多線程中正常工作了。?
一般的Web應用劃分為展現層、服務層和持久層三個層次,在不同的層中編寫對應的邏輯,下層通過接口向上層開放功能調用。在一般情況下,從接收請求到返回響應所經過的所有程序調用都同屬于一個線程,如圖9-2所示。?
?
?
這樣用戶就可以根據需要,將一些非線程安全的變量以ThreadLocal存放,在同一次請求響應的調用線程中,所有對象所訪問的同一ThreadLocal變量都是當前線程所綁定的。
下面的實例能夠體現Spring對有狀態Bean的改造思路:
?
代碼清單9-5? TopicDao:非線程安全
public class TopicDao {//①一個非線程安全的變量private Connection conn; public void addTopic(){//②引用非線程安全變量Statement stat = conn.createStatement();…}由于①處的conn是成員變量,因為addTopic()方法是非線程安全的,必須在使用時創建一個新TopicDao實例(非singleton)。下面使用ThreadLocal對conn這個非線程安全的“狀態”進行改造:?
代碼清單9-6? TopicDao:線程安全?
不同的線程在使用TopicDao時,先判斷connThreadLocal.get()是否為null,如果為null,則說明當前線程還沒有對應的Connection對象,這時創建一個Connection對象并添加到本地線程變量中;如果不為null,則說明當前的線程已經擁有了Connection對象,直接使用就可以了。這樣,就保證了不同的線程使用線程相關的Connection,而不會使用其他線程的Connection。因此,這個TopicDao就可以做到singleton共享了。?
當然,這個例子本身很粗糙,將Connection的ThreadLocal直接放在Dao只能做到本Dao的多個方法共享Connection時不發生線程安全問題,但無法和其他Dao共用同一個Connection,要做到同一事務多Dao共享同一個Connection,必須在一個共同的外部類使用ThreadLocal保存Connection。但這個實例基本上說明了Spring對有狀態類線程安全化的解決思路。在本章后面的內容中,我們將詳細說明Spring如何通過ThreadLocal解決事務管理的問題。
后續在補充
下一篇:史上最全ThreadLocal 詳解(二)
大家幫我看看這篇文章:一文搞懂AQS,AQS從入門到源碼閱讀
參考:
ThreadLocal的應用場景 - sw_kong - 博客園
百度安全驗證
ThreadLocal使用場景分析 - 簡書
ThreadLocal原理分析與使用場景 - 阿凡盧 - 博客園
ThreadLocal原理分析與使用場景 - 阿凡盧 - 博客園
ThreadLocal使用場景分析 - 簡書
(轉)spring里面對ThreadLocal的使用_迷茫之欲的博客-CSDN博客_spring threadlocal
總結
以上是生活随笔為你收集整理的史上最全ThreadLocal 详解(一)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 易优CMS瓜果蔬菜农业种植基地网站模板源
- 下一篇: 二维ITK连通区域提取