轉載請注明原創(chuàng)出處懂不東
支持和尊重原創(chuàng)奴迅,謝謝渣淤!
說明(廢話一下)
這是自己寫簡書的第一篇文章颖对,其實很早就有想法開始寫捻撑,但是一直總覺得不到時候,因為總想以比較完美開始進行。但時不待人顾患,最近想法上出現(xiàn)了一些變化番捂,我覺得完美是經(jīng)過不斷探索,糾正和思考之后所精煉出來的江解。因此设预,想借助簡書這個平臺,讓自己和文章不斷地取得進步犁河。寫簡書的目標有以下幾點:
1.文章追求極簡鳖枕,系統(tǒng)和精細化知識體系,作為自己的思維映射桨螺,簡單點宾符,就是為了復習和記憶。
2.樹立自己一套寫文章最舒服的風格灭翔,著重原理上講解魏烫。
3.希望自己寫的東西能給有需要的人一點點幫助和借鑒吧。
最后肝箱,非常希望網(wǎng)友能夠神吐槽则奥,指出不足之處,自己不斷思索和改進狭园,萬事開頭難读处,不喜勿噴,謝謝唱矛!
前言
Java并發(fā)庫是一個非常重要的知識點罚舱,在java1.5之前可能用synchroized關鍵字較多,但是在真正高并發(fā)系統(tǒng)里绎谦,對于鎖的細凉苊疲化操作和靈活度上明顯synchroized心有余而力不足,比如要保證讀寫一致synchroized一定對讀寫都加鎖窃肠,在極端情況下包个,全為讀讀操作,這性能犧牲的就很不值得了冤留。因此java1.5就引入了Lock碧囊,它是一個接口,指定了鎖的基本操作纤怒,ReentrantLock是其中一個實現(xiàn)類糯而,個人認為也是最重要的一個,因為它是看其他并發(fā)類的基礎泊窘, ReentrantLock里面包裝了Sync熄驼,繼承自AbstractQueuedSynchronizer(AQS)像寒,AQS是一個抽象類,實現(xiàn)了很多基礎方法瓜贾,利用模板模式诺祸,讓子類去能夠很好的擴展和運用。該文章不是為了講解ReentrantLock的基礎知識或者源碼解讀祭芦,關于這部分知識筷笨,網(wǎng)上已經(jīng)有很多優(yōu)秀的文章了,可以自行查看实束。 下面對ReentrantLock的一些自己認為容易疑惑的點進行講解,建議事先對 ReentrantLock的源碼能夠有所了解逊彭,看了可能會有點收獲吧咸灿。
一。獨占鎖(排他鎖)
獨占鎖(排他鎖):多個線程并發(fā)請求獲取鎖侮叮,只能一個線程得到鎖避矢,其他線程放到雙向不循環(huán)隊列等待
T1,T2,T3是三個線程,同時來獲取鎖囊榜,只有T1獲得到鎖审胸;T2和T3加入到雙向隊列中
二。雙向隊列與鎖競爭
場景1.多個線程同時競爭鎖卸勺,只有一個線程獲得鎖砂沛,其他線程存入雙向隊列中等待(第一點已經(jīng)說明了,不多說)
下面的場景根據(jù)前提:
已知T1線程獲取鎖曙求,T2碍庵,T3線程在等待隊列中
場景2: T1線程釋放鎖了,在沒有新的線程來競爭鎖的情況下悟狱,隊列中head節(jié)點的下一個節(jié)點T2直接獲取鎖静浴,T2線程變成head節(jié)點,原來的head節(jié)點置空挤渐,被GC
場景3:T1線程釋放鎖了苹享,此時新來線程T4,那么新的線程T4和head節(jié)點的下一個節(jié)點T2互相競爭鎖浴麻,T4獲取鎖得问,隊列結構不變(非公平鎖)
場景4:T1線程釋放鎖了,此時新來線程T4和head節(jié)點的下一個節(jié)點T2互相競爭鎖软免,T2獲得鎖椭赋,T4就要入隊列尾部?(非公平鎖)
場景5:T1線程釋放鎖了 ,此時新來的線程T4不跟head節(jié)點的下一個節(jié)點T2競爭鎖或杠,直接入隊列哪怔,T2獲得鎖(不存在競爭,公平鎖FIFO,如果隊列為空(只有一個線程)认境,則能直接獲取鎖胚委,否則都要加入到隊尾,按照隊列順序獲得鎖叉信,這樣就保證了公平性)亩冬,圖與場景4一樣,只是內部原理不一樣
三硼身」杓保可重入鎖與State
可重入鎖指的是,一個線程獲取到鎖之后佳遂,在沒有進行釋放鎖营袜,該線程還能進行再次獲取鎖。state是一個volatile類型丑罪,是一個同步狀態(tài)位荚板,在ReentrantLock可表現(xiàn)為獲取鎖的個數(shù)。ReentrantLock與synchroized都是可重入的吩屹。
重入鎖的意義:為了避免造成死鎖而設計的跪另,同一個線程,同一個對象煤搜,用同一個鎖免绿,鎖住多個方法,如果這些方法之間互相調用擦盾,如果鎖不可重入针姿,那么在進入一個鎖方法后,再調用另外一個鎖的方法厌衙,則會一直堵塞距淫,造成死鎖;如下舉例說明
方法sync1與方法sync2用同一個鎖鎖住婶希,使用同一個對象ReentrantLockTest 調用sync1方法里面又調用sync2方法榕暇,如果ReentrantLock是不可重入的,那么在執(zhí)行sync1獲得鎖之后就不能再繼續(xù)獲取鎖喻杈,調用sync2方法是遇到lock.lock()獲取不了鎖就會一直堵塞在那里等待sync1釋放鎖彤枢,sync1在一直等待sync2執(zhí)行完畢,相互等待就造成死鎖
public class ReentrantLockTest {
private Lock lock = new ReentrantLock();
public void sync1() {
try {
lock.lock();
sync2();
} finally { lock.unlock(); } }
public void sync2() {
try {
lock.lock();
System.out.println("sync2");
} finally { lock.unlock(); } }
四筒饰。自旋與堵塞掛起
自旋:指的是不斷的重復循環(huán)缴啡,直到達到特定條件,則跳出循環(huán)瓷们;好處是能夠避免上cpu上下文切換业栅,不好的地方是不斷自旋相對會耗cpu資源秒咐;隨著線程數(shù)的增多性能降低;一般自旋代碼執(zhí)行時間較短碘裕,線程數(shù)少携取,可以用自旋,能夠提高響應
堵塞掛起:堵塞掛起Cpu占有率會相對低帮孔,通過改變線程狀態(tài)進行堵塞雷滋,要繼續(xù)執(zhí)行時需要被通知喚醒,在線程數(shù)運行比較多文兢,競爭激烈的情況優(yōu)勢比較明顯
ReentrantLock對應自旋和堵塞掛起的運用很多晤斩,以下通過例子說明:
場景:ReentrantLock通過自旋+堵塞掛起的方式進行鎖的競爭,等待隊列中的線程通過堵塞掛起的方式等待鎖釋放的通知信號姆坚,然后自旋的去再次競爭鎖
偽代碼如下:
五澳泵。等待隊列線程狀態(tài)
等待對列的每一個節(jié)點(Node)都有一個waitStatus屬性,表示的該節(jié)點線程的線程狀態(tài)旷偿,這個有什么用嘛烹俗?
1.用來標識線程類型爆侣,比如EXCLUSIVE狀態(tài)萍程,標識的該線程是一個獨占線程,而PROPAGATE標識該線程是具有傳播性兔仰,意思是當前節(jié)點獲得鎖茫负,會傳播到下一個節(jié)點,如果下一個節(jié)點也是PROPAGATE狀態(tài)乎赴,就能夠共享同一把鎖忍法。特別是在讀寫鎖里就淋漓盡致體現(xiàn)出來了,這邊稍微提一下榕吼,后續(xù)讀寫鎖會詳細講解饿序,簡單畫一個圖理解
r表示讀鎖,共享鎖羹蚣,具備傳播性原探,w表示寫鎖,是獨占鎖顽素;那么假設另外一個線程釋放鎖咽弦,等待隊列就有機會去獲取鎖了,
根據(jù)隊列的順序胁出,前兩個節(jié)點根據(jù)PROPAGATE傳播屬性型型,兩個線程都能獲得同一把鎖,而w是EXCLUSIVE屬性全蝶,一次性只能有一個線程有一把鎖
2.有時候我們要對某一個線程進行中斷取消闹蒜,不用了寺枉,這個線程剛好在等待隊列,那么為了讓該線程不參與競爭鎖嫂用,總得有個標識吧型凳,這個標識就是CANCELLED,在新來一條的等待線程進入隊列后嘱函,要掛起時甘畅,就會從后邊向前遍歷粤咪,將所有CANCELLED的線程剔除掉槽卫,排到正確節(jié)點后缓升;另外還有CONDITION狀態(tài)疯趟,這個是跟Condition相關的半沽,以后文章再講解
六婆跑。公平鎖與非公平鎖區(qū)別
非公平鎖:當?shù)却犃杏写嬖诰€程喘垂,新來的線程也能夠有機會獲取鎖艰躺,這就不是新來新得鎖(FIFO)撇寞,因此非公平
公平鎖:當?shù)却犃兄写嬖诰€程顿天,新來的線程沒有機會去競爭鎖,老實去排隊尾去蔑担,獲取鎖按照隊列順序來牌废,排隊前的先得
注:當隊列是空的,說明沒有等著要獲取鎖的線程啤握,那么新來的線程就是最優(yōu)先的鸟缕,能直接獲取鎖
關鍵性源碼:
公平鎖:
非公平鎖:
公平鎖與非公平鎖在獲取鎖時,差別僅在于判斷隊列是否有線程等待獲取鎖排抬,也就是hasQueuedPredecessors()方法
七懂从。等待隊列Head頭節(jié)點
等待隊列Head頭節(jié)點表示的是當前獲得鎖的線程,當只有一條線程來獲取鎖蹲蒲,是不存在等待隊列番甩,因此是不存在head頭節(jié)點;當并發(fā)時有多條線程届搁,只有一條線程獲得鎖缘薛,另外的線程會進入到等待隊列。這時候首先會新初始化一個head節(jié)點咖祭,表示的是已經(jīng)獲得鎖的那條線程掩宜,然后才將等待的線程連接到head節(jié)點后面。所以為什么鎖釋放時么翰,要從head節(jié)點的下一個節(jié)點取出進行競爭牺汤,如果獲得鎖,就將head節(jié)點置空浩嫌,進行釋放內存
八檐迟。exclusiveOwnerThread的意義
在Reentrantlock有一個 exclusiveOwnerThread屬性表示的是當前持有鎖的線程补胚,為什么要專門設計這樣一個屬性,是為了方便定位持鎖的線程追迟,假設沒有 exclusiveOwnerThread溶其,將這屬性放到隊列中,還要去專門找敦间,顯得有些麻煩瓶逃。其實這邊更想表達的意思是,個人認為這是一種技巧廓块,不單在這里厢绝,在我們寫程序,或者hashmap源碼带猴,讀寫鎖昔汉,經(jīng)常設計到鏈表結構,隊列的拴清,會將頭尾節(jié)點獨立出來作為屬性靶病,好處在于能夠快速定位和識別特殊節(jié)點,也有助于理解口予。
九娄周。AQS的CAS操作
看一個CAS的API
protected final boolean compareAndSetState(int expect, int update) { // See below for intrinsics setup to support this return unsafe.compareAndSwapInt(this, stateOffset, expect, update);}
CAS:其實就是一個樂觀鎖的設計思想,當并發(fā)時多個線程要對一個共享變量進行設置值苹威,允許這些線程都去設值昆咽,但是有且只有一個線程設置是成功的驾凶,其他則失敗的牙甫。典型的以空間換取時間的想法,一提到樂觀鎖调违,大家可能會想到version版本窟哺,在數(shù)據(jù)庫設計時經(jīng)常用到,比如電商中的庫存減少為了保證多個線程一次性只能有一個線程減庫存成功技肩,偽代碼如下
update product set version = version + 1,stock = stock -1 where version = 且轨? and productId = ?
假設并發(fā)有兩條線程執(zhí)行上面的語句(條件版本號是一樣的),那么就只有一條執(zhí)行成功虚婿,版本加1了旋奢,另一條用執(zhí)行不報錯,但不會更改庫存了然痊。
CAS的原理是一樣的至朗,下面我用圖來進行講解
已知,內存中的值為0剧浸,此時期望值和更新值都不存在
這時有兩條線程(T1,T2)來同時要對內存中的值進行修改锹引,要將更新值設置到內存中矗钟,那么預期原值是什么呢,相當于上面sql中條件中的版本號嫌变,預期原值跟內存中的值進行比較吨艇,如果相同,則運行更新值把值更新給內存腾啥,兩條線程同時要值更新給內存东涡,如下
如果T1線程首先修改成功,那么就會變成內存中的值為1倘待,此時T2要來改內存中的值软啼,結果預期原值跟內存中的值不一樣了,就更改不了延柠。相當于預期原值此時要為1才有可能修改得了(相當于版本號已經(jīng)從0升為1了祸挪,原理其實就是樂觀鎖)