原創聲明:本文轉載自公眾號【胖滾豬學編程】,轉載務必註明出處! 在 "併發編程BUG源頭" 文章中,我們初識了併發編程的三個bug源頭:可見性、原子性、有序性。在 "如何解決可見性和原子性" 文章中我們大致瞭解了可見性和有序性的解決思路,今天輪到最後一個大bug,那就是原子性。 知識回顧 鎖模型 J ...
原創聲明:本文轉載自公眾號【胖滾豬學編程】,轉載務必註明出處!
在併發編程BUG源頭文章中,我們初識了併發編程的三個bug源頭:可見性、原子性、有序性。在如何解決可見性和原子性文章中我們大致瞭解了可見性和有序性的解決思路,今天輪到最後一個大bug,那就是原子性。
知識回顧
鎖模型
JAVA中的鎖模型
鎖是一種通用的技術方案,Java 語言提供的 synchronized 關鍵字,就是鎖的一種實現。
- synchronized 是獨占鎖/排他鎖(就是有你沒我的意思),但是註意!synchronized並不能改變CPU時間片切換的特點,只是當其他線程要訪問這個資源時,發現鎖還未釋放,所以只能在外面等待。
- synchronized一定能保證原子性,因為被 synchronized 修飾某段代碼後,無論是單核 CPU 還是多核 CPU,只有一個線程能夠執行該代碼,所以一定能保證原子操作
- synchronized也能夠保證可見性和有序性。根據前第二篇文章:Happens-Before 規則之管程中鎖的規則:對一個鎖的解鎖 Happens-Before 於後續對這個鎖的加鎖。即前一個線程的解鎖操作對後一個線程的加鎖操作可見。綜合 Happens-Before 的傳遞性原則,我們就能得出前一個線程在臨界區修改的共用變數(該操作在解鎖之前),對後續進入臨界區(該操作在加鎖之後)的線程是可見的。- synchronized 關鍵字可以用來修飾靜態方法,非靜態方法,也可以用來修飾代碼塊
理論說完了,來點實際的吧!首先我們用synchronized 修飾非靜態方法來改寫第一章中原子性問題的那段代碼:
private long count = 0;
// 修飾非靜態方法 當修飾非靜態方法的時候,鎖定的是當前實例對象 this。
// 當該類中有多個普通方法被Synchronized修飾(同步),那麼這些方法的鎖都是這個類的一個對象this。多個線程訪問這些方法時,如果這些線程調用方法時使用的是同一個該類的對象,雖然他們訪問不同方法,但是他們使用同一個對象來調用,那麼這些方法的鎖就是一樣的,就是這個對象,那麼會造成阻塞。如果多個線程通過不同的對象來調用方法,那麼他們的鎖就是不一樣的,不會造成阻塞。
private synchronized void add10K(){
int start = 0;
while (start ++ < 10000){
this.count ++;
}
}
public static void main(String[] args) throws InterruptedException {
TestSynchronized2 test = new TestSynchronized2();
// 創建兩個線程,執行 add() 操作
Thread th1 = new Thread(()->{
test.add10K();
});
Thread th2 = new Thread(()->{
test.add10K();
});
// 啟動兩個線程
th1.start();th2.start();
// 等待兩個線程執行結束
th1.join();th2.join();
System.out.println(test.count);
}
運行一下吧!你會發現永遠都可以達到我們想要的效果了~
除了上面代碼中修飾非靜態方法,還可以修飾靜態方法和代碼塊
// 修飾靜態方法 當修飾靜態方法的時候,鎖定的是當前類的 Class 對象,即TestSynchronized2.class 。這個範圍就比對象鎖大。這裡就算是不同對象,但是只要是該類的對象,就使用的是同一把鎖。
synchronized static void bar() {
// 臨界區
}
// 修飾代碼塊 java中經典的雙重鎖檢查機制
private volatile static TestSynchronized2 instance;
public static TestSynchronized2 getInstance() {
if (instance == null) {
synchronized (TestSynchronized2.class) {
if (instance == null) {
instance = new TestSynchronized2();
}
}
}
return instance;
}
明確鎖和資源的關係
深入分析鎖定的對象和受保護資源的關係,綜合考慮受保護資源的訪問路徑,多方面考量才能用好互斥鎖。受保護資源和鎖之間的關聯關係是 N:1 的關係。如果一個資源用N個鎖,那肯定出問題的,就好像一個廁所坑位,你有10把鑰匙,那不是可以10個人同時進了?
現在給出兩段錯誤代碼,想一想到底為啥錯了吧?
static long value1 = 0L;
synchronized long get1() {
return value1;
}
synchronized static void addOne1() {
value1 += 1;
}
long value = 0L;
long get() {
synchronized (new Object()) {
return value;
}
}
第一段錯誤原因:
因為我們說過synchronized修飾普通方法 鎖定的是當前實例對象 this 而修飾靜態方法 鎖定的是當前類的 Class 對象
所以這裡有兩把鎖 分別是 this 和 TestSynchronized3.class
由於臨界區 get() 和 addOne() 是用兩個鎖保護的,因此這兩個臨界區沒有互斥關係,臨界區 addOne() 對 value 的修改對臨界區 get() 也沒有可見性保證,這就導致併發問題了。
第二段錯誤原因:
加鎖本質就是在鎖對象的對象頭中寫入當前線程id,但是synchronized (new Object())每次在記憶體中都是新對象,所以加鎖無效。
問:剛剛的例子都是多個鎖保護一個資源,這樣百分百是不行的。那麼一個鎖保護多個資源,就一定可以了嗎?
答:如果多個資源彼此之間是沒有關聯的,那可以用一個鎖來保護。如果有關聯的話,那是不行的。比如說銀行轉賬操作,你給我轉賬,我賬戶多100,你賬戶少100,我不能用我的鎖來保護你,就像現實生活中我的鎖是不能保護你的財產的。
劃重點!要區分多個資源是否有關聯!但是一個鎖保護多個沒關聯的資源,未免性能太差了哦,比如我聽歌和玩游戲可以同時進行,你非得讓我做完一個再做另一個,豈不是要雙倍時間。所以即使一個鎖可以保護多個沒關聯的資源,但是一般而已,會各自用不同的鎖,能夠提升性能。這種鎖還有個名字,叫細粒度鎖。
問:剛剛說到銀行轉賬的案例,那麼假如某天在某銀行同時發生這樣一個事,櫃員小王需要完成A賬戶給B賬戶轉賬100元,櫃員小李需要完成B賬戶給A賬戶轉賬100元,請問如何實現呢?
答:其實用兩把鎖就實現了,轉出一把,轉入另一把。只有當兩者都成功時,才執行轉賬操作。
public static void main(String[] args) throws InterruptedException {
Account a = new Account(200); //A的初始賬戶餘額200
Account b = new Account(300); //B的初始賬戶餘額200
Thread threadA = new Thread(()->{
try {
transfer(a,b,100);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread threadB = new Thread(()->{
try {
transfer(b,a,100);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
threadA.start();
threadB.start();
}
static void transfer(Account source,Account target, int amt) throws InterruptedException {
synchronized (source) {
log.info("持有鎖{} 等待鎖{}",source,target);
synchronized (target) {
if (source.getBalance() > amt) {
source.setBalance(source.getBalance() - amt);
target.setBalance(target.getBalance() + amt);
}
}
}
}
至此,恭喜你,一波問題解決了,可是遺憾的告訴你:又導致了另一個bug。這段代碼是有可能發生死鎖的!併發編程中要註意的東西可真是多喲。咱們先把死鎖這個名詞記住!持續關註【胖滾豬學編程】公眾號!在我們後面的文章中找答案!
如何保證原子性
現在我們已經知道互斥鎖可以保證原子性,也知道瞭如何使用synchronized來保證原子性。但synchronized 並不是JAVA中唯一能保證原子性的方案。
如果你粗略的看一下J.U.C(java.util.concurrent包),那麼你可以很顯眼的發現它倆:
一個是lock包,一個是atomic包,只要你英語過了四級。。我相信你都可以馬上斷定,它們可以解決原子性問題。
由於這兩個包比較重要,所以會放在後面的模塊單獨說,持續關註【胖滾豬學編程】公眾號吧!
本文轉載自公眾號【胖滾豬學編程】 用漫畫讓編程so easy and interesting!歡迎關註!形象來源於微信表情包【胖滾家族】喜歡可以下載哦~