Java併發編程實戰 01併發編程的Bug源頭
Java併發編程實戰 02Java如何解決可見性和有序性問題java
在上一篇文章02Java如何解決可見性和有序性問題當中,咱們解決了可見性和有序性的問題,那麼還有一個原子性
問題我們還沒解決。在第一篇文章01併發編程的Bug源頭當中,講到了把一個或者多個操做在 CPU 執行的過程當中不被中斷的特性稱爲原子性,那麼原子性的問題該如何解決。編程
同一時刻只有一個線程執行這個條件很是重要,咱們稱爲互斥,若是能保護對共享變量的修改時互斥的,那麼就能保住原子性。緩存
咱們把一段須要互斥執行的代碼稱爲臨界區,線程進入臨界區以前,首先嚐試獲取加鎖,若加鎖成功則能夠進入臨界區執行代碼,不然就等待,直到持有鎖的線程執行了解鎖unlock()
操做。以下圖:
微信
可是有兩個點要咱們理解清楚:咱們的鎖是什麼?要保護的又是什麼?併發
在併發編程世界中,鎖和鎖要保護的資源是有對應關係的。
首先咱們須要把臨界區要保護的資源R
標記出來,而後須要建立一把該資源的鎖LR
,最後針對這把鎖,咱們須要在進出臨界區時添加加鎖lock(LR)
操做和解鎖unlock(LR)
操做。以下:
app
synchronized
可修飾方法和代碼塊。加鎖lock()
和解鎖unlock()
都會在synchronized
修飾的方法或代碼塊先後自動加上加鎖lock()
和解鎖unlock()
操做。這樣作的好處就是加鎖和解鎖操做會成對出現,畢竟忘了執行解鎖unlock()
操做但是會讓其餘線程死等下去。
那咱們怎麼去鎖住須要保護的資源呢?在下面的代碼中,add1()
非靜態方法鎖定的是this
對象(當前實例對象),add2()
靜態方法鎖定的是X.class
(當前類的Class對象)性能
public class X { public synchronized void add1() { // 臨界區 } public synchronized static void add2() { // 臨界區 } }
上面的代碼能夠理解爲這樣:this
public class X { public synchronized(this) void add() { // 臨界區 } public synchronized(X.class) static void add2() { // 臨界區 } }
在01 併發編程的Bug源頭文章當中,咱們提到過count += 1 存在的併發問題,如今咱們嘗試使用synchronized
解決該問題。線程
public class Calc { private int value = 0; public synchronized int get() { return value; } public synchronized void addOne() { value += 1; } }
addOne()
方法被synchronized
修飾後,只有一個線程能執行,因此必定能保證原子性,那麼可見性問題呢?在上一篇文章02 Java如何解決可見性和有序性問題當中,提到了管程中的鎖規則,一個鎖的解鎖 Happens-Before 於後續對這個鎖的加鎖。管程,在這裏就是synchronized
(管程的在後續的文章中介紹)。根據這個規則,前一個線程執行了value += 1
操做是對後續線程可見的。而查看get()
方法也必須加上synchronized
修飾,不然也無法保證其可見性。
上面這個例子以下圖:
code
那麼可使用多個鎖保護一個資源嗎,修改一下上面的例子後,get()
方法使用this
對象鎖來保護資源value
,addOne()
方法使用Calc.class
類對象來保護資源value
,代碼以下:
public class Calc { private static int value = 0; public synchronized int get() { return value; } public static synchronized void addOne() { value += 1; } }
上面的例子用圖來表示:
在這個例子當中,get()
方法使用的是this
鎖,addOne()
方法使用的是Calc.class
鎖,所以這兩個臨界區(方法)並無互斥性,addOne()
方法的修改對get()
方法是不可見的,因此就會致使併發問題。
結論:不可以使用多把鎖保護一個資源,但能使用一把鎖保護多個資源(這裏沒寫例子,只寫了一把鎖保護一個資源)
在銀行的業務當中,修改密碼和取款是兩個再常常不過的操做了,修改密碼操做和取款操做是沒有關聯關係的,沒有關聯關係的資源咱們可使用不一樣的互斥鎖來解決併發問題。代碼以下:
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元,那麼這兩個帳戶就是有關聯關係的。在沒有理解互斥鎖以前,寫出的代碼可能以下:
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
鎖才能去進行轉帳操做,正確的代碼以下:
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互斥鎖(上)
極客時間:Java併發編程實戰 04互斥鎖(下)
我的博客網址: https://colablog.cn/
若是個人文章幫助到您,能夠關注個人微信公衆號,第一時間分享文章給您