溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

掌握之并發(fā)編程-3.鎖

發(fā)布時(shí)間:2020-06-24 11:01:03 來(lái)源:網(wǎng)絡(luò) 閱讀:201 作者:學(xué)習(xí)Lr 欄目:編程語(yǔ)言

掌握高并發(fā)、高可用架構(gòu)

第二課 并發(fā)編程

從本課開(kāi)始學(xué)習(xí)并發(fā)編程的內(nèi)容。主要介紹并發(fā)編程的基礎(chǔ)知識(shí)、鎖、內(nèi)存模型、線程池、各種并發(fā)容器的使用。

第三節(jié) 鎖

并發(fā)編程 并發(fā)基礎(chǔ) AQS Synchronized Lock

這小節(jié)咱們來(lái)學(xué)習(xí)并發(fā)編程中鎖的知識(shí)。主要包括關(guān)鍵字synchronized、各種LockAQS的原理、以及各自的應(yīng)用。

synchronized

可以修飾方法或者代碼塊

表示多個(gè)線程訪問(wèn)該方法或者代碼塊時(shí)要進(jìn)行排隊(duì),串行的執(zhí)行該方法或者代碼塊

執(zhí)行效率低,但是它是并發(fā)編程容器的基礎(chǔ)

分類(lèi) 具體分類(lèi) 被鎖的對(duì)象 示例代碼 說(shuō)明
方法 實(shí)例方法 類(lèi)的實(shí)例對(duì)象 synchronized void methodA() {};<br />void methodB() {};<br />synchronized void methodC() {}; 線程調(diào)用了同步方法,<br />別的線程可以調(diào)用非同步方法,<br />對(duì)于其他同步方法,必須該方法在執(zhí)行完成后才能調(diào)用<br />不影響靜態(tài)方法的調(diào)用(包括同步,非同步)
靜態(tài)方法 類(lèi)對(duì)象 static synchronized void methodA() {};<br />static void methodB() {};<br />static synchronized void methodC() {}; 線程調(diào)用了同步方法,<br />別的線程可以調(diào)用非同步方法,<br />對(duì)于其他同步方法,必須該方法在執(zhí)行完成后才能調(diào)用<br />不影響對(duì)象方法的調(diào)用(包括同步,非同步)
代碼塊 實(shí)例對(duì)象 類(lèi)的實(shí)例對(duì)象 synchronized(this) {} 同上
class對(duì)象 類(lèi)對(duì)象 synchronized(SynchronizedTest.class) {} 同上
任意實(shí)例對(duì)象 實(shí)例對(duì)象Object Object lock = new Object();<br />synchronized(lock) {} 只影響鎖住的對(duì)象,而不影響類(lèi)和類(lèi)的實(shí)例對(duì)象
synchronized的實(shí)現(xiàn)機(jī)制

JAVA對(duì)象頭Monitor是實(shí)現(xiàn)synchronized的基礎(chǔ)。

  1. JAVA對(duì)象頭,對(duì)于Hotspot虛擬機(jī)的對(duì)象頭主要包含兩部分?jǐn)?shù)據(jù):Mark Word(標(biāo)記字段)、Klass Pointer(類(lèi)型指針)

    • Mark Word,用于存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù),如哈希碼(Hash Code)、GC分代年齡、鎖狀態(tài)標(biāo)識(shí)、線程持有的鎖、偏向線程ID、偏向時(shí)間戳等
    • Klass Pointer,是對(duì)象指向它的類(lèi)元數(shù)據(jù)的指針,虛擬機(jī)通過(guò)這個(gè)指針來(lái)確定這個(gè)對(duì)象是哪個(gè)類(lèi)的實(shí)例
  2. Monitor,是一種同步機(jī)制,即同一時(shí)刻只允許一個(gè)線程進(jìn)入Monitor的臨界區(qū),從而達(dá)到互斥的效果。synchronized的對(duì)象鎖,其指針指向的是一個(gè)Monitor對(duì)象的起始地址。每個(gè)對(duì)象實(shí)例都有一個(gè)monitor。由C++實(shí)現(xiàn),其數(shù)據(jù)結(jié)構(gòu)如下。

    ```C++
    ObjectMonitor() {
    _count = 0;
    _owner = NULL;
    _waitSet = NULL;
    _waitSetLock = 0;
    _EntryList = NULL;
    }

    
    
    其中,**_owner**指向持有ObjectMonitor對(duì)象的線程。當(dāng)多個(gè)線程同時(shí)訪問(wèn)一段同步代碼時(shí),會(huì)把線程存放在鎖的對(duì)象的**_EntryList**中。當(dāng)某個(gè)線程獲得對(duì)象的Monitor時(shí),就會(huì)把*_owner*的值設(shè)置為當(dāng)前線程,同時(shí)*_count*加1。如果線程調(diào)用**wait()**方法,就會(huì)釋放當(dāng)前持有的Monitor,*_owner*置為null,*_count*減1,并將該線程放入**_waitSet**中。當(dāng)然,如果持有monitor的線程正常執(zhí)行完畢,也會(huì)釋放monitor,*_owner*置為null,*_count*減1。

對(duì)于加在代碼塊上的synchronized,其字節(jié)碼是:一次monitorenter、兩次monitorexit(含有一次編譯器自動(dòng)生成的異常處理的monitorexit);

對(duì)于加在方法上的synchronized,其字節(jié)碼是:標(biāo)識(shí)方法為ACC_SYNCHRONIZED

新特性

synchronized是一個(gè)重量級(jí)鎖,相較Lock,比較笨重,不高效。在JDK1.6中,其實(shí)現(xiàn)過(guò)程引入了大量的優(yōu)化,如自旋鎖自適應(yīng)自旋鎖、鎖消除鎖粗化、偏向鎖輕量級(jí)鎖等技術(shù)來(lái)減少鎖的開(kāi)銷(xiāo)。

  • 自旋鎖,是指當(dāng)一個(gè)線程在獲取鎖的時(shí)候,如果鎖已經(jīng)被其他線程獲取,那么該線程將循環(huán)等待,然后不斷判斷鎖是否能夠成功獲取,直到獲取到鎖才退出循環(huán)。

    優(yōu)點(diǎn):自旋鎖不會(huì)使線程發(fā)生狀態(tài)切換,而是一直處于活動(dòng)狀態(tài),不會(huì)進(jìn)入阻塞狀態(tài),減少了不必要的上下文切換,執(zhí)行速度快

    缺點(diǎn):如果某個(gè)線程持有鎖的時(shí)間過(guò)長(zhǎng),就會(huì)導(dǎo)致其他等待獲取鎖的線程進(jìn)入循環(huán)等待,消耗CPU。如果使用不當(dāng)會(huì)導(dǎo)致CPU使用率極高;不公平的鎖會(huì)導(dǎo)致“線程饑餓”問(wèn)題

  • 自適應(yīng)自旋鎖,JDK1.6引入的更聰明的自旋鎖。就是說(shuō)自旋的次數(shù)不是固定的,它是由前一次在同一個(gè)鎖上的自旋時(shí)間以及鎖的持有者的狀態(tài)決定的。JVM自適應(yīng)的調(diào)整自旋次數(shù),使能更有效的獲取到鎖,避免浪費(fèi)資源

  • 鎖消除,當(dāng)JVM檢測(cè)到不可能存在共享數(shù)據(jù)競(jìng)爭(zhēng),此時(shí)會(huì)對(duì)這些鎖進(jìn)行鎖消除

  • 鎖粗化,在使用鎖時(shí),需要讓同步代碼塊的作用范圍盡可能小。所謂鎖粗化,就是將多個(gè)連續(xù)的加鎖、解鎖操作連接在一起,擴(kuò)展成一個(gè)范圍更大的鎖

  • 偏向鎖,是指同一段代碼一直被同一個(gè)線程訪問(wèn),那么該線程會(huì)自動(dòng)獲取到鎖,從而降低獲取鎖的代價(jià)

  • 輕量級(jí)鎖,當(dāng)鎖是偏向鎖的時(shí)候,此時(shí)被另一個(gè)線程訪問(wèn),偏向鎖會(huì)升級(jí)為輕量級(jí)鎖。其他線程會(huì)通過(guò)自旋的形式嘗試獲取鎖,不會(huì)阻塞

  • 重量級(jí)鎖,當(dāng)鎖是輕量級(jí)鎖時(shí),另一個(gè)線程雖然在自旋等待獲取鎖,但是自旋不會(huì)一直持續(xù),當(dāng)自旋一定次數(shù)后,還沒(méi)有獲取到鎖,就會(huì)進(jìn)入阻塞,該鎖也會(huì)膨脹為重量級(jí)鎖。重量級(jí)鎖會(huì)讓其他線程進(jìn)入阻塞,性能降低。此時(shí)就成為了原始的synchronized的實(shí)現(xiàn)
鎖的等級(jí)

依次是:無(wú)鎖狀態(tài)、偏向鎖狀態(tài)、輕量級(jí)鎖狀態(tài)、重量級(jí)鎖狀態(tài)

Lock

Lock是一個(gè)接口,有以下方法。

public interface Lock {
    void lock();
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    void lockInterruptibly();
    Condition newCondition();
}

這里說(shuō)下方法void lockInterruptibly()一個(gè)線程獲得鎖之后是不可以被interrupt()方法中斷的,是不能中斷正在執(zhí)行中的線程的,只能中斷阻塞過(guò)程中的線程,lockInterruptibly方法允許當(dāng)線程等待獲取鎖的過(guò)程中由其他線程來(lái)中斷等待。

Lock 和 synchronized的區(qū)別與相同點(diǎn)

區(qū)別:

  • Lock是一個(gè)接口,synchronized是內(nèi)置關(guān)鍵字;Lock只能在代碼塊中,而synchronized可以在方法上和代碼塊中
  • synchronized不管正常退出還是異常退出,都會(huì)自動(dòng)釋放鎖資源,不會(huì)導(dǎo)致死鎖的發(fā)生;Lock需要手動(dòng)加鎖和解鎖,所以如果解鎖操作未執(zhí)行就會(huì)導(dǎo)致死鎖
  • Lock可以讓等待鎖的線程響應(yīng)中斷,而synchronized不行,使用synchronized時(shí),等待的線程會(huì)一直等待下去,不能夠響應(yīng)中斷
  • 通過(guò)Lock可以知道有沒(méi)有獲取到鎖,而synchronized無(wú)法知道
  • Lock可以選擇公平鎖和非公平鎖,而synchronized只有非公平鎖
  • Lock是樂(lè)觀鎖,通過(guò)CAS(compare and swap 比較交換)來(lái)實(shí)現(xiàn),底層是AbstractQueuedSynchronizer(AQS),而synchronized是悲觀鎖,是由JVM來(lái)控制的
  • Lock的鎖是針對(duì)lock對(duì)象,而synchronized的鎖定對(duì)象是類(lèi)或者指定對(duì)象
  • Lock可以提高多個(gè)線程進(jìn)行讀操作的效率

相同點(diǎn):

  • 都是可重入鎖
ReentrantLock

可重入鎖,是Lock接口的唯一實(shí)現(xiàn)類(lèi)。

可重入鎖:是指如果一個(gè)線程獲得了一個(gè)對(duì)象的鎖,那么它不需要再獲取該對(duì)象的鎖而可以直接執(zhí)行方法。也就是鎖的分配機(jī)制是基于線程來(lái)分配的,而不是基于方法調(diào)用的分配。

可中斷鎖:可以響應(yīng)中斷的鎖。只有lockInterruptibly()方法的鎖是可中斷鎖,lock()還是不可中斷的

公平鎖:指盡量以請(qǐng)求鎖的順序來(lái)獲取鎖。比如有多個(gè)線程在等待一個(gè)鎖,當(dāng)鎖被其他線程釋放時(shí),最先請(qǐng)求鎖的線程會(huì)獲得該鎖

非公平鎖:無(wú)法保證鎖的獲取是按照請(qǐng)求鎖的順序進(jìn)行的??赡軐?dǎo)致某個(gè)或者一些線程永遠(yuǎn)獲取不到鎖

對(duì)于ReentrantLock,默認(rèn)是非公平鎖,但可指定為公平鎖。

ReentrantLock lock = new ReentrantLock(true)

ReentrantLock中定義了兩個(gè)內(nèi)部類(lèi),一個(gè)是NotFairSync,一個(gè)是FairSync。當(dāng)構(gòu)造器參數(shù)為true時(shí),表示創(chuàng)建公平鎖,參數(shù)為false或者無(wú)參時(shí),表示非公平鎖。

ReadWriteLock
public interface ReadWriteLock {
    Lock readLock();
    Lock writeLock();
}

一個(gè)用來(lái)獲取讀鎖,一個(gè)用來(lái)獲取寫(xiě)鎖。

ReentrantReadWriteLock

可重入讀寫(xiě)鎖,實(shí)現(xiàn)了ReadWriteLock接口。

多個(gè)線程同時(shí)進(jìn)行讀操作時(shí),會(huì)使多個(gè)線程交替進(jìn)行,從而提高讀操作的效率。但是,如果有線程占用讀鎖,此時(shí)其他要獲取寫(xiě)鎖的話,就必須等待讀鎖釋放后才可執(zhí)行;如果有線程占用寫(xiě)鎖,此時(shí)其他線程不管是要獲取讀鎖或者寫(xiě)鎖的話,都必須等待寫(xiě)鎖釋放。

Lock的實(shí)現(xiàn)原理

ReentrantLockFairSyncNotFairSync都繼承了AbstractQueuedSynchronizer,并且真正lock()unlock()的實(shí)現(xiàn)過(guò)程都是在AQS中。

首先,AQS的數(shù)據(jù)結(jié)構(gòu)是:一個(gè)表示鎖狀態(tài)的變量volatile int state,取值范圍是 0 無(wú)鎖、1 有鎖;一個(gè)用于存儲(chǔ)等待獲取鎖的線程的雙向鏈表transient volatile Node headtransient volatile Node tail。

其次,加鎖流程NotFairSync.lock()是:

  1. 通過(guò)CAS去嘗試獲取鎖:判斷當(dāng)前state是0的話表示無(wú)鎖,然后把當(dāng)前線程設(shè)置為獨(dú)占執(zhí)行線程,再修改state為1表示有鎖
    掌握之并發(fā)編程-3.鎖

  2. 如果第一步獲取鎖失敗,那么執(zhí)行acquire(1)(這是AQS的方法)

掌握之并發(fā)編程-3.鎖

主要是三個(gè)方法:

  • tryAcquire,再次嘗試通過(guò)CAS獲取一次鎖(子類(lèi)NotFairSync的方法)
    • 判斷state,0則可以獲取到鎖,非0則判斷執(zhí)行線程是否是當(dāng)前線程,如果是,則把重入次數(shù)加1,都不是則設(shè)置獲取鎖失敗

掌握之并發(fā)編程-3.鎖

  • addWaiter,把當(dāng)前線程放入等待隊(duì)列的雙向鏈表Node中(通過(guò)無(wú)限循環(huán)-自旋,找到鏈表尾,接到尾部)
  • acquireQueued,通過(guò)自旋,判斷當(dāng)前線程是否到達(dá)鏈表頭部,當(dāng)?shù)竭_(dá)頭部時(shí),就嘗試獲取鎖。如果獲取到鎖就把其從頭部移除
    • 在自旋過(guò)程中,通過(guò)shouldParkAfterFailedAcquire判斷當(dāng)前線程的狀態(tài)是Cancelled、Signal等,從而在parkAndCheckInterrupt中對(duì)線程進(jìn)行剔除(Cancelled)、阻塞(Signal)操作

掌握之并發(fā)編程-3.鎖
下面借兩張圖(<https://yq.aliyun.com/articles/640868>)

掌握之并發(fā)編程-3.鎖cdn.com/71e8b71038243dfaf21ebcf6f9fcc5fbaa659b08.png">

獲取鎖的流程:

掌握之并發(fā)編程-3.鎖

然后,解鎖的流程是調(diào)用NotFairSync.release(),主要是對(duì)重入數(shù)量的調(diào)整。每次釋放鎖都只會(huì)對(duì)數(shù)量減1,直到state為0時(shí)表示鎖釋放完成。
掌握之并發(fā)編程-3.鎖

Lock和synchronized的選擇

根據(jù)CAS的特性,建議在低鎖沖突的情況下使用Lock

在JDK1.6后,官方對(duì)synchronized做了大量的優(yōu)化(偏向鎖、自旋、輕量級(jí)鎖等),因此在非必要的情況下,建議都使用synchronized做同步操作

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI