一.樂觀鎖/悲觀鎖
1.樂觀鎖
①基本定義:樂觀主義者,每次不加鎖而是假設沒有衝突而去完成某項操作,如果因爲衝突失敗就重試,直到成功爲止,是一種無鎖的原子算法。適合鎖競爭不激烈的場景。
②實現原理:CAS(Compare And Set or Compare And Swap),三元組CompareAndSet(V,A,B)
CAS是解決多線程並行情況下使用鎖造成性能損耗的一種機制,CAS操作包含三個操作數——內存位置(V)、預期原值(A)、新值(B)。如果內存位置的值與預期原值相匹配,那麼處理器會自動將該位置值更新爲新值。否則,處理器不做任何操 作。
注:CAS造成的ABA問題
ABA問題描述:
*進程P1在共享變量中讀到值爲A
*P1被搶佔了,進程P2執行
*P2把共享變量裏的值從A改成了B,再改回到A,此時被P1搶佔。
*P1回來看到共享變量裏的值沒有被改變,於是繼續執行。
③C#中實現舉例:System.Threading.Interlocked中的原子自增、原子遞減等
④Java中的實現舉例:java.util.concurrent.atomic.AtomicInteger
JVM級別的實現:
2.悲觀鎖(阻塞式)
①基本定義:悲觀主義者,總是認爲有其他線程會隨時和自己爭奪資源,因此每次在操作數據前會先鎖定該資源資源使得其他線程不能獲取該資源,具有排它性。適合鎖競爭比較激烈的場景
②實現原理
③C#中實現舉例:Lock關鍵字(底層還是使用的Monitor,是對是Monitor.Enter和Monitor.Exit的封裝)、Monitor類等
反編譯(彙編)
源碼註釋:
④Java中的實現舉例:synchronized關鍵字、Lock類
lock方法源碼註釋:
synchronized反編譯代碼(javap -v):
private static int i=10;
private static Object o=new Object();
public static void main(String[] args) {
synchronized (o)
{
i++;
}
}
注:還有MySQL關係型數據庫裏邊就用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等。
二.可重入鎖
1.定義:簡單的說就是同一個線程可以重複獲取到鎖
加鎖場景代碼:
public class Count{
Lock lock = new Lock();
public void print(){
lock.lock();
doAdd();
lock.unlock();
}
public void doAdd(){
lock.lock();
//do something
lock.unlock();
}
}
2.自實現可重入鎖
public class Lock{
boolean isLocked = false;
Thread lockedBy = null;
int lockedCount = 0;
public synchronized void lock()
throws InterruptedException{
Thread thread = Thread.currentThread();
while(isLocked && lockedBy != thread){
wait();
}
isLocked = true;
lockedCount++;
lockedBy = thread;
}
public synchronized void unlock(){
if(Thread.currentThread() == this.lockedBy){
lockedCount--;
if(lockedCount == 0){
isLocked = false;
notify();
}
}
}
}
自實現不可重入鎖:
public class Lock{
private boolean isLocked = false;
public synchronized void lock() throws InterruptedException{
while(isLocked){
wait();
}
isLocked = true;
}
public synchronized void unlock(){
isLocked = false;
notify();
}
}
3.Java中的可重入鎖的實現:ReentrantLock屬於可重入鎖
三.公平鎖/非公平鎖
1.公平鎖
就是很公平,在併發環境中,每個線程在獲取鎖時會先查看此鎖維護的等待隊列,如果爲空,或者當前線程是等待隊列的第一個,就佔有鎖,否則就會加入到等待隊列中,以後會按照FIFO的規則從隊列中取到自己。
公平鎖的優點是等待鎖的線程不會餓死。缺點是整體吞吐效率相對非公平鎖要低,等待隊列中除第一個線程以外的所有線程都會阻塞,CPU喚醒阻塞線程的開銷比非公平鎖大。
2.非公平鎖
上來就直接嘗試佔有鎖,如果嘗試失敗,就再採用類似公平鎖那種方式。 非公平鎖的優點是可以減少喚起線程的開銷,整體的吞吐效率高,因爲線程有機率不阻塞直接獲得鎖,CPU不必喚醒所有線程。缺點是處於等待隊列中的線程可能會餓死,或者等很久纔會獲得鎖。
Java語言中應用實例:ReentrantLock可以通過構造函數的參數設置來定義公平鎖和非公平鎖
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
公平鎖:
非公平鎖:
結論:相對來說非公平鎖效率高於公平鎖,因爲非公平鎖減少了線程掛起的機率,後來的線程有一定機率逃離被掛起的開銷。但是如果線程佔用鎖的實踐短的話可能造成先來的線程飢餓。
四.分段鎖
①特點:對數據存儲空間進行分段,段內加鎖,段間並行
②Java中的實現:
java.util.concurrent.ConcurrentHashMap
put源碼:
public V put(K key, V value) {
Segment<K,V> s;
if (value == null)
throw new NullPointerException();
int hash = hash(key);
int j = (hash >>> segmentShift) & segmentMask;//計算segment索引
if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
s = ensureSegment(j);//獲取到key取hash後所在的段
return s.put(key, hash, value, false);//存入對應的段中
}
s.put()源碼:
注:之前我斷章截意的認爲C#中的ConcurrentDictionary和Java中的ConcurrentHashMap一樣會使用分段鎖的機制來實現線程安全,直到我看了源碼我才驚奇的發現它沒有使用分段鎖,他和jdk1.8之後 ConcurrentHashMap的加鎖機制是相似的,即之鎖住鏈表的頭部。
五.自旋鎖
①由於線程從阻塞狀態切換到運行態是一個耗費性能的過程,因此線程持有鎖的實踐短的話,如果當前線程獲取鎖失敗,則不會立刻阻塞自己而是先自旋等待一會,一般採用空循環的方式(空跑)進行自旋。
②基本實現原理:
*當前線程競爭鎖失敗時,打算阻塞自己
*不直接阻塞自己,而是自旋(空等待,比如一個空的有限for循環)一會
*在自旋的同時重新競爭鎖
*如果自旋結束前獲得了鎖,那麼鎖獲取成功;否則,自旋結束後阻塞自己
③C#中的實現舉例
ConcurrentQueue中的入隊列操作中的空跑自旋
SpinOnce()方法源碼:
public bool NextSpinWillYield
{
//PlatformHelper.IsSingleProcessor檢查當前機器是否爲單處理器機器,如果是則不進行自旋,直接
//執行讓步處理
//以及如果自旋次數超過YIELD_THRESHOLD,也不進行自旋,同樣進入讓步處理
get { return m_count > YIELD_THRESHOLD || PlatformHelper.IsSingleProcessor; }
}
public void SpinOnce()
{
//檢查是進入自旋狀態還是進入不同程度的讓步策略
if (NextSpinWillYield)
{
//We prefer to call Thread.Yield first, triggering a SwitchToThread. This
// unfortunately doesn't consider all runnable threads on all OS SKUs. In
// some cases, it may only consult the runnable threads whose ideal processor
// is the one currently executing code. Thus we oc----ionally issue a call to
// Sleep(0), which considers all runnable threads at equal priority. Even this
// is insufficient since we may be spin waiting for lower priority threads to
// execute; we therefore must call Sleep(1) once in a while too, which considers
// all runnable threads, regardless of ideal processor and priority, but may
// remove the thread from the scheduler's queue for 10+ms, if the system is
// configured to use the (default) coarse-grained system timer.
CdsSyncEtwBCLProvider.Log.SpinWait_NextSpinWillYield();
int yieldsSoFar = (m_count >= YIELD_THRESHOLD ? m_count - YIELD_THRESHOLD : m_count);
if ((yieldsSoFar % SLEEP_1_EVERY_HOW_MANY_TIMES) == (SLEEP_1_EVERY_HOW_MANY_TIMES - 1))
{
//小睡1毫秒,讓所有的線程都有機會獲得鎖
Thread.Sleep(1);
}
else if ((yieldsSoFar % SLEEP_0_EVERY_HOW_MANY_TIMES) == (SLEEP_0_EVERY_HOW_MANY_TIMES - 1))
{
//放棄自己的剩餘時間片,只允許>=自己優先級的線程佔用資源,如果沒有則自己繼續執行
Thread.Sleep(0);
}
else
{
#if PFX_LEGACY_3_5
Platform.Yield();
#else
//將當前線程放入就緒隊列,如果隊列中沒有其他線程,則繼續執行
Thread.Yield();
#endif
}
}
else
{
//線程進入自旋等待狀態
Thread.SpinWait(4 << m_count);
}
// Finally, increment our spin counter.
m_count = (m_count == int.MaxValue ? YIELD_THRESHOLD : m_count + 1);
}
注:單處理器機器不許進入自旋狀態,原因很簡單,“你一個人佔着僅有的資源在那自嗨半天結果啥事也沒幹”
六.偏向鎖/輕量級鎖/重量級鎖
①重量級鎖:傳統重量級鎖指使用操作系統互斥量來實現加鎖(PV操作),使用這種鎖的話,即使沒有所競爭的時候還是會調用操作系統級別的互斥量實現加鎖,造成性能消耗。
②輕量級鎖:通過簡單的數據操作實現加鎖,不需要調用OS級別的加鎖機制。
*在當前線程自己的棧區創建Lock Record用於存儲Object的Mark Word的拷貝
*將Mark Word副本存入lock record
*基於CAS的機制將Object的Mark Word跟新爲執行Lock record的指針
*如果CAS跟新成功,則當前線程獲取到鎖,否則檢查Mark Word出的指針是否指向自己的棧針
*如果指向自己的棧針,則說明已經獲取到鎖,則重入代碼快執行,否則說明鎖已經被佔用,則膨脹爲重量級鎖
CAS操作前:
CAS操作後:
總結:在沒有線程競爭時,JVM使用輕量級鎖進行加鎖處理,一旦存在兩個以上線程的競爭的話,馬上膨脹爲重量級鎖。
③偏向鎖:偏向鎖可以說是在輕量級鎖上的進一步優化,輕量級鎖在沒有鎖競爭的條件下還需要執行CAS操作,偏向鎖連CAS都不操作。所謂“偏”就是說偏袒於第一次獲得鎖的線程,如果在接下來的執行過程中,該鎖沒有被其他的線程獲取,則持有偏向鎖的線程將不需要執行同步操作,直接獲得運行資源。直到另外一個線程到來,偏向鎖開始膨脹。
七.獨享鎖/共享鎖
①獨享鎖:每次只能被一個線程佔有的鎖
共享鎖:每次可以被多個線程佔有的鎖
②Java中的實現舉例:
Lock接口的實現類ReentrantLock,其是獨享鎖。ReentrantReadWriteLock,其讀鎖是共享鎖,其寫鎖是獨享鎖。讀鎖的共享鎖可保證併發讀是非常高效的,其中讀寫,寫讀 ,寫寫的過程是互斥的。
③C#中的實現舉例
ReaderWriterLockSlim類
八.互斥鎖/讀寫鎖
讀寫鎖使用場景:如果你的數據有讀有些,並且讀多寫少,並且希望在寫這個數據的時候不允許讀,那麼就可以使用讀寫鎖了,這將支持你併發的讀。
讀寫鎖中的鎖降級:
鎖降級指的是寫鎖降級成爲讀鎖。如果當前線程擁有寫鎖,然後將其釋放,最後再獲取讀鎖,這種分段完成的過程不能稱之爲鎖降級。鎖降級是指把持住(當前擁有的)寫鎖,再獲取到讀鎖,隨後釋放(先前擁有的)寫鎖的過程。
也就是說一個線程目前持有寫鎖,在釋放寫鎖之前獲取到讀鎖(注意同一線程具有可重入性),當釋放寫鎖後直接過渡到讀鎖,這期間不存在任何阻塞間隔。
使用場景:如果你寫完數據後需要讀取數據,爲了不出現幻讀,則可以使用鎖降級。