九、【Java 并發(fā)】鎖的概述

樂觀鎖 & 悲觀鎖

樂觀鎖和悲觀鎖是在數(shù)據(jù)庫中引入的名詞,但是在 Java 并發(fā)包鎖里面也引入了類似的思想。

悲觀鎖

悲觀鎖 指對數(shù)據(jù)被外界修改持保守態(tài)度,認為數(shù)據(jù)很容易就會被其他線程修改,所以在數(shù)據(jù)被處理前先對數(shù)據(jù)進行加鎖,并在整個數(shù)據(jù)處理過程中,使數(shù)據(jù)處于鎖定狀態(tài)。

悲觀鎖 的實現(xiàn)往往依靠數(shù)據(jù)庫提供的鎖機制,即在數(shù)據(jù)庫中,在對數(shù)據(jù)記錄操作前給記錄加排它鎖。如果獲取鎖失敗,則說明數(shù)據(jù)正在被其他線程修改,當前線程則等待或者拋出異常。如果獲取鎖成功,則對記錄進行操作,然后提交事務(wù)后釋放排它鎖。

public int updateEntry(long id) l
    // 使用悲觀鎖獲取指定記錄
    EntryObject entry = query("select * from table1 where id=#{id} for update", id) ;
    // 修改名稱
    entry.setName (“new name”) ;
    // 執(zhí)行更新操作
    int count = update("update table1 set name=#{name}, age=#{age} where id=#{id}",entry) ;
    return count;
}

當多個線程同時調(diào)用 updateEntry 方法,并且傳遞的是同一個id時,只有一個線程執(zhí)行代碼 query 方法會成功,其他線程則會被阻塞,這是因為在同一時間只有一個線程可以獲取對應(yīng)記錄的鎖,在獲取鎖的線程釋放鎖前 ( update Entry執(zhí)行完畢,提交事務(wù)之前 ),其他線程必須等待,也就是在同一時間只有一個線程可以對該記錄進行修改。

樂觀鎖

樂觀鎖 是相對悲觀鎖來說的,它認為數(shù)據(jù)在一般情況下不會造成沖突,所以在訪問記錄前不會加排它鎖,而是在進行數(shù)據(jù)提交更新時,才會正式對數(shù)據(jù)沖突與否進行檢測。具體來說,根據(jù)update返回的行數(shù)讓用戶決定如何去做。將上面的例子改為使用樂觀鎖的代碼如下。

public int updateEntry(long id) {
    //(1)使用樂觀鎖獲取指定記錄
    EntryObject entry = query("select * from table1 where id = #{id}",id) ;
    //(2)修改記錄內(nèi)容,version 字段不能被修改
    entry.setName(“name”);
    // (3) update操作
    int count = update("update table1 set name=#{name} , age=#{age}, version=${version}+1 where id =#{id} and version=#{version}",entry);
    return count; 
}

樂觀鎖并不會使用數(shù)據(jù)庫提供的鎖機制,一般在表中添加version字段或者使用業(yè)務(wù)狀態(tài)來實現(xiàn)。樂觀鎖直到提交時才鎖定,所以不會產(chǎn)生任何死鎖。

公平鎖 & 非公平鎖

根據(jù)線程獲取鎖的搶占機制,鎖可以分為公平鎖和非公平鎖,公平鎖表示線程獲取鎖的順序是按照線程請求鎖的時間先后來決定的,也就是最先請求鎖的線程將最先獲取到鎖。而非公平鎖則在運行時闖入,也就是先來不一定先得。

例如,假設(shè)線程A已經(jīng)持有了鎖,這時候線程B請求該鎖其將會被掛起。當線程A釋放鎖后,假如當前有線程C也需要獲取該鎖,如果采用非公平鎖方式,則根據(jù)線程調(diào)度策略,線程B和線程C兩者之一可能獲取鎖,這時候不需要任何其他干涉,而如果使用公平鎖則需要把C掛起,讓B獲取當前鎖。

在沒有公平性需求的前提下盡量使用非公平鎖,因為公平鎖會帶來性能開銷

獨占鎖 & 共享鎖

根據(jù)鎖只能被單個線程持有還是能被多個線程共同持有,鎖可以分為獨占鎖和共享鎖。

獨占鎖保證任何時候都只有一個線程能得到鎖,ReentrantLock 就是以獨占方式實現(xiàn)的。共享鎖則可以同時由多個線程持有,例如 ReadWriteLock 讀寫鎖,它允許一個資源可以被多線程同時進行讀操作。

獨占鎖是一種悲觀鎖,由于每次訪問資源都先加上互斥鎖,這限制了并發(fā)性,因為讀操作并不會影響數(shù)據(jù)的一致性,而獨占鎖只允許在同一時間由一個線程讀取數(shù)據(jù),其他線程必須等待當前線程釋放鎖才能進行讀取。

共享鎖則是一種樂觀鎖,它放寬了加鎖的條件,允許多個線程同時進行讀操作。

可重入鎖

當一個線程要獲取一個被其他線程持有的獨占鎖時,該線程會被阻塞,那么當一個線程再次獲取它自己已經(jīng)獲取的鎖時是否會被阻塞呢?如果不被阻塞,那么我們說該鎖是可重入的,也就是只要該線程獲取了該鎖,那么可以無限次數(shù)(嚴格來說是有限次數(shù))地進入被該鎖鎖住的代碼。

synchronized 內(nèi)部鎖是可重入鎖??芍厝腈i的原理是在鎖內(nèi)部維護一個線程
標示,用來標示該鎖目前被哪個線程占用,然后關(guān)聯(lián)一個計數(shù)器。一開始計數(shù)器值為 0,說明該鎖沒有被任何線程占用。當一個線程獲取了該鎖時,計數(shù)器的值會變成1,這時其他線程再來獲取該鎖時會發(fā)現(xiàn)鎖的所有者不是自己而被阻塞掛起。

但是當獲取了該鎖的線程再次獲取鎖時發(fā)現(xiàn)鎖擁有者是自己,就會把計數(shù)器值加+1,當釋放鎖后計數(shù)器值-1。當計數(shù)器值為0時,鎖里面的線程標示被重置為null,這時候被阻塞的線程會被喚醒來競爭獲取該鎖。

自旋鎖

由于Java中的線程是與操作系統(tǒng)中的線程一一對應(yīng)的,所以當一個線程在獲取鎖(比如獨占鎖)失敗后,會被切換到內(nèi)核狀態(tài)而被掛起。當該線程獲取到鎖時又需要將其切換到內(nèi)核狀態(tài)而喚醒該線程。而從用戶狀態(tài)切換到內(nèi)核狀態(tài)的開銷是比較大的,在一定程度上會影響并發(fā)性能。自旋鎖則是,當前線程在獲取鎖時,如果發(fā)現(xiàn)鎖已經(jīng)被其他線程占有,它不馬上阻塞自己,在不放棄CPU使用權(quán)的情況下,多次嘗試獲取(默認次數(shù)是10,可以使用 -XX:PreBlockSpinsh 參數(shù)設(shè)置該值),很有可能在后面幾次嘗試中其他線程已經(jīng)釋放了鎖。如果嘗試指定的次數(shù)后仍沒有獲取到鎖則當前線程才會被阻塞掛起。由此看來自旋鎖是使用CPU時間換取線程阻塞與調(diào)度的開銷,但是很有可能這些CPU時間白白浪費了。

Lock 接口

在 Lock 接口出現(xiàn)之前,Java 程序是靠 synchronized 關(guān)鍵字實現(xiàn)鎖功能的,而 Java Se5 之后,并發(fā)包中新增了 Lock 接口(以及相關(guān)實現(xiàn)類)用來實現(xiàn)鎖功能,它提供了與 synchronized 關(guān)鍵字類似的同步功能,只是在使用時需要顯式地獲取和釋放鎖。雖然它缺少了(通過 synchronized 塊或者方法所提供的)隱式獲取釋放鎖的便捷性,但是卻擁有了鎖獲取與釋放的可操作性、可中斷的獲取鎖以及超時獲取鎖等多種 synchronized 關(guān)鍵字所不具備的同步
特性。

使用 synchronized 關(guān)鍵字將會隱式地獲取鎖,但是它將鎖的獲取和釋放固化了,也就是先獲取再釋放。當然,這種方式簡化了同步的管理,可是擴展性沒有顯示的鎖獲取和釋放來的好。Lock 的使用其實也很簡單,如下代碼

    Lock lock = new ReentrantLock();
    lock.lock();
    try{
    }finally{
        lock.unlock();
    }

在 finally 塊中釋放鎖,目的是保證在獲取到鎖之后,最終能夠被釋放。

Lock 接口方法

  • void lock();
    獲取鎖,調(diào)用該方法當前線程將會獲取鎖,當鎖獲得后,從該方法返回

  • void lockInterruptibly() throws InterruptedException;
    可中斷地獲取鎖,和 lock 方法的不同之處在于該方法會響應(yīng)中斷,即在鎖的獲取中可以中斷當前線程

  • boolean tryLock()
    嘗試非阻塞的獲取鎖,調(diào)用該方法后立刻返回,如果能夠獲取則返回 true,否則返回 false

  • boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    超時的獲取鎖,當前線程在以下3種情況下會返回:

    • 當前線程在超時時間內(nèi)獲得了鎖
    • 當前線程在超時間內(nèi)被中斷
    • 超時時間結(jié)束,返回 false
  • void unlock
    釋放鎖

  • Condition newCondition();
    獲取等待通知組件,該組件和當前的鎖綁定,當前線程只有獲得了鎖,才能調(diào)用該組件的 wait 方法,而調(diào)用后,當前線程將釋放鎖

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容