1. 程式人生 > >Java併發程式設計實戰 03互斥鎖 解決原子性問題

Java併發程式設計實戰 03互斥鎖 解決原子性問題

# 文章系列 [Java併發程式設計實戰 01併發程式設計的Bug源頭](https://mp.weixin.qq.com/s/QT44HS47l_ir08pCZeFU5Q) [Java併發程式設計實戰 02Java如何解決可見性和有序性問題](https://mp.weixin.qq.com/s/Ryud9nizdqWI25CMLL3E_g) # 摘要 在上一篇文章[02Java如何解決可見性和有序性問題](https://mp.weixin.qq.com/s/Ryud9nizdqWI25CMLL3E_g)當中,我們解決了可見性和有序性的問題,那麼還有一個`原子性`問題咱們還沒解決。在第一篇文章[01併發程式設計的Bug源頭](https://mp.weixin.qq.com/s/QT44HS47l_ir08pCZeFU5Q)當中,講到了**把一個或者多個操作在 CPU 執行的過程中不被中斷的特性稱為原子性**,那麼原子性的問題該如何解決。 **同一時刻只有一個執行緒執行**這個條件非常重要,我們稱為**互斥**,如果能保護對共享變數的修改時互斥的,那麼就能保住原子性。 # 簡易鎖 我們把一段需要互斥執行的程式碼稱為臨界區,執行緒進入臨界區之前,首先嚐試獲取加鎖,若加鎖成功則可以進入臨界區執行程式碼,否則就等待,直到持有鎖的執行緒執行了解鎖`unlock()`操作。如下圖: ![互斥鎖1.jpg](http://qiniuyun.colablog.cn/0d5c549f-ba67-4cc1-bc92-a7c6dff01743.jpg) 但是有兩個點要我們理解清楚:**我們的鎖是什麼?要保護的又是什麼?** # 改進後的鎖模型 在併發程式設計世界中,鎖和鎖要保護的資源是有對應關係的。 首先我們需要把臨界區要保護的資源`R`標記出來,然後需要建立一把該資源的鎖`LR`,最後針對這把鎖,我們需要在進出臨界區時新增加鎖`lock(LR)`操作和解鎖`unlock(LR)`操作。如下: ![互斥鎖2.jpg](http://qiniuyun.colablog.cn/d1f11c56-82cd-416a-bb6c-35603efa3e65.jpg) ## Java語言提供的鎖技術:synchronized `synchronized`可修飾方法和程式碼塊。加鎖`lock()`和解鎖`unlock()`都會在`synchronized`修飾的方法或程式碼塊前後自動加上加鎖`lock()`和解鎖`unlock()`操作。這樣做的好處就是加鎖和解鎖操作會成對出現,畢竟忘了執行解鎖`unlock()`操作可是會讓其他執行緒死等下去。 那我們怎麼去鎖住需要保護的資源呢?在下面的程式碼中,`add1()`非靜態方法鎖定的是`this`物件(當前例項物件),`add2()`靜態方法鎖定的是`X.class`(當前類的Class物件) ```java public class X { public synchronized void add1() { // 臨界區 } public synchronized static void add2() { // 臨界區 } } ``` 上面的程式碼可以理解為這樣: ```java public class X { public synchronized(this) void add() { // 臨界區 } public synchronized(X.class) static void add2() { // 臨界區 } } ``` # 使用synchronized 解決 count += 1 問題 在[01 併發程式設計的Bug源頭](https://mp.weixin.qq.com/s/QT44HS47l_ir08pCZeFU5Q)文章當中,我們提到過count += 1 存在的併發問題,現在我們嘗試使用`synchronized`解決該問題。 ```java public class Calc { private int value = 0; public synchronized int get() { return value; } public synchronized void addOne() { value += 1; } } ``` `addOne()`方法被`synchronized`修飾後,只有一個執行緒能執行,所以一定能保證原子性,那麼可見性問題呢?在上一篇文章[02 Java如何解決可見性和有序性問題](https://mp.weixin.qq.com/s/Ryud9nizdqWI25CMLL3E_g)當中,提到了**管程中的鎖規則**,一個鎖的解鎖 Happens-Before 於後續對這個鎖的加鎖。管程,在這裡就是`synchronized`(管程的在後續的文章中介紹)。根據這個規則,前一個執行緒執行了`value += 1`操作是對後續執行緒可見的。而檢視`get()`方法也必須加上`synchronized`修飾,否則也沒法保證其可見性。 上面這個例子如下圖: ![互斥鎖3.jpg](http://qiniuyun.colablog.cn/eb9e54d0-b045-4adb-9f1c-e3e3874056bc.jpg) 那麼可以使用多個鎖保護一個資源嗎,修改一下上面的例子後,`get()`方法使用`this`物件鎖來保護資源`value`,`addOne()`方法使用`Calc.class`類物件來保護資源`value`,程式碼如下: ```java public class Calc { private static int value = 0; public synchronized int get() { return value; } public static synchronized void addOne() { value += 1; } } ``` 上面的例子用圖來表示: ![互斥鎖4.jpg](http://qiniuyun.colablog.cn/cd2573bd-ed93-4392-a406-5d4e0c8c4237.jpg) 在這個例子當中,`get()`方法使用的是`this`鎖,`addOne()`方法使用的是`Calc.class`鎖,因此這兩個臨界區(方法)並沒有互斥性,`addOne()`方法的修改對`get()`方法是不可見的,所以就會導致併發問題。 **結論:不可使用多把鎖保護一個資源,但能使用一把鎖保護多個資源**(這裡沒寫例子,只寫了一把鎖保護一個資源) # 保護沒有關聯關係的多個資源 在銀行的業務當中,修改密碼和取款是兩個再經常不過的操作了,修改密碼操作和取款操作是沒有關聯關係的,沒有關聯關係的資源我們可以使用不同的互斥鎖來解決併發問題。程式碼如下: ```java public class Account { // 保護密碼的鎖 private final Object pwLock = new Object(); // 密碼 private String password; // 保護餘額的鎖 private final Object moneyLock = new Object(); // 餘額 private Long money; public void updatePassword(String password) { synchronized (pwLock) { // 修改密碼 } } public void withdrawals(Long money) { synchronized (moneyLock) { // 取款 } } } ``` 分別使用`pwLock`和`moneyLock`來保護密碼和餘額,這樣修改密碼和修改餘額就可以並行了。**使用不同的鎖對受保護的資源進行進行更細化管理,能夠提升效能,這種鎖叫做細粒度鎖。** 在這個例子當中,你可能發現我使用了`final Object`來當成一把鎖,這裡解釋一下:**使用鎖必須是不可變物件,若把可變物件作為鎖,當可變物件被修改時相當於換鎖**,而且使用`Long`或`Integer`作為鎖時,在`-128到127`之間時,會使用快取,詳情可檢視他們的`valueOf()`方法。 # 保護有關聯關係的多個資源 在銀行業務當中,除了修改密碼和取款的操作比較多之外,還有一個操作比較多的功能就是轉賬。賬戶 A 轉賬給 賬戶B 100元,賬戶A的餘額減少100元,賬戶B的餘額增加100元,那麼這兩個賬戶就是有關聯關係的。在沒有理解互斥鎖之前,寫出的程式碼可能如下: ```java public class Account { // 餘額 private Long money; public synchronized void transfer(Account target, Long money) { this.money -= money; if (this.money < 0) { // throw exception } target.money += money; } } ``` 在轉賬`transfer`方法當中,鎖定的是`this`物件(使用者A),那麼這裡的目標使用者`target`(使用者B)的能被鎖定嗎?當然不能。這兩個物件是沒有關聯關係的。正確的操作應該是獲取`this`鎖和`target`鎖才能去進行轉賬操作,正確的程式碼如下: ```java public class Account { // 餘額 private Long money; public synchronized void transfer(Account target, Long money) { synchronized(this) { synchronized (target) { this.money -= money; if (this.money < 0) { // throw exception } target.money += money; } } } } ``` 在這個例子當中,**我們需要清晰的明白要保護的資源是什麼,只要我們的鎖能覆蓋所有受保護的資源就可以了**。 但是你以為這個例子很完美?那就錯了,這裡面很有可能會發生**死鎖**。你看出來了嗎?下一篇文章我就用這個例子來聊聊死鎖。 # 總結 使用互斥鎖最最重要的是:**我們的鎖是什麼?鎖要保護的資源是什麼?**,要理清楚這兩點就好下手了。而且鎖必須為**不可變物件**。使用不同的鎖保護不同的資源,可以細化管理,提升效能,稱為**細粒度鎖**。 參考文章: [極客時間:Java併發程式設計實戰 03互斥鎖(上)](https://time.geekbang.org/column/article/84344) [極客時間:Java併發程式設計實戰 04互斥鎖(下)](https://time.geekbang.org/column/article/84601) > 個人部落格網址: https://colablog.cn/ 如果我的文章幫助到您,可以關注我的微信公眾號,第一時間分享文章給您 ![微信公眾號](http://qiniuyun.colablog.cn/%E4%BA%8C%E7%BB%B4%E7%A0