前言
共享變量一直是并發(fā)中比較讓人頭疼的問題,
每個線程都對它有操作權(quán), 所以線程之間的同步就顯得很關(guān)鍵.
前幾章說了很多, 大部分解決之道都和“鎖”相關(guān)!
總兒言之就是對于“共享變量”進行“鎖”控制, 確保某一時刻只有一個線程拿著這個變量, 來解決共享變量的爭用問題.
可是大家也知道, 加“鎖”這個還是比較消耗性能的, 有沒有更好的方式呢?
于是, 大佬們開始思考了:
其實沒必要對所有場景進行加鎖操作, 對某些場景采用生成“副本”的方式來解決顯得更加合適且高效!
經(jīng)過討論 , 總結(jié), 大佬們將共享變量分為以下兩種使用場景
需要線程間同步
無需線程間同步
為了說明這兩種場景, 咱舉個不太恰當(dāng)?shù)呛美斫獾睦?
假設(shè)10個人(線程)在包子鋪需要不多不少共吃完100個包子(共享變量), 你如何安排?
方式一(需要線程間同步)
10個人, 同時開吃, 每個人吃之前還得與其他人進行溝通, 及時了解還剩多少個包子可以吃(防止多吃或少吃)方式二(無需線程間同步)
10個人, 開吃之前明確的被告知只能吃10個, 此時, 只需知道自己的10個有沒有吃完, 無需關(guān)心其他人吃了多少.
方式一就是我們平時加鎖的方式, 而方式二就是我們今天要學(xué)習(xí)ThreadLocal!
ThreadLocal介紹
通過以上的吃包子案例不難理解ThreadLocal的核心理念: 共享變量私有化.
每個線程都擁有一份共享變量的本地副本, 每個線程對應(yīng)一個副本, 同時對共享變量的操作也改為對屬于自己的副本的操作, 這樣每個線程處理自己的本地變量, 形成數(shù)據(jù)隔離.
ps: 每個人只關(guān)心自己有沒有吃完10個包子, 而不關(guān)心其他人吃了多少個!
ThreadLocal和Synchronized都是為了解決多線程中共享變量的訪問沖突問題
不同的是:
Synchronized通過線程等待, 犧牲時間來解決訪問沖突
ThreadLocal通過每個線程單獨一份存儲空間來存儲共享變量的副本, 犧牲空間來解決沖突
相比于Synchronized, ThreadLocal具有線程隔離的效果:
只有在線程內(nèi)才能獲取到對應(yīng)的值, 線程外則不能訪問到想要的值.
ThreadLocal最適合的是共享變量在線程間隔離, 而在本線程內(nèi)方法或類間共享的場景.
ThreadLocal使用
為了說明ThreadLocal的作用, 我們舉個例子
public class UserLocal {
private static ThreadLocal<String> local = new ThreadLocal<String>() {
//初始化個值
@Override
protected String initialValue() {
return "init";
}
};
//private static String threadName;
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
public void run() {
//threadlocal設(shè)置個線程隔離的值
local.set("線程值:" + Thread.currentThread().getName());
//普通共享變量
//threadName = Thread.currentThread().getName();
//睡眠一下,讓其它線程可以讀取本線程的local值
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " " + local.get());
//System.out.println(Thread.currentThread().getName() + " " + threadName);
}
}).start();
}
}
}
很簡單的案例, 聲明一個static ThreadLocal變量 local, 用于存放線程名稱.
為了說明ThreadLocal的線程隔離特性, 所以Thread.sleep(1)下, (理論上)讓其它線程可以讀取到當(dāng)前的local值.
按常規(guī)來說, ThreadLocal定義成了全局變量, 值應(yīng)該是共享的, 任何一個線程更改之后,其他線程應(yīng)該也會更改.
但從結(jié)果上來看, 線程間互不影響, 這就是ThreadLocal的線程隔離特性.
Thread-0 線程值:Thread-0
Thread-1 線程值:Thread-1
Thread-2 線程值:Thread-2
Thread-3 線程值:Thread-3
Thread-4 線程值:Thread-4
Thread-5 線程值:Thread-5
Thread-7 線程值:Thread-7
Thread-8 線程值:Thread-8
Thread-9 線程值:Thread-9
Thread-6 線程值:Thread-6
注: 案例中也寫了普通的共享變量用來對比, 大家自行嘗試!
ThreadLocal原理
直接打開ThreadLocal類文件, 查看代碼, 不難找到以下幾個函數(shù)
-
set()
set() -
get()
get() -
getMap()
getMap() -
createMap()
createMap() -
setInitialValue()
setInitialValue() -
remove()
remove()
需要注意的就是getMap()這個方法, 它接受一個Thread對象作為參數(shù), 然后直接返回了這個Thread對象中的threadLocals成員變量.
ThreadLocal的set()吃靠、get()谈截、remove()等方法都是基于Thread對象中的threadLocals成員變量來進行工作的.
打開Thread類文件,發(fā)現(xiàn), Thread中的確定義了這么個成員變量,并且類型為ThreadLocal.ThreadLocalMap.
不妨再看看ThreadLocalMap類, IDEA直接command單擊查看類文件, 居然直接跳到了ThreadLocal類文件
原來, ThreadLocalMap是ThreadLocal的一個靜態(tài)內(nèi)部類!
不難看出, ThreadLocalMap就是個特殊點的Map結(jié)構(gòu), 也就是K-V結(jié)構(gòu), 只不過, 這個Key特殊點, 居然是個ThreadLocal類型的弱引用.
看到此處, 我們理清了Thread告喊、ThreadLocal、ThreadLocalMap的關(guān)系:
ThreadLocal雖然提供了set()、get()等存取方法, 但是它并不直接存儲數(shù)據(jù), 它只是作為一個Key來關(guān)聯(lián)Value, 并且這個Key是個弱引用
ThreadLocalMap是ThreadLocal的靜態(tài)內(nèi)部類, Thread實例會初始化ThreadLocalMap(也就是threadLocals成員變量), 并用它來真正保存K-V數(shù)據(jù)
為了加深理解, 我們再舉個例子
public class UserLocal {
private static ThreadLocal<String> local1 = new ThreadLocal<String>();
private static ThreadLocal<String> local2 = new ThreadLocal<String>();
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
public void run() {
local1.set("local1:" + Thread.currentThread().getName());
local2.set("local2:" + Thread.currentThread().getName());
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " " + local1.get()+ " " +local2.get());
}
}).start();
}
}
}
- 首先實例了兩個ThreadLocal對象local1,local2;
- 線程實例執(zhí)行到local1.set()時, 會先獲取當(dāng)前線程實例的threadLocals(ThreadLocalMap類型)
- 此時當(dāng)前線程實例的threadLocals為null, 則初始化線程實例的threadLocals
- 將當(dāng)前ThreadLocal實例local1的弱引用作為key, "local1:" + Thread.currentThread().getName()作為value, 放入threadLocals中
- 線程實例執(zhí)行到local2.set()時, 會先獲取當(dāng)前線程實例的threadLocals
- 此時當(dāng)前線程實例的threadLocals不為null, 則直接返回
- 將當(dāng)前ThreadLocal實例local2的弱引用作為key, "local2:" + Thread.currentThread().getName()作為value, 放入threadLocals中
- ……
看到這, 想必一切都很清楚了.
不過可能有朋友和小白的咸魚君一樣, 對文章中提到的“弱引用”產(chǎn)生了疑惑.
也就是這段代碼
關(guān)于“弱引用”, 其實是Java中“引用類型”中的一種, 要是你和咸魚一樣好奇&小白, 不妨繼續(xù)往下看.
Java中的引用
8種基本類型
我們知道, Java中有8種基本類型
除了這8種基本類型外, 還有個引用類型的概念.
引用類型
那什么叫引用類型呢?
簡單來說
所有的非基本數(shù)據(jù)類型都是引用數(shù)據(jù)類型.
具體點就是:
- 類
- 接口類型
- 數(shù)組類型
- 枚舉類型
- 注解類型
- 字符串型
- 其它……
基本類型和引用類型的區(qū)別
區(qū)別其實很簡單: 基本數(shù)據(jù)類型是分配在棧上的, 而引用類型是分配在堆上的(需要java中的棧、堆概念)
舉個例子:
int a = 1;
String str = new String("abc");
我們創(chuàng)建了兩個變量, 他們都會先在棧中分配一塊內(nèi)存;
對于基本類型a來說, 這塊區(qū)域包含的是基本類型的內(nèi)容, 也就是1;
對于引用類型str來說坝冕,這塊區(qū)域包含的是指向真正內(nèi)容的指針(地址),真正的內(nèi)容“abc”被保存在堆內(nèi)存上.
這個差別也體現(xiàn)在我們常用的比對上, 比如“==”和“equals”的區(qū)別, 這里就不過多介紹了.
引用類型的類別
在 JDK.1.2 之后, Java 對引用的概念進行了擴充, 將引用按強度分為了以下4種
- 強引用
- 軟引用
- 弱引用
- 虛引用
引用就引用, 為什么還要區(qū)分出4種類型??
Java中區(qū)分這4種引用類型主要有兩個目的 :
- 可以讓程序員通過代碼的方式?jīng)Q定某些對象的生命周期
- 有利于JVM進行垃圾回收坐梯。
那什么是強引用徽诲、軟引用、弱引用以及虛引用呢?
我們舉例說明
強引用(Java中默認聲明的就是強引用)
把一個對象賦給一個引用變量,這個引用變量就是一個強引用;
當(dāng)一個對象被強引用變量引用時吵血,它處于可達狀態(tài)谎替,它是不可能被垃圾回收機制回收的,即使該對象以后永遠都不會被用到JVM也不會回收蹋辅。因此強引用是造成Java內(nèi)存泄漏的主要原因之一钱贯。只要強引用存在, 垃圾回收器將永遠不會回收被引用的對象;
哪怕內(nèi)存不足時, JVM也會直接拋出OutOfMemoryError, 不會去回收;
如果想中斷強引用與對象之間的聯(lián)系, 可以顯示的將強引用賦值為null, 這樣一來,JVM就可以適時的回收對象了
//new Object()實例化一個對象, 并將引用obj1指向這個對象
Object obj1 = new Object();
//obj2 將和 obj1 指向同一個對象
Object obj2= obj1;
//obj1 斷開了引用, 但是obj1指向的對象不會成為垃圾空間被gc回收, 因為 obj2還引用著這個對象
obj1 = null;
System.gc();
System.out.println("obj1:"+obj1);
System.out.println("obj2:"+obj2);
軟引用
軟引用是用來描述一些非必需但仍有用的對象.
內(nèi)存足夠時, 軟引用對象不會被回收;
只有在內(nèi)存不足時,系統(tǒng)則會回收軟引用對象;
如果回收了軟引用對象之后仍然沒有足夠的內(nèi)存, 才會拋出內(nèi)存溢出異常.
這種特性常常被用來實現(xiàn)緩存技術(shù),比如網(wǎng)頁緩存、圖片緩存等.
Object obj = new Object();
SoftReference softRef = new SoftReference(obj);
obj = null;
System.gc();
System.out.println("obj:"+obj);
System.out.println("softRef:"+softRef.get());
弱引用
弱引用的引用強度比軟引用要更弱一些;
無論內(nèi)存是否足夠, 只要 JVM 開始進行GC垃圾回收,那些被弱引用關(guān)聯(lián)的對象都會被回收
Object obj = new Object();
WeakReference weakRef = new WeakReference(obj);
obj = null;
System.gc();
System.out.println("obj:"+obj);
System.out.println("weakRef:"+weakRef.get());
虛引用
虛引用是最弱的一種引用關(guān)系;
虛引用與其它幾種引用都不同, 虛引用并不會決定對象的生命周期.
如果一個對象僅持有虛引用, 那么它就和沒有任何引用一樣,它隨時可能會被回收.
虛引用主要用來跟蹤對象被垃圾回收器回收的活動, 必須和引用隊列(ReferenceQueue)結(jié)合使用.
垃圾回收器回收對象時,該對象還有虛引用,就會在回收對象的內(nèi)存之前,把這個虛引用加入到與之關(guān)聯(lián)的引用隊列中(程序可以通過判斷引用隊列中是否已經(jīng)加入了引用,來判斷被引用的對象是否將要被垃圾回收,這樣就可以在對象被回收之前采取一些必要的措施.)
ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<Object> phantomReference = new PhantomReference<>(new Object(), queue);
System.out.println("phantomReference:"+phantomReference.get());
一張圖的說明ThreadLocal
先來張圖總結(jié)下上面對ThreadLocal的介紹
Thread運行時,線程的的一些局部變量和引用使用的內(nèi)存屬于Stack(棧)區(qū)侦另,而普通的對象是存儲在Heap(堆)區(qū).
線程運行時,我們定義的TheadLocal對象被初始化,存儲在Heap,同時線程運行的棧區(qū)保存了指向該實例的引用,也就是圖中的ThreadLocalRef
當(dāng)ThreadLocal的set/get被調(diào)用時,虛擬機會根據(jù)當(dāng)前線程的引用也就是CurrentThreadRef找到其對應(yīng)在堆區(qū)的實例,然后查看其對用的TheadLocalMap實例是否被創(chuàng)建,如果沒有,則創(chuàng)建并初始化
Map實例化之后,也就拿到了該ThreadLocalMap的句柄,然后如果將當(dāng)前ThreadLocal對象作為key,進行存取操作
圖中的虛線,表示key對ThreadLocal實例的引用是個弱引用
老生常談的“內(nèi)存泄漏”問題
ThreadLocalMap是以弱引用的方式引用著ThreadLocal, 換句話說,就是ThreadLocal是被ThreadLocalMap以弱引用的方式關(guān)聯(lián)著.
如果ThreadLocal沒有被ThreadLocalMap以外的對象引用, 那么依據(jù)弱引用的特性, 在下一次GC的時候, ThreadLocal實例就會被回收.
此時ThreadLocalMap里的一組 K-V 的 K 就是null了, 那么此處的V便不會被外部訪問到.
并且只要Thread實例一直存在, Thread實例就強引用著ThreadLocalMap, 因此ThreadLocalMap就不會被回收, 那么這里K為null的V就一直占用著內(nèi)存.
TheadLocal本身的優(yōu)化
針對上面的弱引用引起的內(nèi)存泄漏問題, TheadLocal本身做了如下優(yōu)化
可以看到, 調(diào)用set(), Key為null時, 會執(zhí)行replaceStaleEntry(key, value, i)方法, 用當(dāng)前的值替換掉以前的Key為null的值, 重復(fù)利用了空間.
另外ThreadLocalMap的set()秩命、get()尉共、remove()方法還有有以下邏輯(這里以remove()為例):
當(dāng)Key為null時, 在下一次ThreadLocalMap調(diào)用remove()方法的時候會被清除value值.
為什么使用弱引用
既然ThreadLocalMap K-V 的 K 使用弱應(yīng)用關(guān)聯(lián)ThreadLocal會有內(nèi)存泄漏問題, 那為什么不使用強引用呢??
我們不如換個問法:
key 使用強引用會怎么樣?
若ThreadLocalMap的Key為強引用, 那么回收ThreadLocal時, 因為ThreadLocalMap還持有ThreadLocal的強引用, 如果沒有手動刪除, ThreadLocal不會被回收, 這會導(dǎo)致Entry內(nèi)存泄漏.
key 使用弱引用會怎么樣?
若ThreadLocalMap的key為弱引用, 那么回收ThreadLocal時,由于ThreadLocalMap持有ThreadLocal的弱引用, 即使沒有手動刪除, ThreadLocal也會被回收;
當(dāng)Key為null, 在下一次ThreadLocalMap調(diào)用set(), get(), remove()方法的時候會被清除value值.
總結(jié)
Thread包含ThreadLocalMap, 因此ThreadLocalMap與Thread的生命周期是一樣的.
如果沒有手動刪除對應(yīng)Key, 都會導(dǎo)致內(nèi)存泄漏.
不過, 使用弱引用可以多一層保障:
弱引用ThreadLocal不會使得Entry內(nèi)存泄漏; Key 為 null 時, 對應(yīng)的value在下一次ThreadLocalMap調(diào)用set(),get(),remove()的時候會被清除.
因此, ThreadLocal內(nèi)存泄漏的根源是:
ThreadLocalMap的生命周期跟Thread一樣長, 如果沒有手動刪除對應(yīng)Key就會導(dǎo)致內(nèi)存泄漏; 而不是因為弱引用.
ThreadLocal正確的使用方法
每次使用完ThreadLocal都調(diào)用它的remove()方法清除數(shù)據(jù)
將ThreadLocal變量定義成private static, 這樣就一直存在ThreadLocal的強引用, 也就能保證任何時候都能通過ThreadLocal的弱引用訪問到Entry的value值, 進而清除掉.
小結(jié)下
原本以為ThreadLocal這么簡單, 簡簡單單就寫完這篇了.
沒想到, 深扒一下源碼, 洋洋灑灑寫了近4-5千字, 希望能幫到大家的學(xué)習(xí)!
歡迎關(guān)注我
技術(shù)公眾號 “CTO技術(shù)”