前言:前面的內容中我們一直在講鎖,其實多線程的關鍵問題就是在線程安全,而保障線程安全的方式一般有兩種,一種就是加鎖,另一種則是CAS,CAS之前已經知道了是什么東西,接下來說一下鎖,其實鎖也有很多種分類。例如悲觀鎖,樂觀鎖等等。。。有助於理解后面的難點
悲觀鎖和樂觀鎖
一般樂觀鎖和悲觀鎖都是在數據庫層面的。
-
悲觀鎖:悲觀鎖認為數據會很容易被其他的線程更改,在自己改數據之前,會有其他的線程來改這個數據了,因此在數據處理器,會對整個數據處理過程進行一個互斥鎖,禁止其他線程對這個數據進行染指。一般悲觀鎖都是依靠數據庫的鎖機制,在操作之前,訪問數據的時候先獲取鎖,如果獲取鎖失敗了,就代表有其他的線程正在修改這個數據,然后等待數據的鎖被釋放,如果獲取成功了,就對數據進行加鎖,然后數據操作成功之后,提交事務再釋放鎖。通過這種方式來保證,同一時刻只有一個線程能進入到這個更新操作來保證數據的一致性
-
樂觀鎖:樂觀鎖跟悲觀鎖不一樣的地方是,樂觀鎖認為數據一般不會造成沖突,因此在訪問記錄之前,不會加互斥鎖,只有在數據庫提交更新的時候,才會檢測數據是否沖突,一般常規操作是在數據庫中,加一個version字段,在更新操作之前,先查一遍數據庫,獲取到version字段的值,由於數據庫的update操作本身就是原子性的,在更新操作的時候,where條件后加入一個version的比較操作,如果version的值對應上才更新,否則則不更新
//偽代碼思路 public int update(Entity entity){ int version = execute('select version from where id = #{entity.id}'); entity.setVersion(version) int count = execute('update table set name=#{entity.name}... version=#{entity.version} where id = {entity.id} and version = #{version}') return count; }
如果count的數值為0就代表數據在當前線程改之前已經被其他的線程改過了,因此不執行更新,也可以繼續獲取數據,通過比較數據繼續更新。由於樂觀鎖在提交的時候才會鎖定(因為update的原子性),因此不會產生任何死鎖
公平鎖和非公平鎖
公平和非公平鎖是根據線程的搶占機制來分的,如果是公平鎖,則線程獲取鎖的順序是按照線程請求鎖的時間早晚來決定的,來的晚的進阻塞隊列,可以把公平鎖理解成排隊,而非公平鎖則是大家一起搶,不管你先來后到,誰搶到了算誰的,可以理解成平時咱們擠公交和擠地鐵。
ReentrantLock則提供了公平和非公平鎖的實現。說的具體一點就是。如果有三個線程1,2,3,此時線程1持有了鎖,2,3線程也都需要獲取這把鎖,並且2請求比3早,如果是公平鎖,那就是2線程獲取,3線程先一邊待着去,等2用完了他才能用。非公平鎖就是,2,3的機會都是一樣的,你們倆根據線程調度策略,誰搶着算誰的。
一般在沒有非常需要公平鎖的前提下做好使用非公平,因為公平鎖的排列方式會帶來額外的性能開銷。
可重入鎖
可重入,顧名思義就是可以反復的進入,放到一個線程當中,當一個線程想要獲取一個被其他線程已經取得的互斥鎖的時候,毫無疑問會被阻塞。但是如果一個線程再次獲取他已經持有的鎖的時會不會阻塞呢?
舉個具體的例子:
/**
* 測試可重入鎖
*/
public class ReSyncDemo {
public synchronized void m1(){
System.out.println(" I am M1");
try {
//調用m2
m2();
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void m2(){
System.out.println("I am M2");
}
public static void main(String[] args) {
ReSyncDemo reSync = new ReSyncDemo();
new Thread(() -> {
reSync.m1();
}).start();
new Thread(() -> {
reSync.m2();
}).start();
}
}
輸出結果
I am M1
I am M2
I am M2
運行上面的代碼,可以觀察到 第一個 I am M1和M2幾乎是同時輸出的。而第二個I am M2卻是在兩秒后輸出的。這一點就可以印證synchronized是可重入鎖。因為我們知道,synchronized鎖的是對象,當線程1進入m1方法的時候。運行第一個打印,當他運行到調用m2方法的時候,發現是m2是需要持有鎖才能訪問,但是這個鎖已經被自己持有了,就是當前對象的鎖。於是可以直接調用。調用完畢之后然后休眠兩秒。等待程序結束,線程2才可以去執行。
關於可重入鎖的原理其實是這樣的,在鎖內部維護一個線程的標志,用來標識該鎖是被哪個線程持有的。然后關聯一個計數器,當計數器為0的時候,代表該鎖沒有被任何線程占用。當一個線程獲取了鎖的時候,計數器會變成1.然后其他線程再來的時候,會發現這個鎖已經被其他線程持有了,並且比較這個鎖不是自己持有的,於是阻塞掛起。
但是獲取了該鎖的線程,再次訪問同步方法的時候,例如上面的m1調用m2,跟線程標志比較一下發現這個鎖的擁有者是自己。於是就可以直接進入,然后把count+1,釋放之后-1,直到計數器為0的時候,代表線程不管重入了多少次,現在都已經全部釋放了。然后把線程的標識置為null,然后其他被阻塞的線程就會來搶這個鎖
自旋鎖
關於自旋鎖,關於自旋鎖其實很多地方都用到了。CAS就是一種自旋鎖,在synchronized的鎖升級過程,AQS中也用到了自旋鎖。在很多鎖中,一個線程獲取鎖失敗后,一般都會被阻塞而被掛起。等到線程獲取鎖的時候,又要把線程喚醒。這種反復的切換開銷比較大,於是就出現了自旋鎖,自旋鎖嚴格意義上來說不是鎖,或者說是一種非阻塞的“鎖”,自旋鎖的過程是這樣的,當前線程獲取鎖的時候,如果發現這個鎖被其他的線程占有,在不放棄cpu使用權的情況下,多次嘗試獲取(默認是十次,可以更改)。自旋鎖認為,自己在這十次獲取的過程中,其他線程已經釋放了鎖。如果指定的次數還沒有獲取到,當前線程才會被阻塞掛起。所以自旋鎖是一種用cpu時間換線程阻塞和調度的開銷。但是造成的問題是,如果指定的次數還沒有獲取到,這些cpu時間可能會被白白浪費,所以要根據實際情況使用。