鎖和synchronized
鎖的常見概念
- 互斥: 同一時刻只有一個執行緒執行
- 臨界區:一段需要互斥執行的程式碼
- 細粒度鎖: 用不同的鎖對受保護資源進行精細化管理。 細粒度鎖可以提高並行度,是效能優化的一個重要手段
- 死鎖 :一組互相競爭資源的執行緒因互相等待,導致“永久”阻塞的現象 。
用鎖的最佳實踐
- 永遠只再更新物件的成員變數時加鎖。
- 永遠只在訪問可變的成員變數時加鎖。
- 永遠不再呼叫其它物件的方法時加鎖。
- 減少所得持有時間,減小鎖的粒度。
同步與非同步
- 呼叫方法如果需要等待結果,就是同步;如果不需要等待結果就是非同步。
- 同步是Java程式碼預設的處理方式。
如何實現程式支援非同步:
- 非同步呼叫: 呼叫方建立一個子執行緒,再子執行緒中執行方法呼叫。
- 非同步方法: 被呼叫方;方法實現的時候,建立一個顯得執行緒執行主要邏輯,主執行緒直接return。
synchronized
class X{
//修飾非靜態方法
synchronized void foo(){
//臨界區
}
//修飾靜態方法
synchronized static void bar(){
//臨界區
}
//修飾程式碼塊
Object obj = new Object();
void baz(){
synchronized(obj){
//臨界區
}
}
}
Java編譯器會在synchronized修飾的方法或程式碼塊前後自動加上加鎖lock()和解鎖unlock(),這樣做的好處就是加鎖lock()和解鎖unlock()一定 是成對出現的,畢竟忘記解鎖unlock()可是個致命的Bug(意味著其他執行緒只能死等下去了)。
修飾靜態方法:
//修飾靜態方法是用當前類的位元組碼檔案作為鎖
class X{
//修飾靜態方法
synchronized(X.class) static void bar(){
//臨界區
}
}
修飾非靜態方法:
//修飾非靜態方法是用當前物件作為鎖
class X{
//修飾非靜態方法
synchronized(this) static void bar(){
//臨界區
}
}
如何用一把鎖保護多個資源
受保護資源和鎖之間合理的關聯關係應該是N:1的關係,也就是說可以用一把鎖來保護多個資源,但是不能用多把鎖來保護一個資源,
使用鎖的正確姿勢
依轉賬業務作為示例
示例一:
public class Account {
/**
*鎖:保護賬⼾餘額
*/
private final Object balLock = new Object();
/**
* 賬⼾餘額
*/
private Integer balance;
/**
* 錯誤的做法
* 非靜態方法的鎖是this,
* this這把鎖可以保護自己的餘額this.balance,保護不了別人的餘額 target.balance
*
*/
synchronized void transfer(Account target,int amt){
if (this.balance > amt) {
this.balance -= amt;
target.balance += amt;//這段程式碼會出現執行緒安全,要保證執行緒安全的話要使用同一個鎖
}
}
}
示例二:
public class Account {
/**
*鎖:保護賬⼾餘額
*/
private final Object balLock = new Object();
/**
* 賬⼾餘額
*/
private Integer balance;
/**
* 正確的做法,但是會導致整個轉賬系統的序列
*
* Account.class是所有Account物件共享的,
* 而且這個物件是Java虛擬機器在載入Account類的時候建立的,
* 所以我們不用擔心它的唯一性
*
* 這樣還有個弊端:所有的轉賬都是串行了
*/
void transfer2(Account target,int amt){
synchronized(Account.class){
if (this.balance > amt) {
this.balance -= amt;
target.balance += amt;
}
}
}
}
這樣的話轉賬操作就成了序列的了,正常的邏輯應該只鎖轉入賬號和被轉入賬戶;不影響其他的轉賬操作。稍作改造:
示例三:
public class Account {
/**
*鎖:保護賬⼾餘額
*/
private final Object lock;
/**
* 賬⼾餘額
*/
private Integer balance;
//私有化無參構造
private Account(){}
//設定一個傳遞lock的有參構造
private Account(Object lock){
this.lock = lock;
}
/**
* 轉賬
*/
void transfer(Account target,int amt){
//此處檢查所有物件共享鎖
synchronized(lock){
if (this.balance > amt) {
this.balance -= amt;
target.balance += amt;
}
}
}
}
這個方法雖然能夠解決問題,但是它要求建立Account物件的時候必須傳入同一個物件,
還有就是傳遞物件過於麻煩,寫法繁瑣缺乏可行性。
示例四:
public class Account {
/**
* 賬⼾餘額
*/
private Integer balance;
/**
* 轉賬
*/
void transfer(Account target,int amt){
//此處檢查所有物件共享鎖
synchronized(Account.class){
if (this.balance > amt) {
this.balance -= amt;
target.balance += amt;
}
}
}
}
用Account.class作為共享的鎖,鎖定的範圍太大。 Account.class是所有Account物件共享的,而且這個物件是Java虛擬機器在載入Account類的時候建立的,所以我們不用擔心它的唯一性。使用Account.class作為共享的鎖,我們就無需在建立Account物件時傳入了。
這樣新的問題就出來了雖然用Account.class作為互斥鎖,來解決銀行業務裡面的轉賬問題,雖然這個方案不存在 併發問題,但是所有賬戶的轉賬操作都是序列的,例如賬戶A轉賬戶B、賬戶C轉賬戶D這兩個轉賬操作現實 世界裡是可以並行的,但是在這個方案裡卻被序列化了,這樣的話,效能太差。所以如果考慮併發量這種方法也不行的
正確的寫法是這樣的(使用細粒度鎖):
示例五:
public class Account {
/**
* 賬⼾餘額
*/
private Integer balance;
/**
* 轉賬
*/
void transfer(Account target,int amt){
//鎖定轉出賬戶
synchronized(this){
//鎖住轉入賬戶
synchronized(target){
if (this.balance > amt) {
this.balance -= amt;
target.balance += amt;
}
}
}
}
}
我們試想在古代,沒有資訊化,賬戶的存在形式真的就是一個賬本,而且每個賬戶都有一個賬本,這些賬本 都統一存放在檔案架上。銀行櫃員在給我們做**轉賬時,要去檔案架上把轉出賬本和轉入賬本都拿到手,然後做轉賬。**這個櫃員在拿賬本的時候可能遇到以下三種情況:
- 檔案架上恰好有轉出賬本和轉入賬本,那就同時拿走;
- 如果檔案架上只有轉出賬本和轉入賬本之一,那這個櫃員就先把檔案架上有的賬本拿到手,同時等著其 他櫃員把另外一個賬本送回來;
- 轉出賬本和轉入賬本都沒有,那這個櫃員就等著兩個賬本都被送回來。
細粒度鎖有可能會出現死鎖
- 死鎖 :一組互相競爭資源的執行緒因互相等待,導致“永久”阻塞的現象 。
- 兩個執行緒彼此拿著對方的資源都不釋放就會導致死鎖,
- 使用細粒度鎖可能會導致死鎖
如果有客戶找櫃員張三做個轉賬業務:賬戶 A轉賬戶B 100元,此時另一個客戶找櫃員李四也做個轉賬業務:賬戶B轉賬戶A 100元,於是張三和李四同時都去檔案架上拿賬本,這時候有可能湊巧張三拿到了賬本A,李四拿到了賬本B。張三拿到賬本A後就等著 賬本B(賬本B已經被李四拿走),而李四拿到賬本B後就等著賬本A(賬本A已經被張三拿走),他們要等 多久呢?他們會永遠等待下去…因為張三不會把賬本A送回去,李四也不會把賬本B送回去。我們姑且稱為死等吧。
如何避免死鎖
- 互斥,共享資源X和Y只能被一個執行緒佔用;
- 佔有且等待,執行緒T1已經取得共享資源X,在等待共享資源Y的時候,不釋放共享資源x;
- 不可搶佔,其他執行緒不能強行搶佔執行緒T1佔有的資源;
- 迴圈等待,執行緒1等待執行緒T2佔有的資源,執行緒T2等待執行緒T1佔有的資源,就是迴圈等待。
只要破壞其中一個就可以避免死鎖
等待-通知機制
用synchronized實現等待-通知機制
- synchronized 配合wait(),notif(),notifyAll()這三個方法能夠輕鬆實現.
- wait(): 當前執行緒釋放鎖,進入阻塞狀態
- notif(),notifAll(): 通知阻塞的執行緒有可以繼續執行,執行緒進入可執行狀態
- notif()是會隨機地地通知等待隊歹一個執行緒
- notifyAll()會通知等待佇列中的所有執行緒,建議使用notifAll()
wait與sleep區別:
sleep是Object的中的方法,wait是Thread中的方法
wait會釋放鎖,sleep不會釋放鎖
wait需要用notif喚醒,sleep設定時間,時間到了喚醒
wait無需捕獲異常,而sleep需要
wait(): 當前執行緒進入阻塞
**** 碼字不易如果對你有幫助請給個關注****
**** 愛技術愛生活