在并發(fā)編程BUG源頭文章中,我們初識了并發(fā)編程的三個bug源頭:可見性、原子性、有序性。在如何解決可見性和原子性文章中我們大致了解了可見性和有序性的解決思路,今天輪到最后一個大bug,那就是原子性。
知識回顧

鎖模型


JAVA中的鎖模型
鎖是一種通用的技術(shù)方案,Java 語言提供的 synchronized 關(guān)鍵字,就是鎖的一種實現(xiàn)。
- synchronized 是獨占鎖/排他鎖(就是有你沒我的意思),但是注意!synchronized并不能改變CPU時間片切換的特點,只是當(dāng)其他線程要訪問這個資源時,發(fā)現(xiàn)鎖還未釋放,所以只能在外面等待。
- synchronized一定能保證原子性,因為被 synchronized 修飾某段代碼后,無論是單核 CPU 還是多核 CPU,只有一個線程能夠執(zhí)行該代碼,所以一定能保證原子操作
- synchronized也能夠保證可見性和有序性。根據(jù)前第二篇文章:Happens-Before 規(guī)則之管程中鎖的規(guī)則:對一個鎖的解鎖 Happens-Before 于后續(xù)對這個鎖的加鎖。即前一個線程的解鎖操作對后一個線程的加鎖操作可見。綜合 Happens-Before 的傳遞性原則,我們就能得出前一個線程在臨界區(qū)修改的共享變量(該操作在解鎖之前),對后續(xù)進入臨界區(qū)(該操作在加鎖之后)的線程是可見的。- synchronized 關(guān)鍵字可以用來修飾靜態(tài)方法,非靜態(tài)方法,也可以用來修飾代碼塊
理論說完了,來點實際的吧!首先我們用synchronized 修飾非靜態(tài)方法來改寫第一章中原子性問題的那段代碼:
private long count = 0;
// 修飾非靜態(tài)方法 當(dāng)修飾非靜態(tài)方法的時候,鎖定的是當(dāng)前實例對象 this。
// 當(dāng)該類中有多個普通方法被Synchronized修飾(同步),那么這些方法的鎖都是這個類的一個對象this。多個線程訪問這些方法時,如果這些線程調(diào)用方法時使用的是同一個該類的對象,雖然他們訪問不同方法,但是他們使用同一個對象來調(diào)用,那么這些方法的鎖就是一樣的,就是這個對象,那么會造成阻塞。如果多個線程通過不同的對象來調(diào)用方法,那么他們的鎖就是不一樣的,不會造成阻塞。
private synchronized void add10K(){
int start = 0;
while (start ++ < 10000){
this.count ++;
}
}
public static void main(String[] args) throws InterruptedException {
TestSynchronized2 test = new TestSynchronized2();
// 創(chuàng)建兩個線程,執(zhí)行 add() 操作
Thread th1 = new Thread(()->{
test.add10K();
});
Thread th2 = new Thread(()->{
test.add10K();
});
// 啟動兩個線程
th1.start();th2.start();
// 等待兩個線程執(zhí)行結(jié)束
th1.join();th2.join();
System.out.println(test.count);
}
運行一下吧!你會發(fā)現(xiàn)永遠(yuǎn)都可以達到我們想要的效果了~
除了上面代碼中修飾非靜態(tài)方法,還可以修飾靜態(tài)方法和代碼塊
// 修飾靜態(tài)方法 當(dāng)修飾靜態(tài)方法的時候,鎖定的是當(dāng)前類的 Class 對象,即TestSynchronized2.class 。這個范圍就比對象鎖大。這里就算是不同對象,但是只要是該類的對象,就使用的是同一把鎖。
synchronized static void bar() {
// 臨界區(qū)
}
// 修飾代碼塊 java中經(jīng)典的雙重鎖檢查機制
private volatile static TestSynchronized2 instance;
public static TestSynchronized2 getInstance() {
if (instance == null) {
synchronized (TestSynchronized2.class) {
if (instance == null) {
instance = new TestSynchronized2();
}
}
}
return instance;
}
明確鎖和資源的關(guān)系
深入分析鎖定的對象和受保護資源的關(guān)系,綜合考慮受保護資源的訪問路徑,多方面考量才能用好互斥鎖。受保護資源和鎖之間的關(guān)聯(lián)關(guān)系是 N:1 的關(guān)系。如果一個資源用N個鎖,那肯定出問題的,就好像一個廁所坑位,你有10把鑰匙,那不是可以10個人同時進了?
現(xiàn)在給出兩段錯誤代碼,想一想到底為啥錯了吧?
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修飾普通方法 鎖定的是當(dāng)前實例對象 this 而修飾靜態(tài)方法 鎖定的是當(dāng)前類的 Class 對象
所以這里有兩把鎖 分別是 this 和 TestSynchronized3.class
由于臨界區(qū) get() 和 addOne() 是用兩個鎖保護的,因此這兩個臨界區(qū)沒有互斥關(guān)系,臨界區(qū) addOne() 對 value 的修改對臨界區(qū) get() 也沒有可見性保證,這就導(dǎo)致并發(fā)問題了。
第二段錯誤原因:
加鎖本質(zhì)就是在鎖對象的對象頭中寫入當(dāng)前線程id,但是synchronized (new Object())每次在內(nèi)存中都是新對象,所以加鎖無效。
問:剛剛的例子都是多個鎖保護一個資源,這樣百分百是不行的。那么一個鎖保護多個資源,就一定可以了嗎?
答:如果多個資源彼此之間是沒有關(guān)聯(lián)的,那可以用一個鎖來保護。如果有關(guān)聯(lián)的話,那是不行的。比如說銀行轉(zhuǎn)賬操作,你給我轉(zhuǎn)賬,我賬戶多100,你賬戶少100,我不能用我的鎖來保護你,就像現(xiàn)實生活中我的鎖是不能保護你的財產(chǎn)的。
劃重點!要區(qū)分多個資源是否有關(guān)聯(lián)!但是一個鎖保護多個沒關(guān)聯(lián)的資源,未免性能太差了哦,比如我聽歌和玩游戲可以同時進行,你非得讓我做完一個再做另一個,豈不是要雙倍時間。所以即使一個鎖可以保護多個沒關(guān)聯(lián)的資源,但是一般而已,會各自用不同的鎖,能夠提升性能。這種鎖還有個名字,叫細(xì)粒度鎖。
問:剛剛說到銀行轉(zhuǎn)賬的案例,那么假如某天在某銀行同時發(fā)生這樣一個事,柜員小王需要完成A賬戶給B賬戶轉(zhuǎn)賬100元,柜員小李需要完成B賬戶給A賬戶轉(zhuǎn)賬100元,請問如何實現(xiàn)呢?
答:其實用兩把鎖就實現(xiàn)了,轉(zhuǎn)出一把,轉(zhuǎn)入另一把。只有當(dāng)兩者都成功時,才執(zhí)行轉(zhuǎn)賬操作。
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);
}
}
}
}
至此,恭喜你,一波問題解決了,可是遺憾的告訴你:又導(dǎo)致了另一個bug。這段代碼是有可能發(fā)生死鎖的!并發(fā)編程中要注意的東西可真是多喲。咱們先把死鎖這個名詞記?。〕掷m(xù)關(guān)注【胖滾豬學(xué)編程】公眾號!在我們后面的文章中找答案!
如何保證原子性
現(xiàn)在我們已經(jīng)知道互斥鎖可以保證原子性,也知道了如何使用synchronized來保證原子性。但synchronized 并不是JAVA中唯一能保證原子性的方案。
如果你粗略的看一下J.U.C(java.util.concurrent包),那么你可以很顯眼的發(fā)現(xiàn)它倆:

一個是lock包,一個是atomic包,只要你英語過了四級。。我相信你都可以馬上斷定,它們可以解決原子性問題。
由于這兩個包比較重要,所以會放在后面的模塊單獨說,持續(xù)關(guān)注【胖滾豬學(xué)編程】公眾號吧!
本文轉(zhuǎn)載自公眾號【胖滾豬學(xué)編程】 用漫畫讓編程so easy and interesting!歡迎關(guān)注!形象來源于微信表情包【胖滾家族】喜歡可以下載哦~