<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          淺談 Java 并發(fā)下的樂(lè)觀(guān)鎖

          共 6735字,需瀏覽 14分鐘

           ·

          2021-01-09 12:52

          引子

          各位少俠大家好!今天我們來(lái)聊聊 Java 并發(fā)下的樂(lè)觀(guān)鎖。

          在聊樂(lè)觀(guān)鎖之前,先給大家復(fù)習(xí)一個(gè)概念:原子操作:

          什么是原子操作呢?

          我們知道,原子(atom)指化學(xué)反應(yīng)不可再分的基本微粒。在 Java 多線(xiàn)程編程中,所謂原子操作,就是即使命令涉及多個(gè)操作,這些操作依次執(zhí)行,不會(huì)被別的線(xiàn)程插隊(duì)打斷。

          原子操作

          聊完原子操作了,我們進(jìn)入正題。

          大家都知道,一般而言,由于多線(xiàn)程并發(fā)會(huì)導(dǎo)致安全問(wèn)題,針對(duì)變量的寫(xiě)操作,都會(huì)采用鎖的機(jī)制。鎖一般會(huì)分為樂(lè)觀(guān)鎖悲觀(guān)鎖兩種。

          悲觀(guān)鎖

          對(duì)于悲觀(guān)鎖,開(kāi)發(fā)者認(rèn)為數(shù)據(jù)發(fā)送時(shí)發(fā)生并發(fā)沖突的概率很大,所以每次進(jìn)行讀操作前都會(huì)上鎖。

          樂(lè)觀(guān)鎖

          對(duì)于樂(lè)觀(guān)鎖,開(kāi)發(fā)者認(rèn)為數(shù)據(jù)發(fā)送時(shí)發(fā)生并發(fā)沖突的概率不大,所以讀操作前不上鎖。

          到了寫(xiě)操作時(shí)才會(huì)進(jìn)行判斷,數(shù)據(jù)在此期間是否被其他線(xiàn)程修改。如果發(fā)生修改,那就返回寫(xiě)入失敗;如果沒(méi)有被修改,那就執(zhí)行修改操作,返回修改成功。

          樂(lè)觀(guān)鎖一般都采用 Compare And Swap(CAS)算法進(jìn)行實(shí)現(xiàn)。顧名思義,該算法涉及到了兩個(gè)操作,比較(Compare)和交換(Swap)。

          CAS 算法流程

          CAS 算法的思路如下:

          1. 該算法認(rèn)為不同線(xiàn)程對(duì)變量的操作時(shí)產(chǎn)生競(jìng)爭(zhēng)的情況比較少。
          2. 該算法的核心是對(duì)當(dāng)前讀取變量值 E 和內(nèi)存中的變量舊值 V 進(jìn)行比較。
          3. 如果相等,就代表其他線(xiàn)程沒(méi)有對(duì)該變量進(jìn)行修改,就將變量值更新為新值 N。
          4. 如果不等,就認(rèn)為在讀取值 E 到比較階段,有其他線(xiàn)程對(duì)變量進(jìn)行過(guò)修改,不進(jìn)行任何操作。

          當(dāng)線(xiàn)程運(yùn)行 CAS 算法時(shí),該運(yùn)行過(guò)程是原子操作,也就是說(shuō),Compare And Swap 這個(gè)過(guò)程雖然涉及邏輯比較繁冗,但具體操作一氣呵成。

          Java中 CAS 的底層實(shí)現(xiàn)

          Java 中的 Unsafe 類(lèi)

          我先問(wèn)大家一個(gè)問(wèn)題:

          什么是指針?

          針對(duì)學(xué)過(guò) C、C++ 語(yǔ)言的同學(xué)想必都不陌生。說(shuō)白了,指針就是內(nèi)存地址,指針變量也就是用來(lái)存放內(nèi)存地址的變量。

          但對(duì)于指針這個(gè)東西的使用,有利有弊。有利的地方在于如果我們有了內(nèi)存的偏移量,換句話(huà)說(shuō)有了數(shù)據(jù)在內(nèi)存中的存儲(chǔ)位置坐標(biāo),就可以直接針對(duì)內(nèi)存的變量操作;

          弊端就在于指針是語(yǔ)言中功能強(qiáng)大的組件,如果一個(gè)新手在編程時(shí),沒(méi)有考慮指針的安全性,錯(cuò)誤的操作指針把某塊不該修改的內(nèi)存值修改,容易導(dǎo)致整個(gè)程序崩潰。

          錯(cuò)誤使用指針

          對(duì)于 Java 語(yǔ)言,沒(méi)有直接的指針組件,一般也不能使用偏移量對(duì)某塊內(nèi)存進(jìn)行操作。這些操作相對(duì)來(lái)講是安全(safe)的。

          但其實(shí) Java 有個(gè)類(lèi)叫 Unsafe 類(lèi),這個(gè)類(lèi)類(lèi)使 Java 擁有了像 C 語(yǔ)言的指針一樣操作內(nèi)存空間的能力,同時(shí)也帶來(lái)了指針的問(wèn)題。這個(gè)類(lèi)可以說(shuō)是 Java 并發(fā)開(kāi)發(fā)的基礎(chǔ)。

          Unsafe 類(lèi)中的 CAS

          一般而言,大家接觸到的 CAS 函數(shù)都是 Unsafe 類(lèi)提供的封裝。下面就是一些 CAS 函數(shù)。

          public?final?native?boolean?compareAndSwapObject(
          ????Object?paramObject1,?
          ????long?paramLong,?
          ????Object?paramObject2,?
          ????Object?paramObject3)
          ;

          public?final?native?boolean?compareAndSwapInt(
          ????Object?paramObject,?
          ????long?paramLong,?
          ????int?paramInt1,?
          ????int?paramInt2)
          ;

          public?final?native?boolean?compareAndSwapLong(
          ????Object?paramObject,?
          ????long?paramLong1,?
          ????long?paramLong2,?
          ????long?paramLong3)
          ;

          這就是 Unsafe 包下提供的 CAS 更新對(duì)象、CAS 更新 int 型變量、CAS 更新 long 型變量三個(gè)函數(shù)。

          我們以最好理解的 compareAndSwapInt 為例,來(lái)看一下吧:

          public?final?native?boolean?compareAndSwapInt(
          ????Object?paramObject,?
          ????long?paramLong,?
          ????int?paramInt1,?
          ????int?paramInt2)
          ;

          可以看到,該函數(shù)有四個(gè)參數(shù):

          • 第一個(gè)是目標(biāo)對(duì)象
          • 第二個(gè)參數(shù)用來(lái)表示我們上文講的指針,這里是一個(gè) long 類(lèi)型的數(shù)值,表示該成員變量在其對(duì)應(yīng)對(duì)象屬性的偏移量。換句話(huà)說(shuō),函數(shù)就可以利用這個(gè)參數(shù),找到變量在內(nèi)存的具體位置,從而進(jìn)行 CAS 操作
          • 第三個(gè)參數(shù)就是預(yù)期的舊值,也就是示例中的 V。
          • 第四個(gè)參數(shù)就是修改出的新值,也就是示例中的 N。

          有同學(xué)會(huì)問(wèn)了,Java 中只有整型的 CAS 函數(shù)嗎?有沒(méi)有針對(duì) double 型和 boolean 型的 CAS 函數(shù)?

          很可惜的是, Java 中 CAS 操作和 UnSafe 類(lèi)沒(méi)有提供對(duì)于 double 型和 boolean 型數(shù)據(jù)的操作方法。但我們可以利用現(xiàn)有方法進(jìn)行包裝,自制 double 型和 boolean 型數(shù)據(jù)的操作方法。

          • 對(duì)于 boolean 類(lèi)型,我們可以在入?yún)⒌臅r(shí)候?qū)?boolean 類(lèi)型轉(zhuǎn)為 int 類(lèi)型,在返回值的時(shí)候,將 int 類(lèi)型轉(zhuǎn)為 boolean 類(lèi)型。
          • 對(duì)于 double 類(lèi)型,則依賴(lài) long 類(lèi)型了, double 類(lèi)型提供了一種 double 類(lèi)型和 long 類(lèi)型互轉(zhuǎn)的函數(shù)。
          public?static?native?double?longBitsToDouble(
          ????long?bits)
          ;

          public?static?native?long?doubleToRawLongBits(
          ????double?value)
          ;

          大家都知道,基礎(chǔ)數(shù)據(jù)類(lèi)型在底層的存儲(chǔ)方式都是bit類(lèi)型。因此無(wú)論是long類(lèi)型還是double類(lèi)型在計(jì)算機(jī)底層存儲(chǔ)方式都是比特。所以就很好理解這兩個(gè)函數(shù)了:

          • longBitsToDouble 函數(shù)將 long 類(lèi)型底層的實(shí)際二進(jìn)制存儲(chǔ)數(shù)據(jù),用 double 類(lèi)型強(qiáng)行翻譯出來(lái)
          • doubleToRawLongBits 函數(shù)將 double 類(lèi)型底層的實(shí)際二進(jìn)制存儲(chǔ)數(shù)據(jù),用 long 類(lèi)型強(qiáng)行翻譯出來(lái)

          CAS 在 Java 中的使用

          一個(gè)比較常見(jiàn)的操作,使用變量 i 來(lái)為程序計(jì)數(shù),可以對(duì) i 自增來(lái)實(shí)現(xiàn)。

          int?i=0;
          i++;?

          但稍有經(jīng)驗(yàn)的同學(xué)都知道這種寫(xiě)法是線(xiàn)程不安全的。

          如果 500 個(gè)線(xiàn)程同時(shí)執(zhí)行一次 i++,得到 i 的結(jié)果不一定為 500,可能會(huì)比 500 小。

          這是因?yàn)?i++ 其實(shí)并不只是一行命令,它涉及以下幾個(gè)操作:(以下代碼為 Java 代碼編譯后的字節(jié)碼)

          getfield??#從內(nèi)存中獲取變量?i?的值
          iadd??????#將?count?加?1
          putfield??#將加?1?后的結(jié)果賦值給?i?變量

          可以看到,簡(jiǎn)簡(jiǎn)單單一個(gè)自增操作涉及這三個(gè)命令,而且這些命令并不是一氣呵成的,在多線(xiàn)程情況下很容易被別的線(xiàn)程打斷。

          自增操作

          雖然兩個(gè)線(xiàn)程都進(jìn)行了 i++ 的操作,i 的值本應(yīng)是 2,但是按上圖的流程來(lái)說(shuō),i 的值就變?yōu)?1 了

          如果需要執(zhí)行我們想要的操作,代碼可以這樣改寫(xiě)。

          int?i=0;
          synchronized{
          ????i++;
          }

          我們知道,通過(guò) synchronized 關(guān)鍵字修飾時(shí)代價(jià)很大,Java 提供了一個(gè) atomic 類(lèi),如果變量 i 被聲明為 atomic 類(lèi),并執(zhí)行對(duì)應(yīng)操作,就不會(huì)有之前所說(shuō)的問(wèn)題了,而且相較 synchronized 代價(jià)較小。

          AtomicInteger?i=?new?AtomicInteger(0);
          i.getAndIncrement();

          Java 的 Atomic 基礎(chǔ)數(shù)據(jù)類(lèi)型類(lèi)還提供

          • AtomicInteger 針對(duì) int 類(lèi)型的原子操作
          • AtomicLong 針對(duì) long 類(lèi)型的原子操作
          • AtomicBoolean 針對(duì) boolean 類(lèi)型的原子操作

          Atomic基礎(chǔ)數(shù)據(jù)類(lèi)型支持的方法如下圖所示:

          Atomic基礎(chǔ)數(shù)據(jù)類(lèi)型
          • getCurrentValue :獲取該基礎(chǔ)數(shù)據(jù)類(lèi)型的當(dāng)前值。
          • setValue :設(shè)置當(dāng)前基礎(chǔ)數(shù)據(jù)類(lèi)型的值為目標(biāo)值。
          • getAndSet :獲取該基礎(chǔ)數(shù)據(jù)類(lèi)型的當(dāng)前值并設(shè)置當(dāng)前基礎(chǔ)數(shù)據(jù)類(lèi)型的值為目標(biāo)值。
          • getAndIncrement :獲取該基礎(chǔ)數(shù)據(jù)類(lèi)型的當(dāng)前值并自增 1,類(lèi)似于 i++。
          • getAndDecrement :獲取該基礎(chǔ)數(shù)據(jù)類(lèi)型的當(dāng)前值并自減 1,類(lèi)似于 i--。
          • getAndAdd :獲取該基礎(chǔ)數(shù)據(jù)類(lèi)型的當(dāng)前值并自增給定參數(shù)的值。
          • IncrementAndGet :自增 1 并獲取增加后的該基礎(chǔ)數(shù)據(jù)類(lèi)型的值,類(lèi)似于 ++i。
          • decrementAndGet :自減 1 并獲取增加后的該基礎(chǔ)數(shù)據(jù)類(lèi)型的值,類(lèi)似于 --i。
          • AddAndGet :自增給定參數(shù)的值并獲取該基礎(chǔ)數(shù)據(jù)類(lèi)型自增后的值。

          這些基本數(shù)據(jù)類(lèi)型的函數(shù)底層實(shí)現(xiàn)都有 CAS 的身影。

          我們來(lái)拿最簡(jiǎn)單的 AtomicIntegergetAndIncrement 函數(shù)舉例吧:(源碼來(lái)源 JDK 7 )

          volatile?int?value;
          ···
          public?final?int?getAndIncrement(){
          ????for(;;){
          ????????int?current?=?get();
          ????????int?next=?current?+?1;
          ????????if(compareAndSet(current,?next))
          ????????????return?current;
          ????}
          }

          這就類(lèi)似之前的 i++ 自增操作,這里的 compareAndSet 其實(shí)就是封裝了 Unsafe 類(lèi)的一個(gè) native 函數(shù):

          public?final?compareAndSet(int?expect,?undate){
          ????return?unsafe.compareAndSwapInt
          ????(this,?valueOffset,?expect,?update);
          }

          也就回到了我們剛剛講述的 unsafe 包下的 compareAndSwapInt 函數(shù)了。

          自旋

          除了 CAS 之外,Atomic 類(lèi)還采用了一種方式優(yōu)化拿到鎖的過(guò)程。

          我們知道,當(dāng)一個(gè)線(xiàn)程拿不到對(duì)應(yīng)的鎖的時(shí)候,可以有兩種策略:

          策略 1:放棄獲得 CPU ,將線(xiàn)程置于阻塞狀態(tài),等待后續(xù)被操作系統(tǒng)喚醒和調(diào)度。

          當(dāng)然這么做的弊端很明顯,這種狀態(tài)的切換涉及到了用戶(hù)態(tài)到內(nèi)核態(tài)的切換,開(kāi)銷(xiāo)一般比較大,如果線(xiàn)程很快就把占用的鎖釋放了,這么做顯然是不合算的。

          策略 2:不放棄 CPU ,不停的重試,這種操作也稱(chēng)為自旋。

          當(dāng)然這么做也有弊端,如果某個(gè)線(xiàn)程持有鎖的時(shí)間過(guò)長(zhǎng),就會(huì)導(dǎo)致其它等待獲取鎖的線(xiàn)程一直在毫無(wú)意義的消耗 CPU 資源。使用不當(dāng)會(huì)造成 CPU 使用率極高。在這種情況下,策略 1 更合理一些。

          我們前文中所說(shuō)的 AtomicIntegerAtomicLong 在執(zhí)行相關(guān)操作的時(shí)候就采取策略 2。一般這種策略也被稱(chēng)為自旋鎖。

          可以看到在 AtomicIntegergetAndIncrement 函數(shù)中,函數(shù)外包了一個(gè)

          for(;;)

          其實(shí)就是一個(gè)不斷重試的死循環(huán),也就是這里說(shuō)的自旋。

          但現(xiàn)在大多采取的策略是開(kāi)發(fā)者設(shè)置一個(gè)門(mén)限值,在門(mén)限值內(nèi)進(jìn)行不斷地自旋。

          如果自旋失敗次數(shù)超過(guò)門(mén)限值了,那就采取進(jìn)入阻塞狀態(tài)。

          自旋

          ABA 問(wèn)題與 AtomicMarkable

          CAS 算法本身有一個(gè)很大的缺陷,那就是 ABA 問(wèn)題。

          我們可以看到, CAS 算法是基于值來(lái)做比較的,如果當(dāng)前有兩個(gè)線(xiàn)程,一個(gè)線(xiàn)程將變量值從 A 改為 B ,再由 B 改回為 A ,當(dāng)前線(xiàn)程開(kāi)始執(zhí)行 CAS 算法時(shí),就很容易認(rèn)為值沒(méi)有變化,誤認(rèn)為讀取數(shù)據(jù)到執(zhí)行 CAS 算法的期間,沒(méi)有線(xiàn)程修改過(guò)數(shù)據(jù)。

          ABA 問(wèn)題

          咋一看好像這個(gè)缺陷不會(huì)引發(fā)什么問(wèn)題,實(shí)則不然,給大家舉個(gè)例子吧。

          假設(shè)小艾銀行卡有 100 塊錢(qián)余額,且假定銀行轉(zhuǎn)賬操作就是一個(gè)單純的 CAS 命令,對(duì)比余額舊值是否與當(dāng)前值相同,如果相同則發(fā)生扣減/增加,我們將這個(gè)指令用 CAS(origin,expect) 表示。于是,我們看看接下來(lái)發(fā)生了什么:

          銀行轉(zhuǎn)賬
          1. 小明欠小艾100塊錢(qián),小艾欠小牛100塊錢(qián),

          2. 小艾在 ATM 1號(hào)機(jī)上打算 轉(zhuǎn)賬 100 塊錢(qián)給小牛;假設(shè)銀行轉(zhuǎn)賬底層是用CAS算法實(shí)現(xiàn)的。由于A(yíng)TM 1號(hào)機(jī)突然卡了,這時(shí)候小艾跑到旁邊的 ATM 2號(hào)機(jī)再次操作轉(zhuǎn)賬;

          3. ATM 2號(hào)機(jī)執(zhí)行了 CAS(100,0),順順利利地完成了轉(zhuǎn)賬,此時(shí)小艾的賬戶(hù)余額為 0;

          4. 小明這時(shí)候又給小艾賬上轉(zhuǎn)了 100,此時(shí)小艾賬上余額為 100;

          5. 這時(shí)候 ATM 1 網(wǎng)絡(luò)恢復(fù),繼續(xù)執(zhí)行 CAS(100,0),居然執(zhí)行成功了,小艾賬戶(hù)上余額又變?yōu)榱?0;

          可憐的小艾,由于 CAS 算法的缺陷,讓他損失了100塊錢(qián)。

          解決 ABA 問(wèn)題的方法也不復(fù)雜,對(duì)于這種 CAS 函數(shù),不僅要比較變量值,還需要比較版本號(hào)。

          public?boolean?compareAndSet(V?expectedReference,
          ?????????????????????????????V?newReference,?
          ?????????????????????????????int?expectedStamp,
          ?????????????????????????????int?newStamp)

          之前的 CAS 只有兩個(gè)參數(shù),帶上版本號(hào)比較的 CAS 就有四個(gè)參數(shù)了,其中 expectedReference 指的是變量預(yù)期的舊值, newReference 指的是變量需要更改成的新值, expectedStamp 指的是版本號(hào)的舊值, newStamp 指的是版本號(hào)新值。

          修改后的 CAS 算法執(zhí)行流程如下圖:

          改正 CAS 算法

          AtomicStampedReference

          那如何能在 Java 中順暢的使用帶版本號(hào)比較的 CAS 函數(shù)呢?

          Java 開(kāi)發(fā)人員都幫我們想好了,他們提供了一個(gè)類(lèi)叫做 Java 的 AtomicStampedReference ,該類(lèi)封裝了帶版本號(hào)比較的 CAS 函數(shù),一起來(lái)看看吧。

          AtomicStampedReference 定義在 java.util.concurrent.atomic 包下。

          下圖描述了該類(lèi)對(duì)應(yīng)的幾個(gè)常用方法:

          AtomicStampedReference
          • attemptStamp :如果 expectReference 和目前值一致,設(shè)置當(dāng)前對(duì)象的版本號(hào)戳為 newStamp
          • compareAndSet :該方法就是前文所述的帶版本號(hào)的 CAS 方法。
          • get :該方法返回當(dāng)前對(duì)象值和當(dāng)前對(duì)象的版本號(hào)戳
          • getReference :該方法返回當(dāng)前對(duì)象值
          • getStamp :該方法返回當(dāng)前對(duì)象的版本號(hào)戳
          • set :直接設(shè)置當(dāng)前對(duì)象值和對(duì)象的版本號(hào)戳

          參考:

          1. Java并發(fā)實(shí)現(xiàn)原理:JDK源碼剖析
          2. https://mp.weixin.qq.com/s/Ad6ufmGSEiQpL38YrvO4mw
          3. https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/atomic/AtomicStampedReference.html
          4. https://zhang0peter.blog.csdn.net/article/details/84020496?utm_medium=distribute.pc_relevant_t0.none-task-blog-searchFromBaidu-1.control&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-searchFromBaidu-1.control
          5. https://mp.weixin.qq.com/s/kvuPxn-vc8dke093XSE5IQ

          感謝各位少俠閱讀,我們將會(huì)為大家?guī)?lái)更多精彩原創(chuàng)文章

          瀏覽 51
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  青青草av网站 | 一区二区三区无码在线 | 琪琪亚洲 | 黄色A片播放 | 中文精品一区 |