java鎖synchronized和lock

同步代碼塊,同步方法,或者是用java提供的鎖機(jī)制,我們可以實(shí)現(xiàn)對共享資源變量的同步控制。

技術(shù)點(diǎn):

1、線程與進(jìn)程:

在開始之前先把進(jìn)程與線程進(jìn)行區(qū)分一下,一個(gè)程序最少需要一個(gè)進(jìn)程,而一個(gè)進(jìn)程最少需要一個(gè)線程。關(guān)系是線程–>進(jìn)程–>程序的大致組成結(jié)構(gòu)。所以線程是程序執(zhí)行流的最小單位,而進(jìn)程是系統(tǒng)進(jìn)行資源分配和調(diào)度的一個(gè)獨(dú)立單位。以下我們所有討論的都是建立在線程基礎(chǔ)之上。

2、Thread的幾個(gè)重要方法:

我們先了解一下Thread的幾個(gè)重要方法。

a、start()方法,開始執(zhí)行該線程;
b、stop()方法,強(qiáng)制結(jié)束該線程執(zhí)行;
c、join方法,等待該線程結(jié)束。
d、sleep()方法,線程進(jìn)入等待。
e、run()方法,直接執(zhí)行線程的run()方法,但是線程調(diào)用start()方法時(shí)也會運(yùn)行run()方法,區(qū)別就是一個(gè)是由線程調(diào)度運(yùn)行run()方法,一個(gè)是直接調(diào)用了線程中的run()方法??!
看到這里,可能有些人就會問啦,那wait()和notify()呢?

要注意,其實(shí)wait()與notify()方法是Object的方法,不是Thread的方法??!

同時(shí),<b>wait()與notify()會配合使用,分別表示線程掛起和線程恢復(fù)。</b>

這里還有一個(gè)很常見的問題,順帶提一下:<b>wait()與sleep()的區(qū)別,簡單來說wait()會釋放對象鎖而sleep()不會釋放對象鎖。</b>

3、線程狀態(tài):

線程總共有<b>5大狀態(tài)</b>,通過上面第二個(gè)知識點(diǎn)的介紹,理解起來就簡單了。

<b>新建狀態(tài)</b>:新建線程對象,并沒有調(diào)用start()方法之前

<b>就緒狀態(tài)</b>:調(diào)用start()方法之后線程就進(jìn)入就緒狀態(tài),但是并不是說只要調(diào)用start()方法線程就馬上變?yōu)楫?dāng)前線程,在變?yōu)楫?dāng)前線程之前都是為就緒狀態(tài)。值得一提的是,線程在睡眠和掛起中恢復(fù)的時(shí)候也會進(jìn)入就緒狀態(tài)哦。

<b>運(yùn)行狀態(tài)</b>:線程被設(shè)置為當(dāng)前線程,開始執(zhí)行run()方法。就是線程進(jìn)入運(yùn)行狀態(tài)

<b>阻塞狀態(tài)</b>:線程被暫停,比如說調(diào)用sleep()方法后線程就進(jìn)入阻塞狀態(tài)

<b>死亡狀態(tài)</b>:線程執(zhí)行結(jié)束

4、鎖類型

<b>可重入鎖</b>(synchronized和ReentrantLock):在執(zhí)行對象中所有同步方法不用再次獲得鎖

<b>可中斷鎖</b>(synchronized就不是可中斷鎖,而Lock是可中斷鎖):在等待獲取鎖過程中可中斷

<b>公平鎖</b>(ReentrantLock和ReentrantReadWriteLock): 按等待獲取鎖的線程的等待時(shí)間進(jìn)行獲取,等待時(shí)間長的具有優(yōu)先獲取鎖權(quán)利

<b>讀寫鎖</b>(ReadWriteLock和ReentrantReadWriteLock):對資源讀取和寫入的時(shí)候拆分為2部分處理,讀的時(shí)候可以多線程一起讀,寫的時(shí)候必須同步地寫

Synchronized與Lock的區(qū)別

類別 synchronized Lock
存在層次 Java的關(guān)鍵字,在jvm層面上 是一個(gè)接口
鎖的釋放 1、以獲取鎖的線程執(zhí)行完同步代碼,釋放鎖
2、線程執(zhí)行發(fā)生異常,jvm會讓線程釋放鎖
在finally中必須釋放鎖,不然容易造成線程死鎖
鎖的獲取 假設(shè)A線程獲得鎖,B線程等待。
如果A線程阻塞,B線程會一直等待
分情況而定,Lock有多個(gè)鎖獲取的方式,
大致就是可以嘗試獲得鎖,線程可以不用一直等待
(可以通過tryLock判斷有沒有鎖)
鎖狀態(tài) 無法判斷 可以判斷
鎖類型 可重入
不可中斷
非公平
可重入
可判斷
可公平(兩者皆可)
性能 少量同步 大量同步
<b>1.</b>Lock可以提高多個(gè)線程進(jìn)行讀操作的效率。
(可以通過readwritelock實(shí)現(xiàn)讀寫分離)
<b>2.</b>在資源競爭不是很激烈的情況下,
Synchronized的性能要優(yōu)于ReetrantLock,
但是在資源競爭很激烈的情況下,
Synchronized的性能會下降幾十倍,
但是ReetrantLock的性能能維持常態(tài);
<b>3.</b>ReentrantLock提供了多樣化的同步,
比如有時(shí)間限制的同步,可以被Interrupt的同步
(synchronized的同步是不能Interrupt的)等。
在資源競爭不激烈的情形下,
性能稍微比synchronized差點(diǎn)點(diǎn)。
但是當(dāng)同步非常激烈的時(shí)候,
synchronized的性能一下子能下降好幾十倍。而ReentrantLock確還能維持常態(tài)。

Synchronized與Static Synchronized

每個(gè)類有一個(gè)鎖,它可以用來控制對static數(shù)據(jù)成員的并發(fā)訪問。
訪問static synchronized方法占用的是類鎖,而訪問非static synchronized方法占用的是對象鎖。

static synchronized控制類的所有實(shí)例(對象)的訪問(相應(yīng)代碼塊)。
synchronized相當(dāng)于 this.synchronized,static synchronized相當(dāng)于Something.synchronized

Lock接口

//Lock是一個(gè)接口
public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}

lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()是用來獲取鎖的。
unLock()方法是用來釋放鎖的。
在Lock中聲明了四個(gè)方法來獲取鎖,那么這四個(gè)方法有何區(qū)別呢?

首先lock()方法是平常使用得最多的一個(gè)方法,就是用來獲取鎖。如果鎖已被其他線程獲取,則進(jìn)行等待。

由于在前面講到如果采用Lock,必須主動去釋放鎖,并且在發(fā)生異常時(shí),不會自動釋放鎖。因此一般來說,使用Lock必須在try{}catch{}塊中進(jìn)行,并且將釋放鎖的操作放在finally塊中進(jìn)行,以保證鎖一定被被釋放,防止死鎖的發(fā)生。通常使用Lock來進(jìn)行同步的話,是以下面這種形式去使用的:

Lock lock = ...;
lock.lock();
try{
    //處理任務(wù)
}catch(Exception ex){
     
}finally{
    //釋放鎖
    lock.unlock();   
}

tryLock()方法是有返回值的,它表示用來嘗試獲取鎖,如果獲取成功,則返回true,如果獲取失?。存i已被其他線程獲取),則返回false,也就說這個(gè)方法無論如何都會立即返回。在拿不到鎖時(shí)不會一直在那等待。

tryLock(long time, TimeUnit unit)方法和tryLock()方法是類似的,只不過區(qū)別在于這個(gè)方法在拿不到鎖時(shí)會等待一定的時(shí)間,在時(shí)間期限之內(nèi)如果還拿不到鎖,就返回false。如果如果一開始拿到鎖或者在等待期間內(nèi)拿到了鎖,則返回true。

所以,一般情況下通過tryLock來獲取鎖時(shí)是這樣使用的:

Lock lock = ...;
if(lock.tryLock()) {
     try{
         //處理任務(wù)
     }catch(Exception ex){
         //
     }finally{
         lock.unlock();   //釋放鎖
     } 
}else {
    //如果不能獲取鎖,則直接做其他事情
}

lockInterruptibly()方法比較特殊,當(dāng)通過這個(gè)方法去獲取鎖時(shí),如果線程正在等待獲取鎖,則這個(gè)線程能夠響應(yīng)中斷,即中斷線程的等待狀態(tài)。也就使說,當(dāng)兩個(gè)線程同時(shí)通過lock.lockInterruptibly()想獲取某個(gè)鎖時(shí),假若此時(shí)線程A獲取到了鎖,而線程B只有在等待,那么對線程B調(diào)用threadB.interrupt()方法能夠中斷線程B的等待過程。

由于lockInterruptibly()的聲明中拋出了異常,所以lock.lockInterruptibly()必須放在try塊中或者在調(diào)用lockInterruptibly()的方法外聲明拋出InterruptedException。

因此lockInterruptibly()一般的使用形式如下:

public void method() throws InterruptedException {
    lock.lockInterruptibly();
    try {  
     //.....
    }
    finally {
        lock.unlock();
    }  
}

注意,當(dāng)一個(gè)線程獲取了鎖之后,是不會被interrupt()方法中斷的。單獨(dú)調(diào)用interrupt()方法不能中斷正在運(yùn)行過程中的線程,只能中斷阻塞過程中的線程。

因此當(dāng)通過lockInterruptibly()方法獲取某個(gè)鎖時(shí),如果不能獲取到,只有進(jìn)行等待的情況下,是可以響應(yīng)中斷的。

而用synchronized修飾的話,當(dāng)一個(gè)線程處于等待某個(gè)鎖的狀態(tài),是無法被中斷的,只有一直等待下去。

Lock類型

一、公平鎖/非公平鎖

公平鎖是指多個(gè)線程按照申請鎖的順序來獲取鎖。
非公平鎖是指多個(gè)線程獲取鎖的順序并不是按照申請鎖的順序,有可能后申請的線程比先申請的線程優(yōu)先獲取鎖。有可能,會造成優(yōu)先級反轉(zhuǎn)或者饑餓現(xiàn)象。
對于ReentrantLock而言,通過構(gòu)造函數(shù)指定該鎖是否是公平鎖,默認(rèn)是非公平鎖。非公平鎖的優(yōu)點(diǎn)在于吞吐量比公平鎖大。
對于Synchronized而言,也是一種非公平鎖。由于其并不像ReentrantLock是通過AQS的來實(shí)現(xiàn)線程調(diào)度,所以并沒有任何辦法使其變成公平鎖。

二、可重入鎖

可重入鎖又名遞歸鎖,是指在同一個(gè)線程在外層方法獲取鎖的時(shí)候,在進(jìn)入內(nèi)層方法會自動獲取鎖。
說的有點(diǎn)抽象,下面會有一個(gè)代碼的示例。
對于Java ReentrantLock而言, 他的名字就可以看出是一個(gè)可重入鎖,其名字是ReentrantLock重新進(jìn)入鎖。
對于Synchronized而言,也是一個(gè)可重入鎖??芍厝腈i的一個(gè)好處是可一定程度避免死鎖。

synchronized void setA() throws Exception{
    Thread.sleep(1000);
    setB();
}

synchronized void setB() throws Exception{
  Thread.sleep(1000);
}
三、獨(dú)享鎖/共享鎖

獨(dú)享鎖是指該鎖一次只能被一個(gè)線程所持有。
共享鎖是指該鎖可被多個(gè)線程所持有。
對于Java ReentrantLock而言,其是獨(dú)享鎖。但是對于Lock的另一個(gè)實(shí)現(xiàn)類ReadWriteLock,其讀鎖是共享鎖,其寫鎖是獨(dú)享鎖。
讀鎖的共享鎖可保證并發(fā)讀是非常高效的,讀寫,寫讀 ,寫寫的過程是互斥的。
獨(dú)享鎖與共享鎖也是通過AQS來實(shí)現(xiàn)的,通過實(shí)現(xiàn)不同的方法,來實(shí)現(xiàn)獨(dú)享或者共享。
對于Synchronized而言,當(dāng)然是獨(dú)享鎖。

四、互斥鎖/讀寫鎖

上面講的獨(dú)享鎖/共享鎖就是一種廣義的說法,互斥鎖/讀寫鎖就是<b>具體的實(shí)現(xiàn)</b>。
互斥鎖在Java中的具體實(shí)現(xiàn)就是ReentrantLock
讀寫鎖在Java中的具體實(shí)現(xiàn)就是ReadWriteLock

五、樂觀鎖/悲觀鎖

樂觀鎖與悲觀鎖不是指具體的什么類型的鎖,而是指看待并發(fā)同步的<b>角度</b>。
悲觀鎖認(rèn)為對于同一個(gè)數(shù)據(jù)的并發(fā)操作,一定是會發(fā)生修改的,哪怕沒有修改,也會認(rèn)為修改。因此對于同一個(gè)數(shù)據(jù)的并發(fā)操作,悲觀鎖采取加鎖的形式。悲觀的認(rèn)為,不加鎖的并發(fā)操作一定會出問題。
樂觀鎖則認(rèn)為對于同一個(gè)數(shù)據(jù)的并發(fā)操作,是不會發(fā)生修改的。在更新數(shù)據(jù)的時(shí)候,會采用嘗試更新,不斷重新的方式更新數(shù)據(jù)。樂觀的認(rèn)為,不加鎖的并發(fā)操作是沒有事情的。
從上面的描述我們可以看出,悲觀鎖適合寫操作非常多的場景,樂觀鎖適合讀操作非常多的場景,不加鎖會帶來大量的性能提升。
悲觀鎖在Java中的使用,就是利用各種鎖。
樂觀鎖在Java中的使用,是無鎖編程,常常采用的是CAS算法,典型的例子就是原子類,通過CAS自旋實(shí)現(xiàn)原子操作的更新。

六、分段鎖

分段鎖其實(shí)是一種鎖的<b>設(shè)計(jì)</b>,并不是具體的一種鎖,對于ConcurrentHashMap而言,其并發(fā)的實(shí)現(xiàn)就是通過分段鎖的形式來實(shí)現(xiàn)高效的并發(fā)操作。
我們以ConcurrentHashMap來說一下分段鎖的含義以及設(shè)計(jì)思想,ConcurrentHashMap中的分段鎖稱為Segment,它即類似于HashMap(JDK7與JDK8中HashMap的實(shí)現(xiàn))的結(jié)構(gòu),即內(nèi)部擁有一個(gè)Entry數(shù)組,數(shù)組中的每個(gè)元素又是一個(gè)鏈表;同時(shí)又是一個(gè)ReentrantLock(Segment繼承了ReentrantLock)。
當(dāng)需要put元素的時(shí)候,并不是對整個(gè)hashmap進(jìn)行加鎖,而是先通過hashcode來知道他要放在那一個(gè)分段中,然后對這個(gè)分段進(jìn)行加鎖,所以當(dāng)多線程put的時(shí)候,只要不是放在一個(gè)分段中,就實(shí)現(xiàn)了真正的并行的插入。
但是,在統(tǒng)計(jì)size的時(shí)候,可就是獲取hashmap全局信息的時(shí)候,就需要獲取所有的分段鎖才能統(tǒng)計(jì)。
分段鎖的設(shè)計(jì)目的是細(xì)化鎖的粒度,當(dāng)操作不需要更新整個(gè)數(shù)組的時(shí)候,就僅僅針對數(shù)組中的一項(xiàng)進(jìn)行加鎖操作。

七、偏向鎖/輕量級鎖/重量級鎖

這三種鎖是指鎖的<b>狀態(tài)</b>,并且是針對Synchronized。在Java 5通過引入鎖升級的機(jī)制來實(shí)現(xiàn)高效Synchronized。這三種鎖的狀態(tài)是通過對象監(jiān)視器在對象頭中的字段來表明的。
<b>偏向鎖</b>是指一段同步代碼一直被一個(gè)線程所訪問,那么該線程會自動獲取鎖。降低獲取鎖的代價(jià)。
<b>輕量級鎖</b>是指當(dāng)鎖是偏向鎖的時(shí)候,被另一個(gè)線程所訪問,偏向鎖就會升級為輕量級鎖,其他線程會通過自旋的形式嘗試獲取鎖,不會阻塞,提高性能。
<b>重量級鎖</b>是指當(dāng)鎖為輕量級鎖的時(shí)候,另一個(gè)線程雖然是自旋,但自旋不會一直持續(xù)下去,當(dāng)自旋一定次數(shù)的時(shí)候,還沒有獲取到鎖,就會進(jìn)入阻塞,該鎖膨脹為重量級鎖。重量級鎖會讓其他申請的線程進(jìn)入阻塞,性能降低。

八、自旋鎖

在Java中,自旋鎖是指嘗試獲取鎖的線程不會立即阻塞,而是采用循環(huán)的方式去嘗試獲取鎖,這樣的好處是減少線程上下文切換的消耗,缺點(diǎn)是循環(huán)會消耗CPU。
線程自旋和適應(yīng)性自旋

我們知道,java線程其實(shí)是映射在內(nèi)核之上的,線程的掛起和恢復(fù)會極大的影響開銷。
并且jdk官方人員發(fā)現(xiàn),很多線程在等待鎖的時(shí)候,在很短的一段時(shí)間就獲得了鎖,所以它們在線程等待的時(shí)候,并不需要把線程掛起,而是讓他無目的的循環(huán),一般設(shè)置10次。
這樣就避免了線程切換的開銷,極大的提升了性能。

而<b>適應(yīng)性自旋</b>,是賦予了自旋一種學(xué)習(xí)能力,它并不固定自旋10次一下。
他可以根據(jù)它前面線程的自旋情況,從而調(diào)整它的自旋,甚至是不經(jīng)過自旋而直接掛起。

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

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

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