偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

淺談 Java 并發(fā)下的樂觀鎖

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

[[376022]]

本文轉(zhuǎn)載自微信公眾號「后端技術(shù)小牛說」,作者小明  。轉(zhuǎn)載本文請聯(lián)系后端技術(shù)小牛說公眾號。 

引子

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

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

什么是原子操作呢?

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

原子操作

 

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

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

悲觀鎖

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

樂觀鎖

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

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

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

CAS 算法流程

 

CAS 算法的思路如下:

該算法認(rèn)為不同線程對變量的操作時(shí)產(chǎn)生競爭的情況比較少。

該算法的核心是對當(dāng)前讀取變量值 E 和內(nèi)存中的變量舊值 V 進(jìn)行比較。

如果相等,就代表其他線程沒有對該變量進(jìn)行修改,就將變量值更新為新值 N。

如果不等,就認(rèn)為在讀取值 E 到比較階段,有其他線程對變量進(jìn)行過修改,不進(jìn)行任何操作。

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

Java中 CAS 的底層

實(shí)現(xiàn)Java 中的 Unsafe 類我先問大家一個(gè)問題:

什么是指針?

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

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

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

錯(cuò)誤使用指針

 

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

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

Unsafe 類中的 CAS

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

  1. public final native boolean compareAndSwapObject( 
  2.     Object paramObject1,  
  3.     long paramLong,  
  4.     Object paramObject2,  
  5.     Object paramObject3); 
  6.  
  7. public final native boolean compareAndSwapInt( 
  8.     Object paramObject,  
  9.     long paramLong,  
  10.     int paramInt1,  
  11.     int paramInt2); 
  12.  
  13. public final native boolean compareAndSwapLong( 
  14.     Object paramObject,  
  15.     long paramLong1,  
  16.     long paramLong2,  
  17.     long paramLong3); 

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

我們以最好理解的 compareAndSwapInt 為例,來看一下吧:

  1. public final native boolean compareAndSwapInt( 
  2.     Object paramObject,  
  3.     long paramLong,  
  4.     int paramInt1,  
  5.     int paramInt2); 

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

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

有同學(xué)會問了,Java 中只有整型的 CAS 函數(shù)嗎?有沒有針對 double 型和 boolean 型的 CAS 函數(shù)?

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

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

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

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

 

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

 

CAS 在 Java 中的使用

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

  1. int i=0; 
  2. i++;  

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

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

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

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

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

自增操作

 

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

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

  1. int i=0; 
  2. synchronized{ 
  3.     i++; 

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

  1. AtomicInteger i= new AtomicInteger(0); 
  2. i.getAndIncrement(); 

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

  • AtomicInteger 針對 int 類型的原子操作
  • AtomicLong 針對 long 類型的原子操作
  • AtomicBoolean 針對 boolean 類型的原子操作

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

Atomic基礎(chǔ)數(shù)據(jù)類型

 

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

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

我們來拿最簡單的 AtomicInteger 的 getAndIncrement 函數(shù)舉例吧:(源碼來源 JDK 7 )

  1. volatile int value; 
  2. ··· 
  3. public final int getAndIncrement(){ 
  4.     for(;;){ 
  5.         int current = get(); 
  6.         int nextcurrent + 1; 
  7.         if(compareAndSet(currentnext)) 
  8.             return current
  9.     } 

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

  1. public final compareAndSet(int expect, undate){ 
  2.     return unsafe.compareAndSwapInt 
  3.     (this, valueOffset, expect, update); 

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

自旋

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

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

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

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

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

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

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

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

  1. for(;;) 

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

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

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

自旋

 

ABA 問題與 AtomicMarkable

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

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

ABA 問題

 

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

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

銀行轉(zhuǎn)賬

 

  1. 小明欠小艾100塊錢,小艾欠小牛100塊錢,
  2. 小艾在 ATM 1號機(jī)上打算 轉(zhuǎn)賬 100 塊錢給小牛;假設(shè)銀行轉(zhuǎn)賬底層是用CAS算法實(shí)現(xiàn)的。由于ATM 1號機(jī)突然卡了,這時(shí)候小艾跑到旁邊的 ATM 2號機(jī)再次操作轉(zhuǎn)賬;
  3. ATM 2號機(jī)執(zhí)行了 CAS(100,0),順順利利地完成了轉(zhuǎn)賬,此時(shí)小艾的賬戶余額為 0;
  4. 小明這時(shí)候又給小艾賬上轉(zhuǎn)了 100,此時(shí)小艾賬上余額為 100;
  5. 這時(shí)候 ATM 1 網(wǎng)絡(luò)恢復(fù),繼續(xù)執(zhí)行 CAS(100,0),居然執(zhí)行成功了,小艾賬戶上余額又變?yōu)榱?0;

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

 

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

  1. public boolean compareAndSet(V expectedReference, 
  2.                              V newReference,  
  3.                              int expectedStamp, 
  4.                              int newStamp) 

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

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

改正 CAS 算法

 

AtomicStampedReference

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

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

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

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

 

 

 

AtomicStampedReference

 

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

參考:

Java并發(fā)實(shí)現(xiàn)原理:JDK源碼剖析

https://mp.weixin.qq.com/s/Ad6ufmGSEiQpL38YrvO4mw

https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/atomic/AtomicStampedReference.html

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

 

https://mp.weixin.qq.com/s/kvuPxn-vc8dke093XSE5IQ

 

責(zé)任編輯:武曉燕 來源: 后端技術(shù)小牛說
相關(guān)推薦

2020-07-06 08:03:32

Java悲觀鎖樂觀鎖

2023-07-05 08:18:54

Atomic類樂觀鎖悲觀鎖

2023-10-13 00:00:00

并發(fā)樂觀鎖CAS

2024-01-29 01:08:01

悲觀鎖遞歸鎖讀寫鎖

2010-08-18 09:00:38

數(shù)據(jù)庫

2014-08-08 13:30:44

Nginx

2013-01-30 10:12:24

NginxNginx優(yōu)化高并發(fā)

2023-11-09 15:28:32

Spring開發(fā)

2024-05-17 09:33:22

樂觀鎖CASversion

2009-09-24 14:43:53

Hibernate樂觀

2019-11-28 16:00:06

重入鎖讀寫鎖樂觀鎖

2025-02-20 00:01:00

2021-03-30 09:45:11

悲觀鎖樂觀鎖Optimistic

2017-08-04 11:41:53

Javathreadpool框架

2017-08-07 20:50:27

JavaForkJoin

2023-08-17 14:10:11

Java開發(fā)前端

2025-04-23 08:45:00

悲觀鎖樂觀鎖并發(fā)控制機(jī)制

2024-09-03 15:14:42

2024-10-18 10:04:01

2019-01-04 11:18:35

獨(dú)享鎖共享鎖非公平鎖
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號