鎖和synchronized

鎖的常見概念

  • 互斥: 同一時刻只有一個線程執行
  • 臨界區:一段須要互斥執行的代碼
  • 細粒度鎖: 用不一樣的鎖對受保護資源進行精細化管理。 細粒度鎖能夠提升並行度,是性能優化的一個重要手段
  • 死鎖 :一組互相競爭資源的線程因互相等待,致使「永久」阻塞的現象 。

用鎖的最佳實踐

  1. 永遠只再更新對象的成員變量時加鎖。
  2. 永遠只在訪問可變的成員變量時加鎖。
  3. 永遠再也不調用其它對象的方法時加鎖。
  4. 減小所得持有時間,減少鎖的粒度。

同步與異步

  • 調用方法若是須要等待結果,就是同步;若是不須要等待結果就是異步。
  • 同步是Java代碼默認的處理方式。

如何實現程序支持異步:java

  1. 異步調用: 調用方建立一個子線程,再子線程中執行方法調用。
  2. 異步方法: 被調用方;方法實現的時候,建立一個顯得線程執行主要邏輯,主線程直接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;
            }
        }
    }
}

這樣的話轉帳操做就成了串行的了,正常的邏輯應該只鎖轉入帳號和被轉入帳戶;不影響其餘的轉帳操做。稍做改造:優化

示例三:this

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對象的時候必須傳入同一個對象,線程

還有就是傳遞對象過於麻煩,寫法繁瑣缺少可行性。code

示例四:

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;
                }
            }
        }
    }
}

咱們試想在古代,沒有信息化,帳戶的存在形式真的就是一個帳本,並且每一個帳戶都有一個帳本,這些帳本 都統一存放在文件架上。銀行櫃員在給咱們作**轉帳時,要去文件架上把轉出帳本和轉入帳本都拿到手,而後作轉帳。**這個櫃員在拿帳本的時候可能遇到如下三種狀況:

  1. 文件架上剛好有轉出帳本和轉入帳本,那就同時拿走;
  2. 若是文件架上只有轉出帳本和轉入帳本之一,那這個櫃員就先把文件架上有的帳本拿到手,同時等着其 他櫃員把另一個帳本送回來;
  3. ​ 轉出帳本和轉入帳本都沒有,那這個櫃員就等着兩個帳本都被送回來。

細粒度鎖有可能會出現死鎖

  • 死鎖 :一組互相競爭資源的線程因互相等待,致使「永久」阻塞的現象 。
  • 兩個線程彼此拿着對方的資源都不釋放就會致使死鎖,
  • 使用細粒度鎖可能會致使死鎖

若是有客戶找櫃員張三作個轉帳業務:帳戶 A轉帳戶B 100元,此時另外一個客戶找櫃員李四也作個轉帳業務:帳戶B轉帳戶A 100元,因而張三和李四同時都去文件架上拿帳本,這時候有可能湊巧張三拿到了帳本A,李四拿到了帳本B。張三拿到帳本A後就等着 帳本B(帳本B已經被李四拿走),而李四拿到帳本B後就等着帳本A(帳本A已經被張三拿走),他們要等 多久呢?他們會永遠等待下去…由於張三不會把帳本A送回去,李四也不會把帳本B送回去。咱們姑且稱爲死等吧。

如何避免死鎖
  1. 互斥,共享資源X和Y只能被一個線程佔用;
  2. 佔有且等待,線程T1已經取得共享資源X,在等待共享資源Y的時候,不釋放共享資源x;
  3. 不可搶佔,其餘線程不能強行搶佔線程T1佔有的資源;
  4. 循環等待,線程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(): 當前線程進入阻塞


**** 碼字不易若是對你有幫助請給個關注****

**** 愛技術愛生活 QQ羣: 894109590****

相關文章
相關標籤/搜索