ThreadLocal(超詳細介紹!!)

關于ThreadLocal,可能很多同學在學習Java的并發編程部分時,都有所耳聞,但是如果要仔細問ThreadLocal是個啥,我們可能也說不清楚,所以這篇博客旨在幫助大家了解ThreadLocal到底是個啥?

1.ThreadLocal是什么?

首先,我們要知道的是,ThreadLocal類位于Java標準庫的java.lang包中,它是Java中的一個類,我們可以用它來聲明一個ThreadLocal變量,如下:

ThreadLocal<String> local = new ThreadLocal<>();

好的,接下來解釋一下ThreadLocal的含義:

它從名字上看,叫做本地線程變量,意思是說,ThreadLocal中填充的的是當前線程的變量,該變量對其他線程而言是封閉且隔離的,ThreadLocal為變量在每個線程中創建了一個副本,這樣每個線程都可以訪問自己內部的副本變量。

相信上面的文字描述大家會不太理解,簡單來說,就是用ThreadLocal創建的變量,我們可能會在不同的線程中用到,那么為了避免線程安全問題,每個線程都會為自己單獨存一份這個變量,并且單獨使用和修改這個變量,這樣不同的線程之間就各自使用各自的ThreadLocal變量,互不影響。

但是到底具體每個線程是怎樣存儲的這個變量,以及這個變量如何被這個線程調用,這些的底層是如何實現的,請接著向下看。

2.舉例

大家先看一個例子:

public class ThreadLocalTest02 {public static void main(String[] args) {
//創建一個ThreadLocal變量,名為localThreadLocal<String> local = new ThreadLocal<>();
//創建10個線程,并且每次都在不同的線程中加入這個local變量IntStream.range(0, 10).forEach(i -> new Thread(() -> {
//使用set方法設置加入的local內容local.set(Thread.currentThread().getName() + ":" + i);
//然后輸入當前線程存儲的local變量的信息System.out.println("線程:" + Thread.currentThread().getName() + ",local:" + local.get());}).start());}
}

結果如下:

輸出結果:
線程:Thread-0,local:Thread-0:0
線程:Thread-1,local:Thread-1:1
線程:Thread-2,local:Thread-2:2
線程:Thread-3,local:Thread-3:3
線程:Thread-4,local:Thread-4:4
線程:Thread-5,local:Thread-5:5
線程:Thread-6,local:Thread-6:6
線程:Thread-7,local:Thread-7:7
線程:Thread-8,local:Thread-8:8
線程:Thread-9,local:Thread-9:9

上面的結果說明了什么呢?我們在每個線程中都添加了local對象,并且內容是不同的,然后我們再使用get方法輸出local的值。我們發現,我們只使用了一個local對象,但是在十個線程中的值都是不同的,而且它們的值不會相互影響,這就是ThreadLocal的簡單應用。不同的線程對這個local對象有著自己的備份。

3.Set方法

請大家先仔細閱讀一下下面這段源碼,邏輯一點也不難,我加了注釋:

public class ThreadLocal<T> {public void set(T value) {
//先獲取當前線程,例如在線程1中調用了local.set方法,那么這個t就是線程1Thread t = Thread.currentThread();//然后獲取當前線程1中的ThreadLocalMapThreadLocalMap map = getMap(t);//如果map為空,說明此線程還沒有存入任何一個ThreadLocal對象,我們就創建一個ThreadLocalMap
//如果map不為空,那么我們就直接將value存入這個ThreadLocalMap中if (map != null)map.set(this, value);elsecreateMap(t, value);}

大家現在可能會有疑惑,什么是ThreadLocalMap啊?為啥是從當前線程中獲取啊?還有createMap方法,到底是干啥的捏?我們一一進行解釋:

什么是ThreadLocalMap?

我們剛才上面說,我們每個線程都會存儲ThreadLocal對象的備份,那么存儲在哪里呢,答案就是在ThreadLocalMap中,ThreadLocalMap為?ThreadLocal的一個靜態內部類,里面定義了Entry來保存數據,那么既然是map,就會有鍵值對的結構,鍵的位置存的就是我們的ThreadLocal對象,而值存儲的就是通過set方法存入的那個值,例如這一句代碼:local.set( i);那么存到這個線程中的ThreadLocalMap的一個entry中,鍵和值就分別是 local:i

接下來我們就看一下ThreadLocalMap(它是ThreadLocal的一個內部類,還有Entry的結構:

  // 內部類ThreadLocalMapstatic class ThreadLocalMap {static class Entry extends WeakReference<ThreadLocal<?>> {Object value;// 內部類Entry,實際存儲數據的地方// Entry的key是ThreadLocal對象,不是當前線程ID或者名稱Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}// 注意這里維護的是Entry數組private Entry[] table;}

可以看出實際上存儲數據的是Entry,而TheadLocalMap則是一個Entry數組;

ok,了解了這個結構后,我們又回到宏觀的角度看待一下問題,剛才說每個線程都有自己的備份,并且這些備份是當前線程獨有的,那么既然上面說存儲數據的是ThreadLocalMap,并且每個線程都有自己的獨有的一份,那么這個ThreadLocalMap到底存在哪里呢?答案就是:ThreadLocalMap是作為Thread類的一個私有屬性實現的,這樣就可以保證每個Thread線程都有自己獨一份的TheadLocalMap來存儲自己的Threadlocal變量。

public class Thread {/* ... 省略其他代碼 ... *//*** ThreadLocalMap實例,用于存儲ThreadLocal變量的鍵值對*/ThreadLocal.ThreadLocalMap threadLocals = null;/* ... 省略其他代碼 ... */
}

?好好好,現在我們算是知道了,原來為每個線程存儲這些ThreadLocal變量的,就是Thread類中的屬性threadLocals?

那么這樣我們就能解釋為什么要從線程中獲取map了,看一下剛才的set方法中的這一句,我們就知道為啥要從線程中獲取了:

//然后獲取當前線程1中的ThreadLocalMapThreadLocalMap map = getMap(t);

接著就是后面的代碼,相信大家也就能明白為什么要這樣寫啦:

//如果map為空,說明此線程還沒有存入任何一個ThreadLocal對象,我們就創建一個ThreadLocalMap
//如果map不為空,那么我們就直接將value存入這個ThreadLocalMap中if (map != null)map.set(this, value);elsecreateMap(t, value);}

這是createMap方法,看到它給什么賦值嗎,就是我們剛才說的Thread線程類中的那個存儲ThreadLocalMap的屬性哦~

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

ok,set方法就介紹到這里了

4.get方法

get方法的作用很簡單,它通過local對象調用,返回當前線程的以local對象為鍵,對應的那個值即可;例如 System.out.println(local.get());就是輸出當前線程的local對象當時通過set方法存入的值。

get方法的源碼如下:

    public T get() {
//先獲取當前調用get方法的線程Thread t = Thread.currentThread();//然后獲取此線程的ThreadLocalMap對象,這里面存儲著local鍵值對ThreadLocalMap map = getMap(t);/*如果map不為空,就在map里面尋找鍵為this的entry,為什么是this呢,因為當前類
是ThreadLocal類,而get方法通過local.get()的方式調用,所以這里的this就指的
是這個local對象,也就是entry的鍵。如果找的了這個以local為鍵的entry,我們就
返回對應的值即可。
*/if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}return setInitialValue();}

ok,ThreadLocal的具體應用和get方法就介紹到這里

5.ThreadLocal的結構

有了上面的基礎,我們現在來看一下他在內存中的結構:

6.內存泄漏問題

仔細看下ThreadLocal內存結構就會發現,Entry數組對象通過ThreadLocalMap最終被Thread持有,并且是強引用。也就是說Entry數組對象的生命周期和當前線程一樣。即使ThreadLocal對象被回收了,Entry數組對象也不一定被回收,這樣就有可能發生內存泄漏。ThreadLocal在設計的時候就提供了一些補救措施:

  • Entry的key是弱引用的ThreadLocal對象,很容易被回收,導致key為null(但是value不為null)。所以在調用get()、set(T)、remove()等方法的時候,會自動清理key為null的Entity。
  • remove()方法就是用來清理無用對象,防止內存泄漏的。所以每次用完ThreadLocal后需要手動remove()。

解決辦法:使用完ThreadLocal后,執行remove操作,避免出現內存溢出情況。

如同?lock?的操作最后要執行解鎖操作一樣,ThreadLocal使用完畢一定記得執行remove 方法,清除當前線程的數值。如果不remove?當前線程對應的VALUE?,就會一直存在這個值。

這里復習一下對象的強引用、軟引用、弱引用

1.強引用

我們平日里面的用到的new了一個對象就是強引用,例如 Object obj = new Object();當JVM的內存空間不足時,寧愿拋出OutOfMemoryError使得程序異常終止也不愿意回收具有強引用的存活著的對象!

2.軟引用

當JVM認為內存空間不足時,就回去試圖回收軟引用指向的對象,也就是說在JVM拋出OutOfMemoryError之前,會去清理軟引用對象。

3.弱引用

在GC的時候,不管內存空間足不足都會回收這個對象,同樣也可以配合ReferenceQueue 使用,也同樣適用于內存敏感的緩存。ThreadLocal中的key就用到了弱引用。

7.最后我們還要知道為什么要使用ThreadLocal?

ThreadLocal類在多線程編程中有幾個重要的用途和優勢:

  1. 線程隔離:ThreadLocal提供了一種將數據與線程關聯的機制。通過使用ThreadLocal,可以為每個線程創建獨立的變量副本,使得每個線程都可以獨立地訪問和修改自己的變量副本,而不會干擾其他線程的數據。這樣可以實現線程間的數據隔離,避免了線程安全問題。

  2. 狀態傳遞:ThreadLocal可以用于在同一個線程的多個方法之間傳遞狀態信息,而無需在方法參數中顯式傳遞。通過將狀態信息存儲在ThreadLocal變量中,不同的方法可以通過ThreadLocal訪問和修改共享的狀態,而無需顯式傳遞參數。這樣可以簡化方法的調用,提高代碼的可讀性和可維護性。

  3. 線程上下文管理:有些情況下,需要在整個線程執行期間共享某些上下文信息,比如用戶認證信息、數據庫連接等。通過將這些信息存儲在ThreadLocal中,可以在同一線程的任何地方方便地訪問和使用這些信息,而無需顯式傳遞或在每個方法中重復獲取。

  4. 避免鎖競爭:在某些情況下,使用ThreadLocal可以避免使用鎖來同步對共享變量的訪問。由于每個線程都有自己的變量副本,線程之間不會產生競爭條件,從而避免了鎖競爭和同步開銷,提高了程序的性能。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/41069.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/41069.shtml
英文地址,請注明出處:http://en.pswp.cn/news/41069.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Android設備通過藍牙HID技術模擬鍵盤實現

目錄 一&#xff0c;背景介紹 二&#xff0c;技術方案 2.1 獲取BluetoothHidDevice實例 2.2 注冊/解除注冊HID實例 2.3 Hid report description描述符生成工具 2.4 鍵盤映射表 2.5 通過HID發送鍵盤事件 三&#xff0c;實例 一&#xff0c;背景介紹 日常生活中&#xff0…

AndroidStudio中修改打包生成的apk名稱

1.配置手機架構 splits {abi {enable truereset()include armeabi-v7a,arm64-v8auniversalApk false} } 2.多渠道 productFlavors {normal {applicationId "*****"manifestPlaceholders [appName: "string/app_name_normal"]}driver {applicationId &qu…

圖片轉換成pdf格式?這幾種轉換格式方法了解一下

圖片轉換成pdf格式&#xff1f;將圖片轉換成PDF格式的好處有很多。首先&#xff0c;PDF格式具有通用性&#xff0c;可以在幾乎任何設備上查看。其次&#xff0c;PDF格式可以更好地保護文件&#xff0c;防止被篡改或者復制。此外&#xff0c;PDF格式還可以更好地壓縮文件大小&am…

使用Kaptcha生成驗證碼

說明&#xff1a;驗證碼&#xff0c;是登錄流程中必不可少的一環&#xff0c;一般企業級的系統&#xff0c;使用都是專門制作驗證碼、審核校驗的第三方SDK&#xff08;如極驗&#xff09;。本文介紹&#xff0c;使用谷歌提供的Kaptcha技術&#xff0c;制作一個簡單的驗證碼。 …

sqlserver數據庫導出到mysql

愛到分才顯珍貴&#xff0c;很多人都不懂珍惜擁有&#xff0c;只到失去才看到&#xff0c;其實那最熟悉的才最珍貴的。 這里只介紹一種方式&#xff0c;有很多的方式。 1.使用Navicat 安裝 下載 2.工具 數據傳輸 3.選擇源和目標 然后開始 4.最好導入前備份一下庫

【KVM虛擬化環境部署】

環境部署 KVM虛擬化環境 1、裝系統時手動選擇安裝 2、CentOS 7 最小化安裝 yum install qemu-kvm qemu-img libvirt -y yum install virt-install libvirt-python virt-manager python-virtinst libvirt-client -y安裝好CentOS 7后&#xff0c;去設置里面點擊處理器&#x…

4.0 Spring Boot入門

1. Spring Boot概述 Spring Boot介紹 Spring Boot是Pivotal團隊在2014年推出的全新框架&#xff0c;主要用于簡化Spring項目的開發過程&#xff0c;可以使用最少的配置快速創建Spring項目。 Spring Boot版本 2014年4月v1.0.0.RELEASE發布。 ? 2.Spring Boot特性 約定優于配…

docker-compose部署可道云

文章目錄 一. Mac1.1 下載源碼1.2 部署1.2.1 修改密碼部署(可忽略)1.2.2 直接部署 1.3 卸載1.4 訪問 二. Win2.1 下載源碼2.2 部署2.2.1 修改密碼部署(可忽略)2.2.2 直接部署 2.3 卸載 一. Mac 1.1 下載源碼 mkdir -p /Users/wanfei/docker-compose && cd /Users/wan…

mysql 數據備份和恢復

操作系統&#xff1a;22.04.1-Ubuntu mysql 版本&#xff1a;8.033 binlog 介紹 binlog 是mysql 二進制日志 binary log的簡稱&#xff0c;可以簡單理解為數據的修改記錄。 需要開啟binlog,才會產生文件&#xff0c;mysql 8.0 默認開啟,開啟后可以在 /var/lib/mysql &#xff…

技術債 筆記

目錄 1. 技術債 筆記1.1. 什么是技術債1.2. 討論1.3. 國內技術從業者怎么看? 1. 技術債 筆記 1.1. 什么是技術債 1992 年, Ward Cunningham 在敏捷宣言中首次提出了"技術債"概念, 主要指有意或無意地做了錯誤的或不理想的技術決策所累積的債務。隨后, 《重構》一書…

sql中union all、union、intersect、minus的區別圖解,測試

相關文章 sql 的 join、left join、full join的區別圖解總結&#xff0c;測試&#xff0c;注意事項 1. 結論示意圖 對于intersect、minus&#xff0c;oracle支持&#xff0c;mysql不支持&#xff0c;可以變通&#xff08;in或exists&#xff09;實現 2.測試 2.1.創建表和數…

vue pc端項目el-upload上傳圖片時加水印

html代碼&#xff1a; <a-uploadclass"avatar-uploader"list-type"picture-card":file-list"uploadFileList":custom-request"uploadDoneHandle":before-upload"beforeUpload":remove"removeHandle"v-decorat…

案例21 基于Spring Boot+Redis實現圖書信息按書號存儲案例

1. 案例需求 基于Spring BootRedis實現圖書信息按書號存儲和取出功能&#xff0c;數據存儲至Redis。 2. 創建Spring Boot項目 創建Spring Boot項目&#xff0c;項目名稱為springboot-redis02。 3. 選擇依賴 ? pom.xml文件內容如下所示&#xff1a; <?xml version&quo…

瀏覽器控制臺調試代碼和JavaScript控制臺方法介紹

瀏覽器控制臺調試代碼和JavaScript控制臺方法介紹 瀏覽器控制臺調試代碼 瀏覽器控制臺&#xff08;Console&#xff09;是瀏覽器提供的一個開發工具&#xff0c;用于在瀏覽器中執行和調試 JavaScript 代碼。它提供了一個交互式環境&#xff0c;可以輸入 JavaScript 代碼&#…

Qt:隱式內存共享

隱式內存共享 Many C classes in Qt use implicit data sharing to maximize resource usage and minimize copying. Implicitly shared classes are both safe and efficient when passed as arguments, because only a pointer to the data is passed around, and the data i…

C語言:每日一練(選擇+編程)

目錄 選擇題&#xff1a; 題一&#xff1a; 題二&#xff1a; 題三&#xff1a; 題四&#xff1a; 題五&#xff1a; 編程題&#xff1a; 題一&#xff1a;打印1到最大的n位數 示例1 思路一&#xff1a; 題二&#xff1a;計算日期到天數轉換 示例1 思路一&#xf…

【JVM】如何判定一個對象已死以及“標記-清除”、“標記-復制”、“標記-整理”三種垃圾收集算法

文章目錄 0、如何判定一個對象的生死&#xff1f;1、上文提到的引用又是什么1、強引用&#xff1a;2、軟引用&#xff1a;3、弱引用&#xff1a;4、虛引用&#xff1a; 2、垃圾收集算法1、標記-清除2、標記-復制優化&#xff1a;&#x1f447; 3、標記-整理 0、如何判定一個對象…

Java面向對象程序設計——知識、概念、定義及作用(簡答)

?專欄&#xff1a;《Java面向對象程序設計》學習筆記 問題是依據考綱整理的&#xff0c;稍微做了一些補充。大部分答案由GPT生成&#xff0c;部分內容摘選自書本。 內容太多了&#xff0c;目前懶得濃縮精煉了&#xff0c;以后再說吧。 如果有大佬可以幫忙精簡一些文字、補充…

R語言實現神經網絡(1)

#R語言實現神經網絡 library(neuralnet) library(caret) library(MASS) library(vcd) data(shuttle) str(shuttle)#因變量use; table1<-structable(windmagn~use,shuttle) mosaic(table1,shadingT) mosaic(use~errorvis,shuttle) prop.table(table(shuttle$use,shuttle$stab…

計算機網絡-物理層(二)- 傳輸方式

計算機網絡-物理層&#xff08;二&#xff09;- 傳輸方式 串型傳輸與并行傳輸 串行傳輸:是指數據是一個比特一個比特依次發送的&#xff0c;因此在發送端和接收端之間&#xff0c;只需要一條數據傳輸線路即可 并行傳輸:是指一次發送n個比特而不是一個比特&#xff0c;因此發送…