寫在前面
多線程訪問共享變量的時(shí)候卒茬,很容易出現(xiàn)并發(fā)問題船老。特別是多個(gè)線程對共享變量進(jìn)行寫入的時(shí)候,由于原子性的問題圃酵,很容易導(dǎo)致最后數(shù)據(jù)的錯(cuò)誤努隙。一般來講,我們可以進(jìn)行同步辜昵,同步的方式就是加鎖。另一方面咽斧,jdk也提供了變量的線程隔離方式——ThreadLocal堪置,盡管它的出現(xiàn)并不是為了解決上述的問題。
共享變量
何為共享變量张惹?說到這個(gè)舀锨,我們想一想什么變量不是共享的。在一個(gè)線程調(diào)用一個(gè)方法的時(shí)候宛逗,會(huì)在棧內(nèi)存上為局部變量和方法參數(shù)申請內(nèi)存坎匿,在方法調(diào)用結(jié)束的時(shí)候,這些內(nèi)存會(huì)被釋放雷激。不同的線程調(diào)用同一個(gè)方法都會(huì)為局部變量和方法參數(shù)copy一個(gè)副本替蔬,所以棧內(nèi)存是私有的,也就是說局部變量和方法參數(shù)不是線程共享的
屎暇。而堆上的數(shù)組和對象是共享的承桥,堆內(nèi)存是所有線程可以訪問的,也就是說成員變量根悼,靜態(tài)變量和數(shù)組元素是可以共享的
凶异。
原子性
即不可中斷的一個(gè)或一系列操作蜀撑,也就是說一個(gè)操作或者多個(gè)操作,要么全部執(zhí)行并且執(zhí)行的過程不會(huì)被任何因素打斷剩彬,要么就都不執(zhí)行酷麦。在線程級(jí)別,我們可以這樣說一個(gè)或幾個(gè)操作只能在一個(gè)線程執(zhí)行完之后喉恋,另一個(gè)線程才能開始執(zhí)行該操作沃饶,也就是說這些操作是不可分割的,線程不能在這些操作上交替執(zhí)行
瀑晒。
舉個(gè)栗子:i++
绍坝,這個(gè)操作的語義可以拆分成三步:
- 讀取變量i的值
- 將變量i值加1
- 將計(jì)算結(jié)果寫入變量i
由于線程是基于處理器分配的時(shí)間片執(zhí)行的,這三個(gè)步驟可能讓多個(gè)線程交叉執(zhí)行苔悦。我們假設(shè)i
的初始值為0
轩褐,如果兩個(gè)線程按照如下順序交替執(zhí)行的話:
我們看到,經(jīng)過了兩次i++
的操作玖详,變量i
最后的值是1
把介,并不是想象中的2
。這就是因?yàn)?code>i++并不是原子性操作所帶來的并發(fā)問題蟋座。
解決方案
從共享性解決
使用局部變量
方法中的方法參數(shù)和局部變量是線程私有的拗踢,自然不會(huì)存在并發(fā)問題。
使用ThreadLocal
ThreadLocal示例
ThreadLocal
作為變量的線程隔離的類向臀,訪問此變量的每個(gè)線程都會(huì)copy一個(gè)此變量的副本巢墅。多個(gè)線程操作這個(gè)變量,實(shí)際上是操作的自己本地內(nèi)存的變量券膀,這樣就避免了多線程操作變量的安全問題君纫。
我們先來看一下ThreadLocal
的簡單使用:
public class ThreadLocalDemo {
static ThreadLocal<String> tl = new ThreadLocal<>();
public static void main(String[] args) {
Thread thread1 = new Thread(()->{
tl.set("thread 1 locals");
System.out.println("thread 1 :"+ tl.get());
});
Thread thread2 = new Thread(()->{
tl.set("thread 2 locals");
System.out.println("thread 2 :"+ tl.get());
});
thread1.start();
thread2.start();
}
}
運(yùn)行結(jié)果:
其實(shí)無論運(yùn)行多少次,無論幾個(gè)線程一起跑芹彬,最后打印出來的都是各個(gè)線程自己維護(hù)在內(nèi)存里的本地變量蓄髓,而不會(huì)出現(xiàn)線程1設(shè)置的變量被線程2修改這種情況。
ThreadLocal源碼解讀
結(jié)構(gòu)
由ThreadLocal和Thread的類結(jié)構(gòu)可知舒帮,Thread里面有兩個(gè)成員變量threadLocals
和threadLocals
会喝,他們都是ThreadLocalMap
類型的,而ThreadLocalMap
是ThreadLocal
的一個(gè)靜態(tài)內(nèi)部類玩郊,這是一個(gè)定制化的HashMap肢执。默認(rèn)每個(gè)線程一開始的時(shí)候,這兩個(gè)變量都是null瓦宜。
類結(jié)構(gòu)圖如下:
Set方法
ThreadLocal#set:
public void set(T value) {
Thread t = Thread.currentThread(); //當(dāng)前線程
ThreadLocalMap map = getMap(t); //獲取當(dāng)前線程的threadlocals
if (map != null)
map.set(this, value); //(k,v)存入ThreadLocalMap
else
createMap(t, value); //初始化當(dāng)前線程的threadlocals蔚万,創(chuàng)建ThreadLocalMap
}
代碼的字面意思:如果當(dāng)前線程的threadLocals變量不為null,則將當(dāng)前的ThreadLocal實(shí)例為key临庇,傳入的value值為value反璃,放入ThreadLocalMap對象里面昵慌;如果當(dāng)前線程的threadLocals為null,則初始化當(dāng)前線程的threadLocals變量淮蜈。
ThreadLocalMap getMap(Thread t) {
return t.threadLocals; //返回當(dāng)前線程的threadLocals
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue); //初始化當(dāng)前線程的threadLocals斋攀,k為當(dāng)前的ThreadLocal對象,v為設(shè)置的值梧田。
}
看到這里淳蔼,說一下為什么為什么會(huì)是個(gè)map。那是因?yàn)橐粋€(gè)線程可以綁定多個(gè)ThreadLocal實(shí)例裁眯。例如:
static ThreadLocal<String> tl1 = new ThreadLocal<>();
static ThreadLocal<Integer> tl2 = new ThreadLocal<>();
Get方法
public T get() {
Thread t = Thread.currentThread(); //當(dāng)前線程
ThreadLocalMap map = getMap(t); //當(dāng)前線程的threadlocals變量
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this); //取出Entry
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
private T setInitialValue() {
T value = initialValue(); //初始化為null
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t); //獲取當(dāng)前線程的thredLocals是變量
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
這段代碼的意思是:我先判斷當(dāng)前線程的本地變量threadLocals
變量是否為null鹉梨,不為null則以當(dāng)前的ThreadLocal
實(shí)例為key從map中取出Entry,能取出Entry則返回對應(yīng)的value值穿稳;若當(dāng)前線程的本地變量threadLocals
變量為null存皂,則初始化threadLocals
變量,初始化的工作和set
差不多逢艘,只不過set
設(shè)置的值為傳入的參數(shù)旦袋,初始化設(shè)置的value是null(在當(dāng)前線程的threadLocals變量不為null的時(shí)候)。
Remove 方法
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
就是如果當(dāng)前線程的 本地變量threadLocals
不為null它改,則刪除當(dāng)前線程中指定 ThreadLocal 實(shí)例的本地變量疤孕。
總結(jié)一下:每個(gè)線程都有一個(gè)成員變量叫threadLocals,它是ThreadLocalMap類型的。其中的key為每一個(gè)ThreadLocal的實(shí)例央拖,value為傳入的參數(shù)值祭阀。
注意:
- 由于
ThreadLocalMap
的key為WeakReference
,在外部沒有強(qiáng)引用時(shí)鲜戒,發(fā)生GC會(huì)回收時(shí)柬讨,如果創(chuàng)建ThreadLocal
一直運(yùn)行,將會(huì)導(dǎo)致這個(gè)key對應(yīng)的value將會(huì)一直在內(nèi)存中得不到回收袍啡,發(fā)生內(nèi)存泄露。所以在用完ThreadLocal的時(shí)候要注意手動(dòng)remove却桶。 - 其實(shí)
ThreadLocal
會(huì)有個(gè)問題境输,那就是子線程通過獲取不了父線程中的ThreadLocal
變量,這個(gè)其實(shí)java已經(jīng)給出了解決方案了颖系,就是Thread的另一個(gè)ThreadLocalMap類型的變量inheritableThreadLocals
,我們通過這個(gè)變量嗅剖,從get方法中能獲取到本線程和父線程的ThreadLocal變量。
同步方法解決
話說回來嘁扼,剛剛我們從共享性角度解決并發(fā)編程的原子性問題信粮,提出了ThreadLocal,也就是每個(gè)線程獨(dú)占的趁啸,自然不會(huì)有并發(fā)問題强缘。下面從另一個(gè)角度來說督惰,也就是我們都知道的方式:加鎖。
鎖的概念
《并發(fā)編程的藝術(shù)》里是這么定義鎖的:"鎖是用來控制多個(gè)線程訪問共享資源的方式旅掂,一般來說赏胚,一個(gè)鎖能防止多個(gè)線程同時(shí)訪問共享資源(但是有些鎖可以允許多個(gè)線程并發(fā)的訪問共享資源,比如讀寫鎖)"商虐。
提到鎖觉阅,一大堆名詞就冒出來了:內(nèi)置鎖,顯示鎖秘车,可重入鎖典勇,讀寫鎖,以及祖師爺抽象隊(duì)列同步器(AQS)……
最大名鼎鼎的要屬于synchronized的了叮趴。
synchronized關(guān)鍵字
synchronized同步關(guān)鍵字割笙,可以修飾方法,使之成為同步方法疫向】任担可以修飾this或Class對象,使之成為同步代碼塊搔驼。
public 返回類型 方法名(參數(shù)列表) {
synchronized (鎖對象) {
需要保持原子性的一系列代碼
}
}
public synchronized 返回類型 方法名(參數(shù)列表) {
需要被同步執(zhí)行的代碼
}
public synchronized static 返回類型 方法名(參數(shù)列表) {
需要被同步執(zhí)行的代碼
}
例如這個(gè)demo:
public class SynchronizedDemo {
private Object lock = new Object();
public synchronized void m1(){
}
public void m2(){
synchronized (lock) {
}
}
}
通過javap反編譯出來:
可以看到谈火,同步代碼塊的的synchronized是用monitorenter
和moniteorexit
實(shí)現(xiàn)的,同步方法看不出來(其實(shí)是jvm底層的的ACC_SYNCHRONIZED
實(shí)現(xiàn)的)舌涨。
monitorenter指令對應(yīng)于同步代碼塊的開始位置糯耍,監(jiān)視器在這個(gè)位置進(jìn)入,獲取鎖囊嘉;moniteorexit指令對應(yīng)于同步代碼塊的結(jié)束位置温技,監(jiān)視器在這個(gè)位置退出,釋放鎖扭粱。
JVM需要保證每一個(gè)monitorenter都有一個(gè)monitorexit與之對應(yīng)舵鳞,任何對象都有個(gè)monitor與之關(guān)聯(lián)。一旦monitor被持有琢蛤,這個(gè)對象將被鎖定蜓堕。
Java對象頭
synchronized用的鎖是存在java對象頭里的,對象頭一般占有2字寬(1字寬為4字節(jié)博其,即32bit)套才,但是如果對象是數(shù)組類型,則需要3字寬慕淡。對象頭里的Mark Word默認(rèn)存儲(chǔ)對象的HashCode背伴,分代年齡和鎖標(biāo)記位。對象頭的存儲(chǔ)結(jié)構(gòu)如下:
(此圖來源于互聯(lián)網(wǎng),侵刪)
鎖的升級(jí)與優(yōu)化
從jdk1.6開始傻寂,對鎖進(jìn)行了進(jìn)一步的升級(jí)和優(yōu)化息尺。鎖一共有4種狀態(tài),無鎖崎逃,偏向鎖掷倔,輕量級(jí)鎖,重量級(jí)鎖个绍,這幾種狀態(tài)會(huì)隨著競爭情況逐漸升級(jí)勒葱。鎖可以升級(jí)但不能降級(jí)。
偏向鎖
偏向鎖的引入背景:只在單線程訪問同步塊的場景巴柿。
當(dāng)鎖不存在多線程競爭的時(shí)候凛虽,為了讓線程獲得鎖的代價(jià)更低引入了偏向鎖。當(dāng)一個(gè)線程訪問同步快并獲取鎖時(shí)广恢,會(huì)在對象頭Mark Word上記錄偏向鎖狀態(tài)位1凯旋,此時(shí)的鎖標(biāo)識(shí)位是01。
當(dāng)一個(gè)線程獲取鎖的時(shí)候钉迷,會(huì)先檢查Mark Word上的可偏狀態(tài)至非,如果是1,則繼續(xù)檢查對象頭的線程Id糠聪。如果線程Id不是當(dāng)前線程荒椭,則通過CAS競爭獲取鎖,競爭成功將線程Id替換舰蟆,如果CAS競爭鎖失敗趣惠,證明存在多線程情況,此時(shí)偏向鎖被掛起身害,升級(jí)升輕量級(jí)鎖味悄。如果線程是當(dāng)先線程,則執(zhí)行同步代碼塊塌鸯。
偏向鎖的釋放使用了一種等到競爭出現(xiàn)才釋放鎖的機(jī)制侍瑟,所以當(dāng)其他線程去競爭鎖時(shí),持有偏向鎖的線程才會(huì)釋放鎖丙猬。此時(shí)將恢復(fù)到無鎖狀態(tài)或偏向于其他線程丢习。
輕量級(jí)鎖
輕量級(jí)鎖的引入背景:沒有多線程競爭的前提下,減少重量級(jí)鎖的互斥產(chǎn)生的性能消耗淮悼。
線程在執(zhí)行同步塊之前,JVM會(huì)首先在當(dāng)前線程的棧楨中創(chuàng)建用于存儲(chǔ)鎖記錄的空間揽思,并通過CAS將Mark Word替換為指向鎖記錄的指針袜腥。如果成功,則當(dāng)前線程獲得鎖,如果失敗羹令,則表示有其他線程競爭鎖鲤屡,此時(shí)會(huì)自旋等待,并會(huì)膨脹成重量級(jí)鎖福侈。
輕量級(jí)鎖的釋放也是通過CAS來執(zhí)行的酒来,如果成功,則表示沒有競爭發(fā)生肪凛,如果失敗堰汉,鎖會(huì)膨脹成重量級(jí)鎖。
我們發(fā)現(xiàn)伟墙,偏向鎖相比較輕量級(jí)鎖通過CAS以及自旋等方式獲取鎖翘鸭,性能更好一些,因?yàn)樗挥性谂袛鄬ο箢^中的線程Id不是當(dāng)前線程的時(shí)候才去CAS競爭鎖戳葵,而輕量級(jí)鎖一開始就CAS競爭鎖了就乓。
重量級(jí)鎖
重量級(jí)鎖通過對象內(nèi)部的monitor
實(shí)現(xiàn),當(dāng)鎖處于整個(gè)狀態(tài)下拱烁,其他線程試圖獲取鎖時(shí)生蚁,都會(huì)被阻塞住,當(dāng)持有鎖的線程釋放鎖之后才會(huì)喚醒這些線程戏自,被喚醒的線程展開新的一輪鎖爭奪邦投。此時(shí)操作系統(tǒng)實(shí)現(xiàn)線程之間的切換需要從用戶態(tài)到內(nèi)核態(tài)的切換,線程切換的成本非常高浦妄。
參考資料
方騰飛:《Java并發(fā)編程的藝術(shù)》