Java中的鎖
Lock接口
鎖是用來控制多個線程控制訪問共享資源的方式,一般來說,一個鎖能限制多個線程同時訪問共享資源(但是有些鎖可以允許多個線程并發(fā)的訪問共享資源,比如讀寫鎖).在Lock接口出現(xiàn)之前,Java程序是靠synchronized關(guān)鍵字實現(xiàn)鎖功能的.
而Java SE 5之后,并發(fā)包中新增了Lock接口(以及相關(guān)實現(xiàn)類)用來實現(xiàn)鎖功能,它提供了與synchronized關(guān)鍵字類似的同步功能,只是在使用時需要顯式地獲取和釋放鎖。雖然它缺少了(通過synchronized塊或者方法所提供的)隱式獲取鎖的便捷性,但是卻擁有了獲取鎖與釋放鎖的可操作性,可中斷性的獲取鎖以及超時獲取鎖等多種synchronized關(guān)鍵字不具備的同步特性.
使用synchronized關(guān)鍵字將會隱式地獲取鎖,但是它將鎖的獲取和釋放固化了,也就是先獲取再釋放.當(dāng)然,這種方式簡化了同步的管理,可是拓展性沒有顯示的鎖獲取和釋放來的好.
eg:針對一個場景昵时,手把手進行鎖獲取和釋放,先獲得鎖A,然后再獲取鎖B瓤湘,當(dāng)鎖B獲得后,釋放鎖A同時獲取鎖C恩尾,當(dāng)鎖C獲得后弛说,再釋放B同時獲取鎖D甥厦,以此類推凛驮。這種場景下,synchronized關(guān)鍵字就不那么容易實現(xiàn)了钠绍,而使用Lock卻容易許多冀偶。
在finally塊中釋放鎖醒第,目的是保證在獲取到鎖之后,最終能夠被釋放进鸠。
注意
:不要將獲取鎖的過程寫在try塊中稠曼,因為如果在獲取鎖(自定義鎖的實現(xiàn))時發(fā)生了異常,異常拋出的同時客年,也會導(dǎo)致鎖無故釋放霞幅。
隊列同步器
隊列同步器的接口與示例
同步器的設(shè)計是基于模板方法的,也就是說搀罢,使用者需要繼承同步器并重寫指定的方法蝗岖,隨后將同步器組合在自定義同步組件的實現(xiàn)中,并調(diào)用同步器提供的模板方法榔至,而這些模板方法將會調(diào)用使用者重寫的方法抵赢。
重寫同步器指定的方法時,需要使用同步器提供如下的3個方法來訪問或修改同步狀態(tài).
- getState():獲取當(dāng)前同步狀態(tài)
- setState(int newState):設(shè)置當(dāng)前同步狀態(tài)。
- compareAndSetState(int expect,int update):使用CAS設(shè)置當(dāng)前狀態(tài)唧取,該方法能夠保證狀態(tài)設(shè)置 的原子性
實現(xiàn)自定義同步組件時铅鲤,將會調(diào)用同步器提供的模板方法
同步器提供的模板方法基本上分為3類:
- 獨占式獲取與釋放同步狀態(tài)
- 共享式獲取與釋放同步狀態(tài)
- 查詢同步隊列中的等待情況
自定義同步組件將使用同步器提供的模板方法來實現(xiàn)自己的同步語義。
獨占鎖:在同一時刻只能有一個線程獲取到鎖枫弟,而其他獲取鎖的線程只能處于同步隊列中等待邢享,只有獲取鎖的線程釋放了鎖,后繼的線程才能夠獲取鎖.
上述的示例中,獨占鎖是一個自定義同步組件淡诗,它在同一時刻只允許一個線程占有鎖骇塘。Mutex中定義了一個靜態(tài)內(nèi)部類伊履,該內(nèi)部類繼承了同步器并實現(xiàn)了獨占式獲取和釋放同步狀態(tài)。在tryAcquire(int acquires)方法中款违,如果經(jīng)過CAS設(shè)置成功(同步狀態(tài)設(shè)置為1)唐瀑,則代表獲取了同步狀態(tài),而在tryRelease(int releases)方法中只是將同步狀態(tài)重置為0插爹。用戶使用Mutex時并不會直接和內(nèi)部同步器的實現(xiàn)打交道哄辣,而是調(diào)Mutex提供的方法,在Mutex的實現(xiàn)中赠尾,以獲取鎖的lock()方法為例力穗,只需要在方法實現(xiàn)中調(diào)用同步器的模板方法acquire(int args)即可,當(dāng)前線程調(diào)用該方法獲取同步狀態(tài)失敗后會被加入到同步隊列中等待气嫁,這樣就大大降低了實現(xiàn)一個可靠自定義同步組件的門檻当窗。
隊列同步器的實現(xiàn)分析
同步隊列
同步器依賴內(nèi)部的同步隊列(一個FIFO雙向隊列)來完成同步狀態(tài)管理.當(dāng)前線程獲取同步狀態(tài)失敗時,同步器會將當(dāng)前線程以及等待狀態(tài)等信息構(gòu)造成一個節(jié)點(Node)并將其加入同步隊列,同時會阻塞當(dāng)前線程,當(dāng)同步狀態(tài)釋放時,會把首節(jié)點中的線程喚醒,使其再次嘗試獲取同步狀態(tài).
同步隊列中的節(jié)點(Node)用來保存獲取同步狀態(tài)失敗的線程引用,等待狀態(tài)以及前驅(qū)和后繼節(jié)點,節(jié)點屬性名稱以及描述如下.
節(jié)點是構(gòu)成同步隊列的基礎(chǔ),同步器擁有首節(jié)點(head)和尾節(jié)點(tail),沒有成功獲取同步狀態(tài)的線程將會成為節(jié)點加入該隊列的尾部.
同步器包含了兩個節(jié)點類型的應(yīng)用,一個指向頭結(jié)點,而另一個指向尾節(jié)點.
當(dāng)一個線程成功獲取了同步狀態(tài)或者鎖的時候,其他線程將無法獲取到同步狀態(tài),轉(zhuǎn)而被構(gòu)造成節(jié)點并加入同步隊列中.而這個加入隊列的過程必須保證線程安全,因此同步器提供了一個基于CAS的設(shè)置尾節(jié)點的方法:compareAndSetTail(Node expect,Node update),他需要傳遞當(dāng)前線程"認(rèn)為"的尾節(jié)點和當(dāng)前節(jié)點,只有設(shè)置成功后,當(dāng)前節(jié)點才正式與之前的尾節(jié)點建立關(guān)聯(lián).
同步隊列遵循循FIFO杉编,首節(jié)點是獲取同步狀態(tài)成功的節(jié)點超全,首節(jié)點的線程在釋放同步狀態(tài)時,將會喚醒后繼節(jié)點邓馒,而后繼節(jié)點將會在獲取同步狀態(tài)成功時將自己設(shè)置為首節(jié)點.
首節(jié)點的設(shè)置是通過讀取同步狀態(tài)成功的線程來完成的,由于只有一個線程能夠獲取到同步狀態(tài),因此設(shè)置頭結(jié)點的方法不需要CAS來保證,它只需要將首節(jié)點設(shè)置成為原首節(jié)點的后繼節(jié)點并斷開首節(jié)點的next引用即可.
獨占式同步狀態(tài)獲取與釋放
通過調(diào)用同步器的的acquire(int arg)方法可以獲取同步狀態(tài)嘶朱,該方法對中斷不敏感,也就是由于線程獲取同步狀態(tài)失敗后進入同步隊列中光酣,后續(xù)對線程進行中斷操作時疏遏,線程不會從同步隊列中移出.
上述代碼主要完成了同步狀態(tài)獲取,節(jié)點構(gòu)造,介入同步隊列以及在同步隊列中自旋等待的相關(guān)工作,其主要邏輯是:首先調(diào)用自定義同步器實現(xiàn)的tryAcquire(int arg)方法,該方法保證線程安全的獲取同步狀態(tài)救军,如果同步狀態(tài)獲取失敗财异,則構(gòu)造同步節(jié)點(獨占式Node.EXCLUSIVE,同一時刻只能有一個線程成功獲取同步狀態(tài))并通過addWaiter(Node node)方法將該節(jié)點加入到同步隊列的尾部唱遭,最后調(diào)用acquireQueued(Node node,int arg)方法戳寸,使得該節(jié)點以“死循環(huán)”的方式獲取同步狀態(tài)。如果獲取不到則阻塞節(jié)點中的線程拷泽,而被阻塞線程的喚醒主要依靠前驅(qū)節(jié)點的出隊或阻塞線程被中斷來實現(xiàn)疫鹊。
上述代碼通過使用compareAndSetTail(Node expect,Node update)方法來確保節(jié)點能夠被線程安全添加.
在enq(final Node node)方法中,同步器通過“死循環(huán)”來保證節(jié)點的正確添加司致,在“死循環(huán)”中只有通過CAS將節(jié)點設(shè)置成為尾節(jié)點之后拆吆,當(dāng)前線程才能從該方法返回,否則當(dāng)前線程不斷地嘗試設(shè)置脂矫≡嬉可以看出,enq(final Node node)方法將并發(fā)添加節(jié)點的請求通過CAS變得“串行化”了庭再。
節(jié)點進入同步隊列之后捞奕,就進入了一個自旋的過程牺堰,每個節(jié)點(或者說每個線程)都在自省地觀察,當(dāng)條件滿足颅围,獲取到了同步狀態(tài)萌焰,就可以從這個自旋過程中退出,否則依舊留在這個自旋過程中(并會阻塞節(jié)點的線程)
在acquireQueued(final Node node,int arg)方法中谷浅,當(dāng)前線程在“死循環(huán)”中嘗試獲取同步狀態(tài),而只有前驅(qū)節(jié)點是頭節(jié)點才能夠嘗試獲取同步狀態(tài)奶卓,這是為什么一疯?原因有兩個,如下夺姑。
1.頭節(jié)點是成功獲取到同步狀態(tài)的節(jié)點墩邀,而頭節(jié)點的線程釋放了同步狀態(tài)之后,將會喚醒其后繼節(jié)點盏浙,后繼節(jié)點的線程被喚醒后需要檢查自己的前驅(qū)節(jié)點是否是頭點眉睹。
2.維護同步隊列的FIFO原則。
在上圖中,由于非首節(jié)點線程前驅(qū)節(jié)點出隊或者被中斷從而等待狀態(tài)返回,隨后檢查自己的前驅(qū)節(jié)點是否是頭節(jié)點,如果是則嘗試獲取同步狀態(tài).看到節(jié)點和節(jié)點之間在循環(huán)檢查的過程中基本不相互通信,而只是簡單地判斷自己的前驅(qū)是否為頭結(jié)點,這樣就使得節(jié)點的釋放規(guī)則符合FIFO,并且也便于過早通知的處理(過早通知是指前驅(qū)節(jié)點不是頭節(jié)點的線程由于中斷而被喚醒)废膘。
前驅(qū)節(jié)點為頭結(jié)點且能夠獲取同步狀態(tài)的判斷條件和線程進入等待狀態(tài)是獲取同步狀態(tài)的自旋過程.當(dāng)同步狀態(tài)獲取成功之后,當(dāng)前線程從acquire(int arg)方法返回竹海,如果對于鎖這種并發(fā)組件而言,代表當(dāng)前線程獲取了鎖.
當(dāng)前線程獲取同步狀態(tài)并執(zhí)行了相應(yīng)的邏輯之后,就需要釋放同步狀態(tài),使得后續(xù)節(jié)點能夠獲取同步狀態(tài).通過調(diào)用同步器的release(int arg)方法可以釋放同步狀態(tài),該方法在釋放了狀態(tài)之后,會喚醒其后集節(jié)點(進而使后繼節(jié)點重新嘗試獲取同步狀態(tài))丐黄。
該方法執(zhí)行時,會喚醒頭結(jié)點的后集節(jié)點線程,unparkSuccessor(Node node)方法使用LockSupport來喚醒處于等待狀態(tài)的線程斋配。
小結(jié):在獲取同步狀態(tài)時,同步器維護了一個同步隊列,獲取狀態(tài)失敗的線程都會被加入到隊列中并在隊列中自旋;移出隊列(或停止自旋)的條件是前驅(qū)節(jié)點為頭結(jié)點且獲取了同步狀態(tài).在釋放同步狀態(tài)時,同步器調(diào)用tryRelease(int arg)方法釋放同步狀態(tài),然后喚醒頭節(jié)點的后繼節(jié)點灌闺。
共享式同步狀態(tài)獲取與釋放
共享式獲取和獨占最主要的區(qū)別在于同一時刻是否能夠有多個線程同時獲取到同步狀態(tài).
以文件的讀寫為例艰争,如果一個程序在對文件進行讀操作,那么這一時刻對于該文件的寫操作均被阻塞桂对,而讀操作能夠同時進行甩卓。寫操作要求對資源的獨占式訪問,而讀操作可以是共享式訪問蕉斜,兩種不同的訪問模式在同一時刻對文件或資源的訪問情況.
- 左半部分,共享式訪問資源時逾柿,其他共享式的訪問均被允許,而獨占式訪問被阻塞
- 右半部分,獨占式訪問資源時蛛勉,同一時刻其他訪問均被阻塞鹿寻。
通過調(diào)用同步器的acquireShared(int arg)方法可以共享式地獲取同步狀態(tài)
在acquireShared(int arg)方法中,同步器調(diào)用tryAcquireShared(int arg)方法嘗試獲取同步狀態(tài)诽凌,tryAcquireShared(int arg)方法返回值為int類型毡熏,當(dāng)返回值大于等于0時,表示能夠獲取到同步狀態(tài)侣诵。因此痢法,在共享式獲取的自旋過程中狱窘,成功獲取到同步狀態(tài)并退出自旋的條件就是tryAcquireShared(int arg)方法返回值大于等于0〔聘椋可以看到蘸炸,在doAcquireShared(int arg)方法的自旋過程中,如果當(dāng)前節(jié)點的前驅(qū)為頭節(jié)點時尖奔,嘗試獲取同步狀態(tài)搭儒,如果返回值大于等于0,表示該次獲取同步狀態(tài)成功并從自旋過程中退出提茁。
與獨占式一樣淹禾,共享式獲取也需要釋放同步狀態(tài),通過調(diào)用releaseShared(int arg)方法可以釋放同步狀態(tài)
該方法在釋放同步狀態(tài)之后,將會喚醒處于等待狀態(tài)的節(jié)點.對于能夠支持多個線程同時訪問的并發(fā)組件(比如Semaphore)茴扁,它和獨占式主要區(qū)別在于tryReleaseShared(int arg)方法必須確保同步狀態(tài)(或者資源數(shù))線程安全釋放铃岔,一般是通過循環(huán)和CAS來保證的,因為釋放同步狀態(tài)的操作會同時來自多個線程.
$ 獨占式超時獲取同步狀態(tài)
通過調(diào)用同步器的doAcquireNanos(int arg,long nanosTimeout)方法可以超時獲取同步狀態(tài)峭火,即在指定的時間段內(nèi)獲取同步狀態(tài)毁习,如果獲取到同步狀態(tài)則返回true,否則卖丸,返回false纺且。該方法提供了傳統(tǒng)Java同步操作(比如synchronized關(guān)鍵字)所不具備的特性。
該方法在自旋過程中坯苹,當(dāng)節(jié)點的前驅(qū)節(jié)點為頭節(jié)點時嘗試獲取同步狀態(tài)隆檀,如果獲取成功則從該方法返回,這個過程和獨占式同步獲取的過程類似粹湃,但是在同步狀態(tài)獲取失敗的處理上有所不同恐仑。如果當(dāng)前線程獲取同步狀態(tài)失敗,則判斷是否超時(nanosTimeout小于等于0表示已經(jīng)超時)为鳄,如果沒有超時裳仆,重新計算超時間隔nanosTimeout,然后使當(dāng)前線程等待nanosTimeout納秒(當(dāng)已到設(shè)置的超時時間孤钦,該線程會從LockSupport.parkNanos(Objectblocker,long nanos)方法返回)歧斟。
獨占式超時獲取同步狀態(tài)doAcquireNanos(int arg,long nanosTimeout)和獨占式獲取同步狀態(tài)acquire(int args)在流程上非常相似,其主要區(qū)別在于未獲取到同步狀態(tài)時的處理邏輯偏形。
- acquire(int args)在未獲取到同步狀態(tài)時静袖,將會使當(dāng)前線程一直處于等待狀態(tài)
- doAcquireNanos(int arg,long nanosTimeout):使當(dāng)前線程等待nanosTimeout納秒,如果當(dāng)前線程在nanosTimeout納秒內(nèi)沒有獲取到同步狀態(tài)俊扭,將會從等待邏輯中自動返回队橙。
參考書籍:<<Java并發(fā)編程的藝術(shù)>>