【高并發ThreadLocal學會了這些,你也能和面試官扯皮了!

      網友投稿 627 2025-04-02

      大家好,我是冰河~~


      我們都知道,在多線程環境下訪問同一個共享變量,可能會出現線程安全的問題,為了保證線程安全,我們往往會在訪問這個共享變量的時候加鎖,以達到同步的效果,如下圖所示。

      對共享變量加鎖雖然能夠保證線程的安全,但是卻增加了開發人員對鎖的使用技能,如果鎖使用不當,則會導致死鎖的問題。而ThreadLocal能夠做到在創建變量后,每個線程對變量訪問時訪問的是線程自己的本地變量。

      什么是ThreadLocal?

      ThreadLocal是JDK提供的,支持線程本地變量。也就是說,如果我們創建了一個ThreadLocal變量,則訪問這個變量的每個線程都會有這個變量的一個本地副本。如果多個線程同時對這個變量進行讀寫操作時,實際上操作的是線程自己本地內存中的變量,從而避免了線程安全的問題。

      ThreadLocal使用示例

      例如,我們使用ThreadLocal保存并打印相關的變量信息,程序如下所示。

      public class ThreadLocalTest { private static ThreadLocal threadLocal = new ThreadLocal(); public static void main(String[] args){ //創建第一個線程 Thread threadA = new Thread(()->{ threadLocal.set("ThreadA:" + Thread.currentThread().getName()); System.out.println("線程A本地變量中的值為:" + threadLocal.get()); }); //創建第二個線程 Thread threadB = new Thread(()->{ threadLocal.set("ThreadB:" + Thread.currentThread().getName()); System.out.println("線程B本地變量中的值為:" + threadLocal.get()); }); //啟動線程A和線程B threadA.start(); threadB.start(); } }

      運行程序,打印的結果信息如下所示。

      線程A本地變量中的值為:ThreadA:Thread-0 線程B本地變量中的值為:ThreadB:Thread-1

      此時,我們為線程A增加刪除ThreadLocal中的變量的操作,如下所示。

      public class ThreadLocalTest { private static ThreadLocal threadLocal = new ThreadLocal(); public static void main(String[] args){ //創建第一個線程 Thread threadA = new Thread(()->{ threadLocal.set("ThreadA:" + Thread.currentThread().getName()); System.out.println("線程A本地變量中的值為:" + threadLocal.get()); threadLocal.remove(); System.out.println("線程A刪除本地變量后ThreadLocal中的值為:" + threadLocal.get()); }); //創建第二個線程 Thread threadB = new Thread(()->{ threadLocal.set("ThreadB:" + Thread.currentThread().getName()); System.out.println("線程B本地變量中的值為:" + threadLocal.get()); System.out.println("線程B沒有刪除本地變量:" + threadLocal.get()); }); //啟動線程A和線程B threadA.start(); threadB.start(); } }

      此時的運行結果如下所示。

      線程A本地變量中的值為:ThreadA:Thread-0 線程B本地變量中的值為:ThreadB:Thread-1 線程B沒有刪除本地變量:ThreadB:Thread-1 線程A刪除本地變量后ThreadLocal中的值為:null

      通過上述程序我們可以看出,線程A和線程B存儲在ThreadLocal中的變量互不干擾,線程A存儲的變量只能由線程A訪問,線程B存儲的變量只能由線程B訪問。

      ThreadLocal原理

      首先,我們看下Thread類的源碼,如下所示。

      public class Thread implements Runnable { /***********省略N行代碼*************/ ThreadLocal.ThreadLocalMap threadLocals = null; ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; /***********省略N行代碼*************/ }

      由Thread類的源碼可以看出,在ThreadLocal類中存在成員變量threadLocals和inheritableThreadLocals,這兩個成員變量都是ThreadLocalMap類型的變量,而且二者的初始值都為null。只有當前線程第一次調用ThreadLocal的set()方法或者get()方法時才會實例化變量。

      這里需要注意的是:每個線程的本地變量不是存放在ThreadLocal實例里面的,而是存放在調用線程的threadLocals變量里面的。也就是說,調用ThreadLocal的set()方法存儲的本地變量是存放在具體線程的內存空間中的,而ThreadLocal類只是提供了set()和get()方法來存儲和讀取本地變量的值,當調用ThreadLocal類的set()方法時,把要存儲的值放入調用線程的threadLocals中存儲起來,當調用ThreadLocal類的get()方法時,從當前線程的threadLocals變量中將存儲的值取出來。

      接下來,我們分析下ThreadLocal類的set()、get()和remove()方法的實現邏輯。

      set()方法

      set()方法的源代碼如下所示。

      public void set(T value) { //獲取當前線程 Thread t = Thread.currentThread(); //以當前線程為Key,獲取ThreadLocalMap對象 ThreadLocalMap map = getMap(t); //獲取的ThreadLocalMap對象不為空 if (map != null) //設置value的值 map.set(this, value); else //獲取的ThreadLocalMap對象為空,創建Thread類中的threadLocals變量 createMap(t, value); }

      在set()方法中,首先獲取調用set()方法的線程,接下來,使用當前線程作為Key調用getMap(t)方法來獲取ThreadLocalMap對象,getMap(Thread t)的方法源碼如下所示。

      ThreadLocalMap getMap(Thread t) { return t.threadLocals; }

      可以看到,getMap(Thread t)方法獲取的是線程變量自身的threadLocals成員變量。

      在set()方法中,如果調用getMap(t)方法返回的對象不為空,則把value值設置到Thread類的threadLocals成員變量中,而傳遞的key為當前ThreadLocal的this對象,value就是通過set()方法傳遞的值。

      【高并發】ThreadLocal學會了這些,你也能和面試官扯皮了!

      如果調用getMap(t)方法返回的對象為空,則程序調用createMap(t, value)方法來實例化Thread類的threadLocals成員變量。

      void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }

      也就是創建當前線程的threadLocals變量。

      get()方法

      get()方法的源代碼如下所示。

      public T get() { //獲取當前線程 Thread t = Thread.currentThread(); //獲取當前線程的threadLocals成員變量 ThreadLocalMap map = getMap(t); //獲取的threadLocals變量不為空 if (map != null) { //返回本地變量對應的值 ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } //初始化threadLocals成員變量的值 return setInitialValue(); }

      通過當前線程來獲取threadLocals成員變量,如果threadLocals成員變量不為空,則直接返回當前線程綁定的本地變量,否則調用setInitialValue()方法初始化threadLocals成員變量的值。

      private T setInitialValue() { //調用初始化Value的方法 T value = initialValue(); Thread t = Thread.currentThread(); //根據當前線程獲取threadLocals成員變量 ThreadLocalMap map = getMap(t); if (map != null) //threadLocals不為空,則設置value值 map.set(this, value); else //threadLocals為空,創建threadLocals變量 createMap(t, value); return value; }

      其中,initialValue()方法的源碼如下所示。

      protected T initialValue() { return null; }

      通過initialValue()方法的源碼可以看出,這個方法可以由子類覆寫,在ThreadLocal類中,這個方法直接返回null。

      remove()方法

      remove()方法的源代碼如下所示。

      public void remove() { //根據當前線程獲取threadLocals成員變量 ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) //threadLocals成員變量不為空,則移除value值 m.remove(this); }

      remove()方法的實現比較簡單,首先根據當前線程獲取threadLocals成員變量,不為空,則直接移除value的值。

      注意:如果調用線程一致不終止,則本地變量會一直存放在調用線程的threadLocals成員變量中,所以,如果不需要使用本地變量時,可以通過調用ThreadLocal的remove()方法,將本地變量從當前線程的threadLocals成員變量中刪除,以免出現內存溢出的問題。

      ThreadLocal變量不具有傳遞性

      使用ThreadLocal存儲本地變量不具有傳遞性,也就是說,同一個ThreadLocal在父線程中設置值后,在子線程中是無法獲取到這個值的,這個現象說明ThreadLocal中存儲的本地變量不具有傳遞性。

      接下來,我們來看一段代碼,如下所示。

      public class ThreadLocalTest { private static ThreadLocal threadLocal = new ThreadLocal(); public static void main(String[] args){ //在主線程中設置值 threadLocal.set("ThreadLocalTest"); //在子線程中獲取值 Thread thread = new Thread(new Runnable() { @Override public void run() { System.out.println("子線程獲取值:" + threadLocal.get()); } }); //啟動子線程 thread.start(); //在主線程中獲取值 System.out.println("主線程獲取值:" + threadLocal.get()); } }

      運行這段代碼輸出的結果信息如下所示。

      主線程獲取值:ThreadLocalTest 子線程獲取值:null

      通過上述程序,我們可以看出在主線程中向ThreadLocal設置值后,在子線程中是無法獲取到這個值的。那有沒有辦法在子線程中獲取到主線程設置的值呢?此時,我們可以使用InheritableThreadLocal來解決這個問題。

      InheritableThreadLocal使用示例

      InheritableThreadLocal類繼承自ThreadLocal類,它能夠讓子線程訪問到在父線程中設置的本地變量的值,例如,我們將ThreadLocalTest類中的threadLocal靜態變量改寫成InheritableThreadLocal類的實例,如下所示。

      public class ThreadLocalTest { private static ThreadLocal threadLocal = new InheritableThreadLocal(); public static void main(String[] args){ //在主線程中設置值 threadLocal.set("ThreadLocalTest"); //在子線程中獲取值 Thread thread = new Thread(new Runnable() { @Override public void run() { System.out.println("子線程獲取值:" + threadLocal.get()); } }); //啟動子線程 thread.start(); //在主線程中獲取值 System.out.println("主線程獲取值:" + threadLocal.get()); } }

      此時,運行程序輸出的結果信息如下所示。

      主線程獲取值:ThreadLocalTest 子線程獲取值:ThreadLocalTest

      可以看到,使用InheritableThreadLocal類存儲本地變量時,子線程能夠獲取到父線程中設置的本地變量。

      InheritableThreadLocal原理

      首先,我們來看下InheritableThreadLocal類的源碼,如下所示。

      public class InheritableThreadLocal extends ThreadLocal { protected T childValue(T parentValue) { return parentValue; } ThreadLocalMap getMap(Thread t) { return t.inheritableThreadLocals; } void createMap(Thread t, T firstValue) { t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue); } }

      由InheritableThreadLocal類的源代碼可知,InheritableThreadLocal類繼承自ThreadLocal類,并且重寫了ThreadLocal類的childValue()方法、getMap()方法和createMap()方法。也就是說,當調用ThreadLocal的set()方法時,創建的是當前Thread線程的inheritableThreadLocals成員變量而不再是threadLocals成員變量。

      這里,我們需要思考一個問題:InheritableThreadLocal類的childValue()方法是何時被調用的呢? 這就需要我們來看下Thread類的構造方法了,如下所示。

      public Thread() { init(null, null, "Thread-" + nextThreadNum(), 0); } public Thread(Runnable target) { init(null, target, "Thread-" + nextThreadNum(), 0); } Thread(Runnable target, AccessControlContext acc) { init(null, target, "Thread-" + nextThreadNum(), 0, acc, false); } public Thread(ThreadGroup group, Runnable target) { init(group, target, "Thread-" + nextThreadNum(), 0); } public Thread(String name) { init(null, null, name, 0); } public Thread(ThreadGroup group, String name) { init(group, null, name, 0); } public Thread(Runnable target, String name) { init(null, target, name, 0); } public Thread(ThreadGroup group, Runnable target, String name) { init(group, target, name, 0); } public Thread(ThreadGroup group, Runnable target, String name, long stackSize) { init(group, target, name, stackSize); }

      可以看到,Thread類的構造方法最終調用的是init()方法,那我們就來看下init()方法,如下所示。

      private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) { /************省略部分源碼************/ if (inheritThreadLocals && parent.inheritableThreadLocals != null) this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); /* Stash the specified stack size in case the VM cares */ this.stackSize = stackSize; /* Set thread ID */ tid = nextThreadID(); }

      可以看到,在init()方法中會判斷傳遞的inheritThreadLocals變量是否為true,同時父線程中的inheritableThreadLocals是否為null,如果傳遞的inheritThreadLocals變量為true,同時,父線程中的inheritableThreadLocals不為null,則調用ThreadLocal類的createInheritedMap()方法。

      static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) { return new ThreadLocalMap(parentMap); }

      在createInheritedMap()中,使用父線程的inheritableThreadLocals變量作為參數創建新的ThreadLocalMap對象。然后在Thread類的init()方法中會將這個ThreadLocalMap對象賦值給子線程的inheritableThreadLocals成員變量。

      接下來,我們來看看ThreadLocalMap的構造函數都干了啥,如下所示。

      private ThreadLocalMap(ThreadLocalMap parentMap) { Entry[] parentTable = parentMap.table; int len = parentTable.length; setThreshold(len); table = new Entry[len]; for (int j = 0; j < len; j++) { Entry e = parentTable[j]; if (e != null) { @SuppressWarnings("unchecked") ThreadLocal key = (ThreadLocal) e.get(); if (key != null) { //調用重寫的childValue方法 Object value = key.childValue(e.value); Entry c = new Entry(key, value); int h = key.threadLocalHashCode & (len - 1); while (table[h] != null) h = nextIndex(h, len); table[h] = c; size++; } } } }

      在ThreadLocalMap的構造函數中,調用了InheritableThreadLocal類重寫的childValue()方法。而InheritableThreadLocal類通過重寫getMap()方法和createMap()方法,讓本地變量保存到了Thread線程的inheritableThreadLocals變量中,線程通過InheritableThreadLocal類的set()方法和get()方法設置變量時,就會創建當前線程的inheritableThreadLocals變量。此時,如果父線程創建子線程,在Thread類的構造函數中會把父線程中的inheritableThreadLocals變量里面的本地變量復制一份保存到子線程的inheritableThreadLocals變量中。

      如果覺得文章對你有點幫助,請微信搜索并關注「 冰河技術 」微信公眾號,跟冰河學習高并發編程技術。

      最后,附上并發編程需要掌握的核心技能知識圖,祝大家在學習并發編程時,少走彎路。

      好了,今天就到這兒吧,我是冰河,我們下期見~~

      Java JDK JVM 任務調度 多線程

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      上一篇:excel表格兩張表格對比怎么弄
      下一篇:excel2007表頭兩條斜線怎么做
      相關文章
      亚洲成人在线免费观看| 亚洲视频一区二区在线观看| 久久亚洲最大成人网4438| 97久久精品亚洲中文字幕无码| 国产亚洲精品a在线无码| 久久影视综合亚洲| 亚洲桃色AV无码| 在线亚洲97se亚洲综合在线| 中文字幕亚洲综合久久男男| 亚洲中文字幕在线第六区| a级亚洲片精品久久久久久久| 国产亚洲日韩一区二区三区| 国产亚洲精品拍拍拍拍拍| 亚洲自偷自偷在线制服| 久久精品国产亚洲网站| 亚洲AV日韩AV永久无码绿巨人| 久久久无码精品亚洲日韩蜜桃| 夜夜亚洲天天久久| 亚洲自偷自拍另类图片二区| 亚洲国产精品成人精品软件| 亚洲人成日本在线观看| 亚洲AV无码一区二区三区牛牛| 亚洲日本成本人观看| 色婷婷六月亚洲综合香蕉| 国产精品无码亚洲精品2021| yy6080亚洲一级理论| 国产精品亚洲产品一区二区三区| 亚洲中文字幕无码中文字在线| 久久国产亚洲电影天堂| 中文字幕亚洲综合久久| 亚洲jjzzjjzz在线播放| 亚洲欧洲日产国码久在线| 综合一区自拍亚洲综合图区 | 亚洲依依成人精品| 亚洲а∨天堂久久精品9966| 日本系列1页亚洲系列| 久久久久亚洲AV无码专区桃色| 精品亚洲综合久久中文字幕| 91亚洲国产成人久久精品网站| 亚洲伊人久久大香线蕉影院| 亚洲乱妇老熟女爽到高潮的片|