Java并發(fā)編程(三)---synchonized解決原子性問題
前言
上一篇我們介紹Java內(nèi)存模型來處理有序性,可見性的問題。但是,還有一個原子性的問題,沒有處理,那么針對原子性的問題我們該怎么處理呢?我們知道在并發(fā)編程中的原子性問題主要原因就是,一條高級語句可能會被分成多個CPU指令,在指令執(zhí)行完之后發(fā)生了線程切換,中間狀態(tài)被暴露造成原子性問題。
鎖
現(xiàn)實生活中,我們用自己的鎖來保護自己的財產(chǎn),買門票來鎖定演唱會的座位。
同理,在并發(fā)編程的世界里我們同樣可以引入鎖的概念來鎖住需要保護的資源。只有獲得了鎖的線程才能操作資源。
synchronized
Java自帶的鎖工具是synchronized,用synchronized修飾的代碼就相當于上了鎖。上了鎖就需要互斥執(zhí)行。即:同一時刻只能有一個線程執(zhí)行。
我們將一段需要互斥執(zhí)行的代碼稱之為臨界區(qū)。
例如:
synchronized (this) {
this.a = 100;
System.out.println("*******test2執(zhí)行");
}
如上程序:代碼System.out.println("*******test2執(zhí)行");被synchronized修飾,故此代碼被稱之為臨界區(qū)。而a這是受保護的資源。其關(guān)系圖如下:
synchronized的運用
synchronized 可以修改方法,修飾代碼塊。使用如下:
class SynchronizedTest {
public synchronized void test1() {
System.out.println("*******test1執(zhí)行");
}
public void test2() {
synchronized (this) {
System.out.println("*******test2執(zhí)行");
}
}
public synchronized static void test3() {
System.out.println("*******test3執(zhí)行");
}
}
當synchronized修飾實例方法時,鎖定的就是當前實例對象this。如方法test1所示。
當synchronized修飾代碼塊塊時,鎖定的就是括號里的對象。如方法test2所示。
當synchronized修飾靜態(tài)方法時,鎖定的就是當前類的class對象,如方法test3所示。
鎖與受保護資源的關(guān)系
在現(xiàn)實生活中,我們可以通過通過一把鎖保護多個東西,例如,用一把大門的鎖,保護你家里面的所有東西。同樣的,你一個給一個東西加上兩把鎖。但是,在并發(fā)編程中,同一個資源只能由一把鎖保護,一把鎖可以保護多個資源。故,并發(fā)編程中,鎖與受保護資源的關(guān)系是1:N。例如:
public class SynchronizedTest3 {
int a = 0;
int b = 0;
static int c = 0;
/**
* 鎖定的是this對象,保護了,a,b兩個資源
*/
synchronized void setValue() {
a = 100;
b = 20;
}
/**
* 鎖定的是SynchronizedTest3的class對象,
* 保護了資源c
* @return
*/
synchronized static int getValue() {
return SynchronizedTest3.c;
}
}
如上程序,synchronized修飾的setValue方法中有a,b兩個資源,因為這兩個資源都所屬與this對象,所以都可以受到synchronized的保護。
而synchronized修飾的getValue方法中只有資源c,而這個c是一個靜態(tài)變量,屬于SynchronizedTest3類,所以它也可以受到保護。
鎖如何保護多個資源
多個資源沒關(guān)聯(lián)
如果多個資源沒有關(guān)聯(lián)的話,我們可以用多個不同的鎖來保護,例如:張三的東西用張三的鎖,李四的東西用李四的鎖。井水不犯河水。例如:
public class Account {
//取款保護鎖
private final Object balLock = new Object();
//密碼保護鎖
private final Object pwdLock = new Object();
private Integer balance = 100;
private String password = null;
/**
* 取款
*/
public void withdrow(Integer amt) {
synchronized (balLock) {
if (balance > amt) {
balance -= amt;
System.out.println("*******扣除后的余額是="+balance);
}
}
}
/**
* 查看余額
* @return
*/
public int getSBalance() {
synchronized (balLock) {
System.out.println("******讀取到的余額是="+balance);
return balance;
}
}
/**
* 更改密碼
* @param newPwd
*/
public void updatePwd(String newPwd) {
synchronized (pwdLock) {
password = newPwd;
}
}
/**
* 查看密碼
* @return
*/
public String getNewPwd() {
synchronized (pwdLock) {
return password;
}
}
public int getBalance() {
return balance;
}
public void setBalance(int balance) {
this.balance = balance;
}
}
如上程序,Account類下面有余額和密碼兩個資源,我們分別創(chuàng)建了取款的保護鎖balLock和密碼修改的保護鎖pwdLock。同時我們可以需要注意的是
相同的資源要用相同的鎖,例如取款用的balLock鎖,那么查看余額也需要用balLock鎖。修改密碼用的pwdLock鎖,那么查看密碼也需要用pwdLock鎖。只要這樣才能保證數(shù)據(jù)的正確性。在本例中,我們也可以用this鎖來保護,但是這樣的話,修改密碼和取款就不能分開。用兩個不同的鎖,可以使得取款和修改密碼可以并行。
2. 多個資源有關(guān)聯(lián)
現(xiàn)實中,有很多資源是有關(guān)聯(lián)關(guān)系的,例如:轉(zhuǎn)賬操作:張三給李四轉(zhuǎn)賬100元,那么張三的賬戶余額和李四的賬戶余額就是有關(guān)聯(lián)關(guān)系的兩個資源。那么:我們該如何加鎖保護這兩個資源使得轉(zhuǎn)賬沒問題呢?
我們先直接用synchronized修飾轉(zhuǎn)賬方法 如下:
synchronized void transfer(Account target, int amt) {
if (balance > amt) {
this.balance = balance - amt;
target.setBalance(target.getBalance() + amt);
}
}
通過前面的描述,我們知道synchronized鎖定的對象是this,那么該鎖肯定能保護資源this.balance。但是對于資源target.balance卻不能保護,因為該資源屬于target對象。
故此種方法不可行。
我們來具體分析下:
假如有賬戶A(有100元),
賬戶B(有100元),賬戶C(有100元),三個賬戶,賬戶A給賬戶B轉(zhuǎn)50元,賬戶B給賬戶C轉(zhuǎn)50元。所以我們期望的結(jié)果是賬戶A
剩下50元,賬戶B剩下100元,賬戶C剩下150元。但是按照我們上述加鎖方式,一定會是在這樣么?
我們假設(shè)線程1執(zhí)行賬戶A轉(zhuǎn)賬戶B50元,線程2執(zhí)行賬戶B給賬戶C50元,那么這兩個線程運行在CPU1和CPU2,顯然CPU1和CPU2是不互斥的。
線程1獲取到的鎖是A.this。線程2獲取到鎖是B.this。如果線程1和線程2同時進入臨界區(qū),那么讀取到的賬戶B余額都是100元,如果線程1先于線程2執(zhí)行完,那么賬戶B的余額是50。如果線程2先于線程1執(zhí)行完那么賬戶B的余額是150元。就是不可能是100元。
上面的原因就是因為鎖沒有覆蓋到所有的應(yīng)受保護資源。那么該如何處理呢?我們可以通過一個共享對象來處理保護資源。 例如:Accout.class對象,這個對象是所有的余額所共享的,所以能夠覆蓋this.balance資源和
target.balance資源。
void transfer3(Account target, int amt) {
synchronized (Account.class) {
if (balance > amt) {
this.balance = balance - amt;
target.setBalance(target.getBalance() + amt);
}
}
}
不過在實際項目中,我們都是通過數(shù)據(jù)庫事務(wù)+數(shù)據(jù)庫樂觀鎖來處理轉(zhuǎn)賬邏輯的。
總結(jié)
本文簡單的介紹了鎖模型以及synchronized的用法和,鎖與資源的關(guān)系,最后介紹了鎖如何保護多個資源,總結(jié)一下就是 鎖與資源的關(guān)系是 1:N,多個沒有關(guān)聯(lián)的資源用多個不同的鎖進行保護,有關(guān)聯(lián)關(guān)系的資源,用共享鎖進行保護。
作者:碼農(nóng)飛哥
微信公眾號:碼農(nóng)飛哥