1. 程式人生 > >大白話解釋一波多執行緒裡面的各種“鎖”

大白話解釋一波多執行緒裡面的各種“鎖”

鎖:解決資源佔用的問題;保證同一時間一個物件只有一個執行緒在訪問;

鎖機制的作用:有些業務邏輯在執行過程中要求對資料進行排他性的訪問,於是需要通過一些機制保證在此過程中資料被鎖住不會被外界修改,這就是所謂的鎖機制。

飢餓:是指如果執行緒T1佔用了資源R,執行緒T2又請求封鎖R,於是T2等待。T3也請求資源R,當T1釋放了R上的封鎖後,系統首先批准了T3的請求,T2仍然等待。然後T4又請求資源R,當T3釋放了R上的封鎖之後,系統又批准了T4的請求......,T2可能永遠等待。(就好比食堂打飯,刷卡的優先打飯,付現金的要等刷卡的打完了才能打,可是拿著現金的很早就在那兒準備好了,可以刷卡的那條隊伍卻一直來了一個又一個,來個沒完,拿現金的只好餓死。這也就是ReentrantLock顯示鎖裡提供的不公平鎖機制

(當然了,ReentrantLock也提供了公平鎖的機制,由使用者根據具體的使用場景而決定到底使用哪種鎖策略),不公平鎖能夠提高吞吐量但不可避免的會造成某些執行緒的飢餓。)

死鎖:線上程間共享多個資源的時候,如果兩個執行緒分別佔有一部分資源並且同時等待對方的資源,就會造成死鎖。儘管死鎖很少發生,但一旦發生就會造成應用的停止響應(就像夫妻吵架,都等著對方先道歉,就會造成死鎖)

活鎖:是指執行緒1可以使用資源,但它很禮貌,讓其他執行緒先使用資源,執行緒2也可以使用資源,但它很紳士,也讓其他執行緒先使用資源。這樣你讓我,我讓你,最後兩個執行緒都無法使用資源。

互斥鎖:對共享資源的訪問必須是順序的,也就是說當多個執行緒對共享資源訪問的時候,只能有一個執行緒可以獲得該共享資源的鎖,當執行緒A嘗試獲取執行緒B的鎖時,執行緒A必須等待或者阻塞,直到執行緒B釋放該鎖為止,否則執行緒A將一直等待下去,因此java內建鎖也稱作互斥鎖,也即是說鎖實際上是一種互斥機制。

死鎖和飢餓的區別:

·死鎖程序等待永遠不會被釋放的資源,餓死程序等待會被釋放但卻不會分配給自己的資源,表現為等待時限沒有上界(排隊等待或忙式等待);

·死鎖一定發生了迴圈等待,而餓死則不然。這也表明通過資源分配圖可以檢測死鎖存在與否,但卻不能檢測是否有程序餓死;

·死鎖一定涉及多個程序,而飢餓或被餓死的程序可能只有一個。

·在飢餓的情形下,系統中有至少一個程序能正常執行,只是飢餓程序得不到執行機會。而死鎖則可能會最終使整個系統陷入死鎖並崩潰

可重入鎖:

如果鎖具備可重入性,則稱作為可重入鎖。像synchronized和ReentrantLock都是可重入鎖,可重入性實際上表明瞭鎖的分配機制:基於執行緒的分配,而不是基於方法呼叫的分配。什麼是可重入性?

舉個簡單的例子,當一個執行緒執行到某個synchronized方法時,比如說method1,而在method1中會呼叫另外一個synchronized方法method2,此時執行緒不必重新去申請鎖,而是可以直接執行方法method2。如下面的程式碼:

class MyClass {

    public synchronized void method1() {

        method2();

    }

    public synchronized void method2() {

    }

}

method1和method2都是synchronized修飾的方法,在method1裡面呼叫method2的時候,不需要重新申請鎖,可以直接呼叫就行了(其實可以反過來想一想,如果synchronized不具有重入性當我呼叫了method1的時候,得申請鎖,申請好了之後那麼method1就擁有了這個鎖,那麼呼叫method2的時候,又要重新申請鎖,而鎖在method1的手上,這時候又要重新申請鎖,顯然是不可能得到的,這不科學。所以,synchronize和lock都是具有可重入性的)

可中斷鎖:如果某一執行緒A正在執行鎖中的程式碼,另一執行緒B正在等待獲取該鎖,可能由於等待時間過長,執行緒B不想等待了,想先處理其他事情,我們可以讓它中斷自己或者在別的執行緒中中斷它,這種就是可中斷鎖。在Java中,synchronized就不是可中斷鎖,而Lock是可中斷鎖。

非公平鎖:剛剛講到的食堂打飯的例子,就是一個不公平鎖的例子;synchronized就是非公平鎖,它無法保證等待的執行緒獲取鎖的順序。這樣就可能導致某個或者一些執行緒永遠獲取不到鎖。

公平鎖:公平鎖即儘量以請求鎖的順序來獲取鎖。比如同是有多個執行緒在等待一個鎖,當這個鎖被釋放時,等待時間最久的執行緒(最先請求的執行緒)會獲得該鎖,這種就是公平鎖。

讀寫鎖:就是將一個資源的訪問分成兩個鎖,一個讀鎖,一個寫鎖;正因為有了讀寫鎖,才使得多個執行緒之間的讀寫操作不會發生衝突。ReadWriteLock就是讀寫鎖,可以通過readLock()獲取讀鎖,通過writeLock()獲取寫鎖。

自旋鎖:舉個例子:獲取到資源的執行緒A對這個資源加鎖,其他執行緒比如B要訪問這個資源首先要獲得鎖,而此時A持有這個資源的鎖,只有等待執行緒A邏輯執行完,釋放鎖,這個時候B才能獲取到資源的鎖進而獲取到該資源。這個過程中,A一直持有著資源的鎖,那麼沒有獲取到鎖的其他執行緒比如B怎麼辦?通常就會有兩種方式:

1. 一種是沒有獲得鎖的程序就直接進入阻塞(BLOCKING),這種就是互斥鎖

2. 另外一種就是沒有獲得鎖的程序,不進入阻塞,而是一直迴圈著,看是否能夠等到A釋放了資源的鎖,這種就是自旋鎖

什麼時候用自旋鎖比較好?如果A執行緒佔用鎖的時間比較短,這個時候用自旋鎖比較好,可以節省CPU在不同執行緒間切換花費的時間開銷;如果A執行緒佔用鎖的時間比較長,那麼使用自旋鎖的話,B執行緒就會長時間浪費CPU的時間而得不到執行(要執行一個執行緒需要CPU,並且需要獲得鎖),這個時候不建議使用自旋鎖;還有遞迴的時候儘量不要使用自旋鎖,可能會造成死鎖。

悲觀鎖:總是假設最壞的情況,每次去拿資料的時候都認為別人會修改,所以每次在拿資料的時候都會上鎖,這樣別人想拿這個資料就會阻塞直到它拿到鎖。這樣可以保證每次都只有一個執行緒在訪問這個資料;傳統的關係型資料庫裡邊就用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。再比如Java裡面的同步原語synchronized關鍵字的實現也是悲觀鎖。

樂觀鎖:很樂觀,每次去拿資料的時候都認為別人不會修改,所以不會上鎖,那麼就會有很多物件可以同時訪問這個鎖裡面的資料,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個資料,可以使用版本號等機制。樂觀鎖適用於多讀的應用型別,這樣可以提高吞吐量。

適用場景:

悲觀鎖:比較適合寫入操作比較頻繁的場景,如果出現大量的讀取操作,每次讀取的時候都會進行加鎖,這樣會增加大量的鎖的開銷,降低了系統的吞吐量。

樂觀鎖:比較適合讀取操作比較頻繁的場景,如果出現大量的寫入操作,資料發生衝突的可能性就會增大,為了保證資料的一致性,應用層需要不斷的重新獲取資料,這樣會增加大量的查詢操作,降低了系統的吞吐量。