自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

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

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

[[376022]]

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

引子

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

在聊樂觀鎖之前,先給大家復習一個概念:原子操作:

什么是原子操作呢?

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

原子操作

 

聊完原子操作了,我們進入正題。

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

悲觀鎖

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

樂觀鎖

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

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

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

CAS 算法流程

 

CAS 算法的思路如下:

該算法認為不同線程對變量的操作時產生競爭的情況比較少。

該算法的核心是對當前讀取變量值 E 和內存中的變量舊值 V 進行比較。

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

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

當線程運行 CAS 算法時,該運行過程是原子操作,也就是說,Compare And Swap 這個過程雖然涉及邏輯比較繁冗,但具體操作一氣呵成。

Java中 CAS 的底層

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

什么是指針?

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

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

弊端就在于指針是語言中功能強大的組件,如果一個新手在編程時,沒有考慮指針的安全性,錯誤的操作指針把某塊不該修改的內存值修改,容易導致整個程序崩潰。

錯誤使用指針

 

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

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

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 型變量三個函數(shù)。

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

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

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

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

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

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

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

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

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

 

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

 

CAS 在 Java 中的使用

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

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

但稍有經驗的同學都知道這種寫法是線程不安全的。

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

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

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

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

自增操作

 

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

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

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

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

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

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

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

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

Atomic基礎數(shù)據類型

 

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

這些基本數(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 其實就是封裝了 Unsafe 類的一個 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)化拿到鎖的過程。

我們知道,當一個線程拿不到對應的鎖的時候,可以有兩種策略:

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

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

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

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

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

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

  1. for(;;) 

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

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

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

自旋

 

ABA 問題與 AtomicMarkable

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

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

ABA 問題

 

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

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

銀行轉賬

 

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

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

 

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

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

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

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

改正 CAS 算法

 

AtomicStampedReference

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

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

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

下圖描述了該類對應的幾個常用方法:

 

 

 

AtomicStampedReference

 

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

參考:

Java并發(fā)實現(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

 

責任編輯:武曉燕 來源: 后端技術小牛說
相關推薦

2020-07-06 08:03:32

Java悲觀鎖樂觀鎖

2024-01-29 01:08:01

悲觀鎖遞歸鎖讀寫鎖

2023-10-13 00:00:00

并發(fā)樂觀鎖CAS

2023-07-05 08:18:54

Atomic類樂觀鎖悲觀鎖

2010-08-18 09:00:38

數(shù)據庫

2023-11-09 15:28:32

Spring開發(fā)

2013-01-30 10:12:24

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

2014-08-08 13:30:44

Nginx

2024-05-17 09:33:22

樂觀鎖CASversion

2019-11-28 16:00:06

重入鎖讀寫鎖樂觀鎖

2009-09-24 14:43:53

Hibernate樂觀

2023-08-17 14:10:11

Java開發(fā)前端

2021-03-30 09:45:11

悲觀鎖樂觀鎖Optimistic

2019-01-04 11:18:35

獨享鎖共享鎖非公平鎖

2025-02-20 00:01:00

2017-08-04 11:41:53

Javathreadpool框架

2017-08-07 20:50:27

JavaForkJoin

2024-09-03 15:14:42

2025-04-23 08:45:00

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

2024-10-18 10:04:01

點贊
收藏

51CTO技術棧公眾號