Java中的鎖
Lock接口
鎖是用來控制多個線程控制訪問共享資源的方式,一般來說,一個鎖能限制多個線程同時訪問共享資源(但是有些鎖可以允許多個線程并發的訪問共享資源,比如讀寫鎖).在Lock接口出現之前,Java程序是靠synchronized關鍵字實現鎖功能的.
而Java SE 5之后,并發包中新增了Lock接口(以及相關實現類)用來實現鎖功能,它提供了與synchronized關鍵字類似的同步功能,只是在使用時需要顯式地獲取和釋放鎖。雖然它缺少了(通過synchronized塊或者方法所提供的)隱式獲取鎖的便捷性,但是卻擁有了獲取鎖與釋放鎖的可操作性,可中斷性的獲取鎖以及超時獲取鎖等多種synchronized關鍵字不具備的同步特性.
使用synchronized關鍵字將會隱式地獲取鎖,但是它將鎖的獲取和釋放固化了,也就是先獲取再釋放.當然,這種方式簡化了同步的管理,可是拓展性沒有顯示的鎖獲取和釋放來的好.
eg:針對一個場景,手把手進行鎖獲取和釋放,先獲得鎖A,然后再獲取鎖B,當鎖B獲得后,釋放鎖A同時獲取鎖C,當鎖C獲得后,再釋放B同時獲取鎖D,以此類推。這種場景下,synchronized關鍵字就不那么容易實現了,而使用Lock卻容易許多。
在finally塊中釋放鎖,目的是保證在獲取到鎖之后,最終能夠被釋放。
注意
:不要將獲取鎖的過程寫在try塊中,因為如果在獲取鎖(自定義鎖的實現)時發生了異常,異常拋出的同時,也會導致鎖無故釋放。
隊列同步器
隊列同步器的接口與示例
同步器的設計是基于模板方法的,也就是說,使用者需要繼承同步器并重寫指定的方法,隨后將同步器組合在自定義同步組件的實現中,并調用同步器提供的模板方法,而這些模板方法將會調用使用者重寫的方法。
重寫同步器指定的方法時,需要使用同步器提供如下的3個方法來訪問或修改同步狀態.
- getState():獲取當前同步狀態
- setState(int newState):設置當前同步狀態。
- compareAndSetState(int expect,int update):使用CAS設置當前狀態,該方法能夠保證狀態設置 的原子性
實現自定義同步組件時,將會調用同步器提供的模板方法
同步器提供的模板方法基本上分為3類:
- 獨占式獲取與釋放同步狀態
- 共享式獲取與釋放同步狀態
- 查詢同步隊列中的等待情況
自定義同步組件將使用同步器提供的模板方法來實現自己的同步語義。
獨占鎖:在同一時刻只能有一個線程獲取到鎖,而其他獲取鎖的線程只能處于同步隊列中等待,只有獲取鎖的線程釋放了鎖,后繼的線程才能夠獲取鎖.
上述的示例中,獨占鎖是一個自定義同步組件,它在同一時刻只允許一個線程占有鎖。Mutex中定義了一個靜態內部類,該內部類繼承了同步器并實現了獨占式獲取和釋放同步狀態。在tryAcquire(int acquires)方法中,如果經過CAS設置成功(同步狀態設置為1),則代表獲取了同步狀態,而在tryRelease(int releases)方法中只是將同步狀態重置為0。用戶使用Mutex時并不會直接和內部同步器的實現打交道,而是調Mutex提供的方法,在Mutex的實現中,以獲取鎖的lock()方法為例,只需要在方法實現中調用同步器的模板方法acquire(int args)即可,當前線程調用該方法獲取同步狀態失敗后會被加入到同步隊列中等待,這樣就大大降低了實現一個可靠自定義同步組件的門檻。
隊列同步器的實現分析
同步隊列
同步器依賴內部的同步隊列(一個FIFO雙向隊列)來完成同步狀態管理.當前線程獲取同步狀態失敗時,同步器會將當前線程以及等待狀態等信息構造成一個節點(Node)并將其加入同步隊列,同時會阻塞當前線程,當同步狀態釋放時,會把首節點中的線程喚醒,使其再次嘗試獲取同步狀態.
同步隊列中的節點(Node)用來保存獲取同步狀態失敗的線程引用,等待狀態以及前驅和后繼節點,節點屬性名稱以及描述如下.
節點是構成同步隊列的基礎,同步器擁有首節點(head)和尾節點(tail),沒有成功獲取同步狀態的線程將會成為節點加入該隊列的尾部.
同步器包含了兩個節點類型的應用,一個指向頭結點,而另一個指向尾節點.
當一個線程成功獲取了同步狀態或者鎖的時候,其他線程將無法獲取到同步狀態,轉而被構造成節點并加入同步隊列中.而這個加入隊列的過程必須保證線程安全,因此同步器提供了一個基于CAS的設置尾節點的方法:compareAndSetTail(Node expect,Node update),他需要傳遞當前線程"認為"的尾節點和當前節點,只有設置成功后,當前節點才正式與之前的尾節點建立關聯.
同步隊列遵循循FIFO,首節點是獲取同步狀態成功的節點,首節點的線程在釋放同步狀態時,將會喚醒后繼節點,而后繼節點將會在獲取同步狀態成功時將自己設置為首節點.
首節點的設置是通過讀取同步狀態成功的線程來完成的,由于只有一個線程能夠獲取到同步狀態,因此設置頭結點的方法不需要CAS來保證,它只需要將首節點設置成為原首節點的后繼節點并斷開首節點的next引用即可.
獨占式同步狀態獲取與釋放
通過調用同步器的的acquire(int arg)方法可以獲取同步狀態,該方法對中斷不敏感,也就是由于線程獲取同步狀態失敗后進入同步隊列中,后續對線程進行中斷操作時,線程不會從同步隊列中移出.
上述代碼主要完成了同步狀態獲取,節點構造,介入同步隊列以及在同步隊列中自旋等待的相關工作,其主要邏輯是:首先調用自定義同步器實現的tryAcquire(int arg)方法,該方法保證線程安全的獲取同步狀態,如果同步狀態獲取失敗,則構造同步節點(獨占式Node.EXCLUSIVE,同一時刻只能有一個線程成功獲取同步狀態)并通過addWaiter(Node node)方法將該節點加入到同步隊列的尾部,最后調用acquireQueued(Node node,int arg)方法,使得該節點以“死循環”的方式獲取同步狀態。如果獲取不到則阻塞節點中的線程,而被阻塞線程的喚醒主要依靠前驅節點的出隊或阻塞線程被中斷來實現。
上述代碼通過使用compareAndSetTail(Node expect,Node update)方法來確保節點能夠被線程安全添加.
在enq(final Node node)方法中,同步器通過“死循環”來保證節點的正確添加,在“死循環”中只有通過CAS將節點設置成為尾節點之后,當前線程才能從該方法返回,否則當前線程不斷地嘗試設置??梢钥闯觯琫nq(final Node node)方法將并發添加節點的請求通過CAS變得“串行化”了。
節點進入同步隊列之后,就進入了一個自旋的過程,每個節點(或者說每個線程)都在自省地觀察,當條件滿足,獲取到了同步狀態,就可以從這個自旋過程中退出,否則依舊留在這個自旋過程中(并會阻塞節點的線程)
在acquireQueued(final Node node,int arg)方法中,當前線程在“死循環”中嘗試獲取同步狀態,而只有前驅節點是頭節點才能夠嘗試獲取同步狀態,這是為什么?原因有兩個,如下。
1.頭節點是成功獲取到同步狀態的節點,而頭節點的線程釋放了同步狀態之后,將會喚醒其后繼節點,后繼節點的線程被喚醒后需要檢查自己的前驅節點是否是頭點。
2.維護同步隊列的FIFO原則。
在上圖中,由于非首節點線程前驅節點出隊或者被中斷從而等待狀態返回,隨后檢查自己的前驅節點是否是頭節點,如果是則嘗試獲取同步狀態.看到節點和節點之間在循環檢查的過程中基本不相互通信,而只是簡單地判斷自己的前驅是否為頭結點,這樣就使得節點的釋放規則符合FIFO,并且也便于過早通知的處理(過早通知是指前驅節點不是頭節點的線程由于中斷而被喚醒)。
前驅節點為頭結點且能夠獲取同步狀態的判斷條件和線程進入等待狀態是獲取同步狀態的自旋過程.當同步狀態獲取成功之后,當前線程從acquire(int arg)方法返回,如果對于鎖這種并發組件而言,代表當前線程獲取了鎖.
當前線程獲取同步狀態并執行了相應的邏輯之后,就需要釋放同步狀態,使得后續節點能夠獲取同步狀態.通過調用同步器的release(int arg)方法可以釋放同步狀態,該方法在釋放了狀態之后,會喚醒其后集節點(進而使后繼節點重新嘗試獲取同步狀態)。
該方法執行時,會喚醒頭結點的后集節點線程,unparkSuccessor(Node node)方法使用LockSupport來喚醒處于等待狀態的線程。
小結:在獲取同步狀態時,同步器維護了一個同步隊列,獲取狀態失敗的線程都會被加入到隊列中并在隊列中自旋;移出隊列(或停止自旋)的條件是前驅節點為頭結點且獲取了同步狀態.在釋放同步狀態時,同步器調用tryRelease(int arg)方法釋放同步狀態,然后喚醒頭節點的后繼節點。
共享式同步狀態獲取與釋放
共享式獲取和獨占最主要的區別在于同一時刻是否能夠有多個線程同時獲取到同步狀態.
以文件的讀寫為例,如果一個程序在對文件進行讀操作,那么這一時刻對于該文件的寫操作均被阻塞,而讀操作能夠同時進行。寫操作要求對資源的獨占式訪問,而讀操作可以是共享式訪問,兩種不同的訪問模式在同一時刻對文件或資源的訪問情況.
- 左半部分,共享式訪問資源時,其他共享式的訪問均被允許,而獨占式訪問被阻塞
- 右半部分,獨占式訪問資源時,同一時刻其他訪問均被阻塞。
通過調用同步器的acquireShared(int arg)方法可以共享式地獲取同步狀態
在acquireShared(int arg)方法中,同步器調用tryAcquireShared(int arg)方法嘗試獲取同步狀態,tryAcquireShared(int arg)方法返回值為int類型,當返回值大于等于0時,表示能夠獲取到同步狀態。因此,在共享式獲取的自旋過程中,成功獲取到同步狀態并退出自旋的條件就是tryAcquireShared(int arg)方法返回值大于等于0??梢钥吹剑赿oAcquireShared(int arg)方法的自旋過程中,如果當前節點的前驅為頭節點時,嘗試獲取同步狀態,如果返回值大于等于0,表示該次獲取同步狀態成功并從自旋過程中退出。
與獨占式一樣,共享式獲取也需要釋放同步狀態,通過調用releaseShared(int arg)方法可以釋放同步狀態
該方法在釋放同步狀態之后,將會喚醒處于等待狀態的節點.對于能夠支持多個線程同時訪問的并發組件(比如Semaphore),它和獨占式主要區別在于tryReleaseShared(int arg)方法必須確保同步狀態(或者資源數)線程安全釋放,一般是通過循環和CAS來保證的,因為釋放同步狀態的操作會同時來自多個線程.
$ 獨占式超時獲取同步狀態
通過調用同步器的doAcquireNanos(int arg,long nanosTimeout)方法可以超時獲取同步狀態,即在指定的時間段內獲取同步狀態,如果獲取到同步狀態則返回true,否則,返回false。該方法提供了傳統Java同步操作(比如synchronized關鍵字)所不具備的特性。
該方法在自旋過程中,當節點的前驅節點為頭節點時嘗試獲取同步狀態,如果獲取成功則從該方法返回,這個過程和獨占式同步獲取的過程類似,但是在同步狀態獲取失敗的處理上有所不同。如果當前線程獲取同步狀態失敗,則判斷是否超時(nanosTimeout小于等于0表示已經超時),如果沒有超時,重新計算超時間隔nanosTimeout,然后使當前線程等待nanosTimeout納秒(當已到設置的超時時間,該線程會從LockSupport.parkNanos(Objectblocker,long nanos)方法返回)。
獨占式超時獲取同步狀態的流程
獨占式超時獲取同步狀態doAcquireNanos(int arg,long nanosTimeout)和獨占式獲取同步狀態acquire(int args)在流程上非常相似,其主要區別在于未獲取到同步狀態時的處理邏輯。
- acquire(int args)在未獲取到同步狀態時,將會使當前線程一直處于等待狀態
- doAcquireNanos(int arg,long nanosTimeout):使當前線程等待nanosTimeout納秒,如果當前線程在nanosTimeout納秒內沒有獲取到同步狀態,將會從等待邏輯中自動返回。
參考書籍:<<Java并發編程的藝術>>