同步機制簡介 線程同步機制是一套用於協調線程間的數據訪問及活動的機制,該機制用於保障線程安全以及實現這些線程的共同目標。 線程同步機制是編程語言為多線程運行制定的一套規則,合理地運用這些規則可以很大程度上保障程式的正確運行。 這套機制包含兩方面的內容,一是關於多線程間的數據訪問的規則,二是多線程間活 ...
同步機制簡介
線程同步機制是一套用於協調線程間的數據訪問及活動的機制,該機制用於保障線程安全以及實現這些線程的共同目標。
線程同步機制是編程語言為多線程運行制定的一套規則,合理地運用這些規則可以很大程度上保障程式的正確運行。
這套機制包含兩方面的內容,一是關於多線程間的數據訪問的規則,二是多線程間活動的規則。前者關乎程式運行的正確與否,是相當重要的內容;後者很大程度上是影響程式的運行效率,也是不容忽視的內容。不太嚴謹地說,數據訪問的規則主要是由鎖來實現,線程間活動的規則則表現線程調度上。
鎖
線程安全問題的產生前提是多個線程併發訪問共用數據,那麼一種保障線程安全的方法就是將多個線程對共用數據的併發訪問轉換為串列訪問,即一個共用數據一次只能被一個線程訪問,該線程訪問結束後其他線程才能對其進行訪問。鎖就是利用這種思路來實現線程同步機制。
GoLang中換了個思路,通過通道(channel)來實現共用數據的安全性。
鎖的相關概念
鎖在編程里是個蠻有趣的概念。
鎖:置於可啟閉的器物上,以鑰匙或暗碼(如字碼機構、時間機構、自動釋放開關、磁性螺線管等)打開的扣件 ——線上新華字典
特定代碼的作用域或是lock()
和unlock()
方法之間的代碼構成的區域就是“器物”的表徵,線程訪問其中的共用數據相當於解開“扣件”,打開了“器物”;通常所說“獲得xx鎖”,更像是獲得了“鑰匙或暗碼”能夠打開“扣件”的憑證。
說人話就是,鎖在生活通常是“護衛”、“保護”的含義,應當是阻止進入或下一步行動的拒絕機制;在編程里略有不同,它一方面是指代了需要被保護代碼(或數據)的範圍,一方面又指代了進入受保護代碼(或數據)的憑證。基於此,許多書與博客中的“申請鎖”這說法才能說的通。不然,按生活常理,你獲得了一把鎖,沒有鑰匙好像也沒什麼用。
上述內容是本人的一點心得體會,不保證正確,不保證嚴謹
下麵介紹與鎖相關的一些概念。
- 臨界區(Critical Section):獲得鎖之後和釋放鎖之前的這段時間內執行的代碼
- 內部鎖(Intrinsic Lock)與顯式鎖(Explicit Lock):按時Java虛擬機對鎖實現的方式劃分,內部鎖(由關鍵字
synchronized
實現)與顯式鎖(Lock
介面的實現類實現) - 可重入性(Reentrancy):一個線程在其持有一個鎖的時候能否再次(或多次)申請該鎖
- 鎖的爭用與調度:鎖也可以被看作是一種排他性的資源,因此爭用、調度概念也對鎖適用。鎖的調用基本上是Java虛擬機的設計者需要考慮的問題。Java平臺中鎖的調度策略包括了公平與非公平兩種,內部鎖屬於非公平鎖而顯式鎖則既支持公平鎖又支持非公平鎖
- 鎖的粒度:一個鎖實例所保護的共用數據的數量大小就被稱為鎖的粒度。但這是一個相對概念,應該根據實際情況來說明鎖粒度的大小
- 如果有多個線程訪問同一個鎖所保護的共用數據,那麼就你這些線程同步在這個鎖上,或是對這些線程所訪問的共用數據訪問進行了加鎖;相應地,這些線程所執行的臨界區就被為這個鎖所引導的臨界區。
- 鎖的排他、互斥:都是指一個鎖一次只能被一個線程所持有的特性。
以上就是與鎖相關的一些概念,這些概念也比較通用,在其他編程語言里或多或少也會有它們的身影。
內部鎖:synchronized
關鍵字
Java平臺中任何一個對象都有唯一一個與之關聯的鎖。這種鎖被稱為監視器或是內部鎖。內部鎖是一種排他鎖,它能保障原子性、可見性和有序性。
內部鎖通過synchronized
關鍵字實現的。synchronized
關鍵字可以修飾方法及代碼塊。修飾方法時,此方法被稱為同步(靜態/實例)方法;修飾代碼塊時,被稱為同步(靜態)代碼塊。
同步方法
synchronized
修飾的方法被稱為同步方法。同步方法的整個方法體就是一個臨界區。
public synchronized void sayHello(){
// do something
}
同步靜態方法相當於以當前類對象為引導鎖的同步塊。
同步塊
synchronized (鎖句柄){
// do something
}
synchronized
關鍵字所引導的代碼塊就是臨界區。鎖句柄是一個對象的引用。鎖句柄可以填寫this
關鍵字,表示當前對象。習慣上也直接稱鎖句柄為鎖。鎖句柄對應的監視器就被稱為相應同步塊的引導鎖。相應地,我們稱呼相應的同步為該鎖引導的同步塊。
作為鎖句柄的變數通常採用final
修飾。這是因為鎖句柄變數的值一旦改變,會導致執行同一個同步塊的多個線程實際上使用不同的鎖,從而導致競態。因而,鎖句柄的變數通常聲明形式為private final Object lock = new Object();
特性
線程在執行臨界區代碼時必須持有該臨界區的引導鎖。一個線程執行到同步塊(同步方法也可看作是同步塊)時必須先申請該同步塊的引導鎖,只有申請成功該的線程才能夠執行相應的臨界區。一個線程執行完成臨界區代碼後引導該臨界區的鎖就會被自動釋放。在這個過程中,線程對內部鎖申請與釋放的動作由Java虛擬機負責完成,這也是synchronized
實現的鎖被稱之為內部鎖的原因。
內部鎖的使用並不會導致鎖泄漏。Java編譯器對同步塊代碼作了特殊的處理,這使得臨界區的代碼即使拋出異常也不會妨礙內部鎖的釋放。
內部鎖的調度
Java虛擬機會為每個內部鎖分配一個入口集,用於記錄等待獲得相應內部鎖的線程。多個線程申請同一個鎖的時候,只有一個申請都能夠成為該鎖的持有線程(即申請鎖成功),而其他申請者的申請操作會失敗。這些申請失敗的線程並不會拋出異常,而是會被暫停(生命周期狀態變為BLOCKED
)並被存入相應鎖的入口集中等待再申請鎖的機會。入口 集中的線程就被稱為相應內部鎖的等待線程。當該內部鎖被釋放時,入口集中的任意線程會被Java虛擬機喚醒,得到再次申請鎖的機會。由於是非公平的高度,被喚醒的等待處理器運行時可能還有其他新和活躍線程(RUNNABLE
狀態且未進入過入口集)與該線程搶占這個被釋放的鎖,因此被喚醒的線程不一定就能成為該鎖的持有線程。另外,Java虛擬機從入口集中選擇一個等待線程的演算法與虛擬機具體實現有關,總的來說是隨機的。(像極了女神和她的備胎們)
顯式鎖:Lock
介面
顯式鎖是自JDK1.5引入的排他鎖,其作用與內部鎖大致相同,並額外提供些特性。
顯式鎖是java.util.concurrent.locks.Lock
介面的實例。該介面對顯式鎖進行了抽象,類java.uitl.concurrent.locks.ReentrantLock
是它預設實現類。
使用模版:
private final Lock lock = ....; // 一個Lock介面的實例
...
lock.lock(); // 申請鎖
try{
// do something
}finally{
lock.unlock(); // 手動釋放鎖,避免鎖泄漏
}
顯式鎖支持公平調度,但開銷相對較大,預設使用非公平調度。
改進型鎖:讀寫鎖
鎖的排他性使得多個線程無法以線程安全的方式在同一時刻對變數進行讀取(只讀不更新),不利於提高系統的併發性。
讀寫鎖(Read/Write Lock)是一種改進型的排他鎖,也被稱為共用/排他鎖(Shared/Exclusive Lock)。讀寫鎖允許多個線程可以同時讀取(只讀)共用變數,但是一次只允許一個線程對共用變數進行更新(包括讀取後再更新)。任何線程讀取變數的時候,其他線程無法更新這些變數 ;一個線程更新共用變數的時候,其他任何線程都無法訪問該變數。
輕量級同步機制:volatile
關鍵字
volatile
有“易揮發”的意思,引申為“不穩定”。volatile
關鍵字用於修飾共用可變變數,即沒有使用final
關鍵字修飾的實例變數或靜態變數,相應的變數被稱為volatile
變數。
volatile
關鍵字表示被修飾的變數的值容易變化(即被其他線程更改),因而不穩定。volatile
變數的不穩定性意味著對這種變數的讀寫操作都必須從高速緩存或者主記憶體中讀取,以獲取變數的相對新值。因些,volatile
變數不會被編譯器分配到寄存器進行存儲,對volatile
變數的讀寫操作都是記憶體訪問操作。
volatile
關鍵字常被稱為輕量級鎖,其作用與鎖的作用有相同的地方:保證可見性的有序性。在原子性方面,它僅能保障寫volatile
變數操作的原子性,但沒有鎖的排他性;其次,volatile
關鍵字的使用不會引起上下文切換(正是“輕量級”的原因)。
作用
volatile
關鍵字的作用包括:保障可見性、有序性和long/double型變數讀寫操作的原子性(不是賦值操作)。
某些32位Java虛擬機上long/double型變數寫操作不具有原子性,加上volatile
關鍵字後,其讀寫操作本身就具有了原子性。一般地,對於volatile
變數的賦值操作,其右邊表達式中涉及共用變數(包括賦值的volatile
變數本身),那麼這個同仁操作就不是原子操作,要保障這樣操作的原子性,仍然需要鎖。
讀線程對volatile
變數讀取操作會產生類似於獲得鎖的效果;寫線程則會產生類似於釋放鎖的效果;因此,volatile
具有保障有序性和可見性的作用。
如果volatile
變數是數組,那麼volatile
關鍵字只能對數組引用本身的操作起作用(讀取數組引用和更新數組引用),而無法對數組元素的操作起作用(讀取、更新數組元素)。
開銷
總的來說,讀寫操作成本介於普通變數寫和在臨界區內進行讀寫操作之間。
CAS指令
CAS(Compare and Swap)是對一種處理器指令的稱呼,不少多線程相關的Java標準庫類的實現最終都會藉助CAS,實際中大多數情況不會直接使用。
對於簡單的自增操作count++
來說,使用鎖的開銷過大,使用volatile
又不能保證原子性,這種情況下可以使用CAS。它能將read-modify-write和check-and-act之類操作轉換為原子操作,其實現如偽代碼所示:
boolean compareAndSwap(Variable v ,Object oldVal ,Object newVal){
if (oldVal == v.get()){ // check: 檢查變數值是否被其他線程修改過
v.set(newVal); // act:更新變數值
return true; // 更新成功
}
return false; // 變數值被其他線程修改,更新失敗
}
CAS操作前提假設是:如果變數v的當前值和客戶請求(即調用)CAS時所提供的變數值(即變數的舊值)是相等的,那麼就說明其他線程並沒有修改過變數v的值,可以執行更新值的操作。其他線程更新值的操作則會失敗。
這個假設並不一定總能成立。
ABA 問題
對於共用變數v,當前線程看到它的值為A的那一刻,其他線程已經將其值更新為B,接著在當前線程執行CAS時該變數又被其他線程更新為A了,這就是ABA問題。
對ABA 問題接受度與要實現的演算法有關,某些情況下無法接受ABA問題的存在。ABA問題常引入修訂號來解決,即用[共用變數實際值,修改號]這樣的元組來表示共用變數的值,AtomicStampedReference
類就是基於這種思想實現的。
原子變數類
原子變數類比鎖的粒度更細,更輕量級,並且對於在多處理器系統上實現高性能的併發代碼來說是非常關鍵的。原子變數將發生競爭的範圍縮小到單個變數上。
原子變數類相當於一種泛化的 volatile 變數,能夠支持原子的、有條件的讀/改/寫操作。
原子類在內部使用 CAS 指令(基於硬體的支持)來實現同步。這些指令通常比鎖更快。
原子變數類可分為4組:
分組 | 類 |
---|---|
基礎數據型 | AtomicBoolean AtomicInteger AtomicLong |
數組型 | AtomicIntegerArray AtomicLongArray AtomicReferenceArray |
欄位更新器 | AtomicIntegerFieldUpdater AtomicLongFieldUpdater AtomicStampedReference |
引用型 | AtomicReference AtomicReferenceFieldUpdater AtomicMarkableReference |