Java中ThreadLocal使用原理及Synchronized區(qū)別
一、ThreadLocal簡介
ThreadLocal叫做線程變量,意思是ThreadLocal中填充的變量屬于當前線程,該變量對其他線程而言是隔離的,也就是說該變量是當前線程獨有的變量。ThreadLocal為變量在每個線程中都創(chuàng)建了一個副本,那么每個線程可以訪問自己內(nèi)部的副本變量。
ThreadLoal 變量,線程局部變量,同一個 ThreadLocal 所包含的對象,在不同的 Thread 中有不同的副本。這里有幾點需要注意:
- 因為每個 Thread 內(nèi)有自己的實例副本,且該副本只能由當前 Thread 使用。這是也是 ThreadLocal 命名的由來。
- 既然每個 Thread 有自己的實例副本,且其它 Thread 不可訪問,那就不存在多線程間共享的問題。
ThreadLocal 提供了線程本地的實例。它與普通變量的區(qū)別在于,每個使用該變量的線程都會初始化一個完全獨立的實例副本。ThreadLocal 變量通常被private static修飾。當一個線程結束時,它所使用的所有 ThreadLocal 相對的實例副本都可被回收。
總的來說,ThreadLocal 適用于每個線程需要自己獨立的實例且該實例需要在多個方法中被使用,也即變量在線程間隔離而在方法或類間共享的場景
下圖可以增強理解:
圖1-1 ThreadLocal在使用過程中狀態(tài)
二、ThreadLocal與Synchronized的區(qū)別
ThreadLocal<T>其實是與線程綁定的一個變量。ThreadLocal和Synchonized都用于解決多線程并發(fā)訪問。
但是ThreadLocal與synchronized有本質(zhì)的區(qū)別:
1、Synchronized用于線程間的數(shù)據(jù)共享,而ThreadLocal則用于線程間的數(shù)據(jù)隔離。
2、Synchronized是利用鎖的機制,使變量或代碼塊在某一時該只能被一個線程訪問。而ThreadLocal為每一個線程都提供了變量的副本,使得每個線程在某一時間訪問到的并不是同一個對象,這樣就隔離了多個線程對數(shù)據(jù)的數(shù)據(jù)共享。
而Synchronized卻正好相反,它用于在多個線程間通信時能夠獲得數(shù)據(jù)共享。
一句話理解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) { //打印當前線程中本地內(nèi)存中本地變量的值 System.out.println(str + " :" + localVar.get()); //清除本地內(nèi)存中的本地變量 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 不為空, //則直接更新要保存的變量值,否則創(chuàng)建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又是怎么做的,我們繼續(xù)往下看。大家最后自己再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的內(nèi)部靜態(tài)類,而它的構成主要是用Entry來保存數(shù)據(jù) ,而且還是繼承的弱引用。在Entry內(nèi)部使用ThreadLocal作為key,使用我們設置的value作為value。詳細內(nèi)容要大家自己去跟。
//這個是threadlocal 的內(nèi)部方法 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、獲取當前線程的ThreadLocalMap ThreadLocalMap map = getMap(t); //3、如果map數(shù)據(jù)不為空, if (map != null) { //3.1、獲取threalLocalMap中存儲的值 ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } //如果是數(shù)據(jù)為null,則初始化,初始化的結果,TheralLocalMap中存放key值為threadLocal,值為null 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; }
4.3 ThreadLocal的remove方法
public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); }
remove方法,直接將ThrealLocal 對應的值從當前相差Thread中的ThreadLocalMap中刪除。為什么要刪除,這涉及到內(nèi)存泄露的問題。
實際上 ThreadLocalMap 中使用的 key 為 ThreadLocal 的弱引用,弱引用的特點是,如果這個對象只存在弱引用,那么在下一次垃圾回收的時候必然會被清理掉。
所以如果 ThreadLocal 沒有被外部強引用的情況下,在垃圾回收的時候會被清理掉的,這樣一來 ThreadLocalMap中使用這個 ThreadLocal 的 key 也會被清理掉。但是,value 是強引用,不會被清理,這樣一來就會出現(xiàn) key 為 null 的 value。
ThreadLocal其實是與線程綁定的一個變量,如此就會出現(xiàn)一個問題:如果沒有將ThreadLocal內(nèi)的變量刪除(remove)或替換,它的生命周期將會與線程共存。通常線程池中對線程管理都是采用線程復用的方法,在線程池中線程很難結束甚至于永遠不會結束,這將意味著線程持續(xù)的時間將不可預測,甚至與JVM的生命周期一致。舉個例字,如果ThreadLocal中直接或間接包裝了集合類或復雜對象,每次在同一個ThreadLocal中取出對象后,再對內(nèi)容做操作,那么內(nèi)部的集合類和復雜對象所占用的空間可能會開始持續(xù)膨脹。
4.4、ThreadLocal與Thread,ThreadLocalMap之間的關系
圖4-1 Thread、THreadLocal、ThreadLocalMap之間啊的數(shù)據(jù)關系圖
從這個圖中我們可以非常直觀的看出,ThreadLocalMap其實是Thread線程的一個屬性值,而ThreadLocal是維護ThreadLocalMap
這個屬性指的一個工具類。Thread線程可以擁有多個ThreadLocal維護的自己線程獨享的共享變量(這個共享變量只是針對自己線程里面共享)
五、ThreadLocal 常見使用場景
如上文所述,ThreadLocal 適用于如下兩種場景
1、每個線程需要有自己單獨的實例
2、實例需要在多個方法中共享,但不希望被多線程共享
對于第一點,每個線程擁有自己實例,實現(xiàn)它的方式很多。例如可以在線程內(nèi)部構建一個單獨的實例。ThreadLoca 可以以非常方便的形式滿足該需求。
對于第二點,可以在滿足第一點(每個線程有自己的實例)的條件下,通過方法間引用傳遞的形式實現(xiàn)。ThreadLocal 使得代碼耦合度更低,且實現(xiàn)更優(yōu)雅。
場景
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; }
場景二、數(shù)據(jù)庫連接,處理數(shù)據(jù)庫事務
場景三、數(shù)據(jù)跨層傳遞(controller,service, dao)
每個線程內(nèi)需要保存類似于全局變量的信息(例如在攔截器中獲取的用戶信息),可以讓不同方法直接使用,避免參數(shù)傳遞的麻煩卻不想被多線程共享(因為不同線程獲取到的用戶信息不一樣)。
例如,用 ThreadLocal 保存一些業(yè)務內(nèi)容(用戶權限信息、從用戶系統(tǒng)獲取到的用戶名、用戶ID 等),這些信息在同一個線程內(nèi)相同,但是不同的線程使用的業(yè)務內(nèi)容是不相同的。
在線程生命周期內(nèi),都通過這個靜態(tài) ThreadLocal 實例的 get() 方法取得自己 set 過的那個對象,避免了將這個對象(如 user 對象)作為參數(shù)傳遞的麻煩。
比如說我們是一個用戶系統(tǒng),那么當一個請求進來的時候,一個線程會負責執(zhí)行這個請求,然后這個請求就會依次調(diào)用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賦值,后續(xù)的服務直接通過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); //在整個流程執(zhí)行完畢后,一定要執(zhí)行remove UserContextHolder.holder.remove(); } } class UserContextHolder { //創(chuàng)建ThreadLocal保存User對象 public static ThreadLocal<User> holder = new ThreadLocal<>(); } class User { String name; public User(String name){ this.name = name; } } 執(zhí)行的結果: service2拿到的用戶:jack service3拿到的用戶:jack
場景四、Spring使用ThreadLocal解決線程安全問題
我們知道在一般情況下,只有無狀態(tài)的Bean才可以在多線程環(huán)境下共享,在Spring中,絕大部分Bean都可以聲明為singleton作用域。就是因為Spring對一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非線程安全的“狀態(tài)性對象”采用ThreadLocal進行封裝,讓它們也成為線程安全的“狀態(tài)性對象”,因此有狀態(tài)的Bean就能夠以singleton的方式在多線程中正常工作了。
一般的Web應用劃分為展現(xiàn)層、服務層和持久層三個層次,在不同的層中編寫對應的邏輯,下層通過接口向上層開放功能調(diào)用。在一般情況下,從接收請求到返回響應所經(jīng)過的所有程序調(diào)用都同屬于一個線程,如圖9-2所示。
這樣用戶就可以根據(jù)需要,將一些非線程安全的變量以ThreadLocal存放,在同一次請求響應的調(diào)用線程中,所有對象所訪問的同一ThreadLocal變量都是當前線程所綁定的。
下面的實例能夠體現(xiàn)Spring對有狀態(tài)Bean的改造思路:
代碼清單9-5 TopicDao:非線程安全
public class TopicDao { //①一個非線程安全的變量 private Connection conn; public void addTopic(){ //②引用非線程安全變量 Statement stat = conn.createStatement(); … }
由于①處的conn是成員變量,因為addTopic()方法是非線程安全的,必須在使用時創(chuàng)建一個新TopicDao實例(非singleton)。下面使用ThreadLocal對conn這個非線程安全的“狀態(tài)”進行改造:
代碼清單9-6 TopicDao:線程安全
import java.sql.Connection; import java.sql.Statement; public class TopicDao { //①使用ThreadLocal保存Connection變量 private static ThreadLocal<Connection> connThreadLocal = new ThreadLocal<Connection>(); public static Connection getConnection(){ //②如果connThreadLocal沒有本線程對應的Connection創(chuàng)建一個新的Connection, //并將其保存到線程本地變量中。 if (connThreadLocal.get() == null) { Connection conn = ConnectionManager.getConnection(); connThreadLocal.set(conn); return conn; }else{ //③直接返回線程本地變量 return connThreadLocal.get(); } } public void addTopic() { //④從ThreadLocal中獲取線程對應的 Statement stat = getConnection().createStatement(); }
不同的線程在使用TopicDao時,先判斷connThreadLocal.get()是否為null,如果為null,則說明當前線程還沒有對應的Connection對象,這時創(chuàng)建一個Connection對象并添加到本地線程變量中;如果不為null,則說明當前的線程已經(jīng)擁有了Connection對象,直接使用就可以了。這樣,就保證了不同的線程使用線程相關的Connection,而不會使用其他線程的Connection。因此,這個TopicDao就可以做到singleton共享了。
當然,這個例子本身很粗糙,將Connection的ThreadLocal直接放在Dao只能做到本Dao的多個方法共享Connection時不發(fā)生線程安全問題,但無法和其他Dao共用同一個Connection,要做到同一事務多Dao共享同一個Connection,必須在一個共同的外部類使用ThreadLocal保存Connection。但這個實例基本上說明了Spring對有狀態(tài)類線程安全化的解決思路。在本章后面的內(nèi)容中,我們將詳細說明Spring如何通過ThreadLocal解決事務管理的問題。
參考
ThreadLocal的應用場景 - sw_kong - 博客園
ThreadLocal原理分析與使用場景 - 阿凡盧 - 博客園
ThreadLocal原理分析與使用場景 - 阿凡盧 - 博客園
(轉(zhuǎn))spring里面對ThreadLocal的使用_迷茫之欲的博客-CSDN博客_spring threadlocal
到此這篇關于Java中ThreadLocal使用原理及Synchronized區(qū)別的文章就介紹到這了,更多相關Java ThreadLocal Synchronized 內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
springboot?使用websocket技術主動給前端發(fā)送消息的實現(xiàn)
這篇文章主要介紹了springboot?使用websocket技術主動給前端發(fā)送消息的實現(xiàn)方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-12-12IDEA創(chuàng)建Spring項目無法選擇Java8的問題及解決
文章描述了在使用Spring創(chuàng)建項目時遇到的問題,通過將服務器地址從https://start.spring.io/替換為https://start.aliyun.com/,成功解決了無法選擇Java8的問題2025-01-01客戶端Socket與服務端ServerSocket串聯(lián)實現(xiàn)網(wǎng)絡通信
這篇文章主要為大家介紹了客戶端Socket與服務端ServerSocket串聯(lián)實現(xiàn)網(wǎng)絡通信的內(nèi)容詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助2022-03-03詳解Java使用Jsch與sftp服務器實現(xiàn)ssh免密登錄
這篇文章主要介紹了詳解Java使用Jsch與sftp服務器實現(xiàn)ssh免密登錄,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-10-10詳解Java如何實現(xiàn)在PDF中插入,替換或刪除圖像
圖文并茂的內(nèi)容往往讓人看起來更加舒服,如果只是文字內(nèi)容的累加,往往會使讀者產(chǎn)生視覺疲勞。搭配精美的文章配圖則會使文章內(nèi)容更加豐富。那我們要如何在PDF中插入、替換或刪除圖像呢?別擔心,今天為大家介紹一種高效便捷的方法2023-01-01