Java併發程式設計實戰 03互斥鎖 解決原子性問題
阿新 • • 發佈:2020-05-07
# 文章系列
[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