欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

關(guān)于Java 并發(fā)的 CAS

 更新時(shí)間:2021年09月17日 14:24:43   作者:IT老哥  
后端開發(fā)鎖成為一個(gè)不可避免的話題,今天我們討論的是與之對(duì)應(yīng)的無(wú)鎖 CAS。本文會(huì)從怎么來(lái)的、是什么、怎么用、原理分析、遇到的問(wèn)題等不同的角度帶你真正搞懂 CAS。

一、為什么要無(wú)鎖

我們一想到在多線程下保證安全的方式頭一個(gè)要拎出來(lái)的肯定是鎖,不管從硬件、操作系統(tǒng)層面都或多或少在使用鎖。鎖有什么缺點(diǎn)嗎?當(dāng)然有了,不然 JDK 里為什么出現(xiàn)那么多各式各樣的鎖,就是因?yàn)槊恳环N鎖都有其優(yōu)劣勢(shì)。

使用鎖就需要獲得鎖、釋放鎖,CPU 需要通過(guò)上下文切換和調(diào)度管理來(lái)進(jìn)行這個(gè)操作,對(duì)于一個(gè) 「獨(dú)占鎖」 而言一個(gè)線程在持有鎖后沒(méi)執(zhí)行結(jié)束其他的哥們就必須在外面等著,等到前面的哥們執(zhí)行完畢 CPU 大哥就會(huì)把鎖拿出來(lái)其他的線程來(lái)?yè)屃耍ǚ枪剑?。鎖的這種概念基于一種悲觀機(jī)制,它總是認(rèn)為數(shù)據(jù)會(huì)被修改,所以你在操作一部分代碼塊之前先加一把鎖,操作完畢后再釋放,這樣就安全了。其實(shí)在 JDK1.5 使用 synchronized就可以做到。

但是像上面的操作在多線程下會(huì)讓 CPU 不斷的切換,非常消耗資源,我們知道可以使用具體的某一類鎖來(lái)避免部分問(wèn)題。那除了鎖的方式還有其他的嗎?當(dāng)然,有人就提出了無(wú)鎖算法,比較有名的就是我們今天要說(shuō)的 CAS(compare and swap),和鎖不同的是它是一種樂(lè)觀的機(jī)制,它認(rèn)為別人去拿數(shù)據(jù)的時(shí)候不會(huì)修改,但是在修改數(shù)據(jù)的時(shí)候去判斷一下數(shù)據(jù)此時(shí)的狀態(tài),這樣的話 CPU 不會(huì)切換,在讀多的情況下性能將得到大幅提升。當(dāng)前我們使用的大部分 CPU 都有 CAS 指令了,從硬件層面支持無(wú)鎖,這樣開發(fā)的時(shí)候去調(diào)用就可以了。

不論是鎖還是無(wú)鎖都有其優(yōu)劣勢(shì),后面我們也會(huì)通過(guò)例子說(shuō)明 CAS 的問(wèn)題。

二、什么是CAS?

前面提了無(wú)鎖的 CAS,那到底 CAS 是個(gè)啥呢?我已經(jīng)迫不及待了,我們來(lái)看看維基百科的解釋

比較并交換(compare and swap, CAS),是原子操作的一種,可用于在多線程編程中實(shí)現(xiàn)不被打斷的數(shù)據(jù)交換操作,從而避免多線程同時(shí)改寫某一數(shù)據(jù)時(shí)由于執(zhí)行順序不確定性以及中斷的不可預(yù)知性產(chǎn)生的數(shù)據(jù)不一致問(wèn)題。該操作通過(guò)將內(nèi)存中的值與指定數(shù)據(jù)進(jìn)行比較,當(dāng)數(shù)值一樣時(shí)將內(nèi)存中的數(shù)據(jù)替換為新的值。

CAS 給我們提供了一種思路,通過(guò) 「比較」「替換」 來(lái)完成原子性,來(lái)看一段代碼:

`int cas(long *addr, long old, long new) {`
 `/* 原子執(zhí)行 */`
 `if(*addr != old)`
 `return 0;`
 `*addr = new;`
 `return 1;`
`}`

這是一段 c 語(yǔ)言代碼,可以看到有 3 個(gè)參數(shù),分別是:

  • addr: 進(jìn)行比較的值
  • old: 內(nèi)存當(dāng)前值
  • new: 準(zhǔn)備修改的新值,寫入到內(nèi)存

只要我們當(dāng)前傳入的進(jìn)行比較的值和內(nèi)存里的值相等,就將新值修改成功,否則返回 0 告訴比較失敗了。學(xué)過(guò)數(shù)據(jù)庫(kù)的同學(xué)都知道悲觀鎖和樂(lè)觀鎖,樂(lè)觀鎖總是認(rèn)為數(shù)據(jù)不會(huì)被修改?;谶@種假設(shè) CAS 的操作也認(rèn)為內(nèi)存里的值和當(dāng)前值是相等的,所以操作總是能成功,我們可以不需要加鎖就實(shí)現(xiàn)多線程下的原子性操作。

在多線程情況下使用 CAS 同時(shí)更新同一個(gè)變量時(shí),只有其中一個(gè)線程能更新變量的值,而其它線程都失敗,失敗的線程并不會(huì)被阻塞掛起,而是告訴它這次修改失敗了,你可以重新嘗試,于是可以寫這樣的代碼。

`while (!cas(&addr, old, newValue)) {`
`}`
`// success`
`printf("new value = %ld", addr);`

不過(guò)這樣的代碼相信你可能看出其中的蹊蹺了,這個(gè)我們后面來(lái)分析,下面來(lái)看看 Java 里是怎么用 CAS 的。

三、Java 中的CAS

還是前面的問(wèn)題,如果讓你用 Java 的 API 來(lái)實(shí)現(xiàn)你可能會(huì)想到兩種方式,一種是加鎖(可能是 synchronized 或者其他種類的鎖),另一種是使用 atomic 類,如 AtomicInteger,這一系列類是在 JDK1.5 的時(shí)候出現(xiàn)的,在我們常用的 java.util.concurrent.atomic 包下,我們來(lái)看個(gè)例子:

`ExecutorService executorService = Executors.newCachedThreadPool();`
`AtomicInteger   atomicInteger   = new AtomicInteger(0);`
`for (int i = 0; i < 5000; i++) {`
 `executorService.execute(atomicInteger::incrementAndGet);`
`}`
`System.out.println(atomicInteger.get());`
`executorService.shutdown();`

這個(gè)例子開啟了 5000 個(gè)線程去進(jìn)行累加操作,不管你執(zhí)行多少次答案都是 5000。這么神奇的操作是如何實(shí)現(xiàn)的呢?就是依靠 CAS 這種技術(shù)來(lái)完成的,我們揭開 AtomicInteger 的老底看看它的代碼:

`public class AtomicInteger extends Number implements java.io.Serializable {`
 `private static final long serialVersionUID = 6214790243416807050L;`
 `// setup to use Unsafe.compareAndSwapInt for updates`
 `private static final Unsafe unsafe = Unsafe.getUnsafe();`
 `private static final long valueOffset;`
 `static {`
 `try {`
 `valueOffset = unsafe.objectFieldOffset`
 `(AtomicInteger.class.getDeclaredField("value"));`
 `} catch (Exception ex) { throw new Error(ex); }`
 `}`
 `private volatile int value;`
 `/**`
 `* Creates a new AtomicInteger with the given initial value.` 
 `* @param initialValue the initial value`
 `*/`
 `public AtomicInteger(int initialValue) {`
 `value = initialValue;`
 `}`
 `/**`
 `* Gets the current value.` 
 `* @return the current value`
 `*/`
 `public final int get() {`
 `return value;`
 `}`
 `/**`
 `* Atomically increments by one the current value.` 
 `* @return the updated value`
 `*/`
 `public final int incrementAndGet() {`
 `return unsafe.getAndAddInt(this, valueOffset, 1) + 1;`
 `}`
`}`

這里我只帖出了我們前面例子相關(guān)的代碼,其他都是類似的,可以看到 incrementAndGet 調(diào)用了 unsafe.getAndAddInt 方法。Unsafe 這個(gè)類是 JDK 提供的一個(gè)比較底層的類,它不讓我們程序員直接使用,主要是怕操作不當(dāng)把機(jī)器玩壞了。。。(其實(shí)可以通過(guò)反射的方式獲取到這個(gè)類的實(shí)例)你會(huì)在 JDK 源碼的很多地方看到這家伙,我們先說(shuō)說(shuō)它有什么能力:

  • 內(nèi)存管理:包括分配內(nèi)存、釋放內(nèi)存
  • 操作類、對(duì)象、變量:通過(guò)獲取對(duì)象和變量偏移量直接修改數(shù)據(jù)
  • 掛起與恢復(fù):將線程阻塞或者恢復(fù)阻塞狀態(tài)
  • CAS:調(diào)用 CPU 的 CAS 指令進(jìn)行比較和交換
  • 內(nèi)存屏障:定義內(nèi)存屏障,避免指令重排序

這里只是大致提一下常用的操作,具體細(xì)節(jié)可以在文末的參考鏈接中查看。下面我們繼續(xù)看 unsafe getAndAddInt 在做什么。

`public final int getAndAddInt(Object var1, long var2, int var4) {`
 `int var5;`
 `do {`
 `var5 = this.getIntVolatile(var1, var2);`
 `} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));`
 `return var5;`
`}`
`public native int getIntVolatile(Object var1, long var2);`
`public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);`

其實(shí)很簡(jiǎn)單,先通過(guò) getIntVolatile 獲取到內(nèi)存的當(dāng)前值,然后進(jìn)行比較,展開 compareAndSwapInt 方法的幾個(gè)參數(shù):

  • var1: 當(dāng)前要操作的對(duì)象(其實(shí)就是 AtomicInteger 實(shí)例)
  • var2: 當(dāng)前要操作的變量偏移量(可以理解為 CAS 中的內(nèi)存當(dāng)前值)
  • var4: 期望內(nèi)存中的值
  • var5: 要修改的新值

所以 this.compareAndSwapInt(var1, var2, var5, var5 + var4) 的意思就是,比較一下 var2 和內(nèi)存當(dāng)前值 var5 是否相等,如果相等那我就將內(nèi)存值 var5 修改為 var5 + var4(var4 就是 1,也可以是其他數(shù))。

這里我們還需要解釋一下 「偏移量」 是個(gè)啥?你在前面的代碼中可能看到這么一段:

`// setup to use Unsafe.compareAndSwapInt for updates`
`private static final Unsafe unsafe = Unsafe.getUnsafe();`
`private static final long valueOffset;`
`static {`
 `try {`
 `valueOffset = unsafe.objectFieldOffset`
 `(AtomicInteger.class.getDeclaredField("value"));`
 `} catch (Exception ex) { throw new Error(ex); }`
`}`
`private volatile int value;`

可以看出在靜態(tài)代碼塊執(zhí)行的時(shí)候?qū)?AtomicInteger 類的 value 這個(gè)字段的偏移量獲取出來(lái),拿這個(gè) long 數(shù)據(jù)干嘛呢?在 Unsafe 類里很多地方都需要傳入 obj 和偏移量,結(jié)合我們說(shuō) Unsafe 的諸多能力,其實(shí)就是直接通過(guò)更底層的方式將對(duì)象字段在內(nèi)存的數(shù)據(jù)修改掉。

使用上面的方式就可以很好的解決多線程下的原子性和可見(jiàn)性問(wèn)題。由于代碼里使用了 do while 這種循環(huán)結(jié)構(gòu),所以 CPU 不會(huì)被掛起,比較失敗后重試,就不存在上下文切換了,實(shí)現(xiàn)了無(wú)鎖并發(fā)編程

四、CAS存在的問(wèn)題

1.自旋的劣勢(shì)

你留意上面的代碼會(huì)發(fā)現(xiàn)一個(gè)問(wèn)題,while 循環(huán)如果在最壞情況下總是失敗怎么辦?會(huì)導(dǎo)致 CPU 在不斷處理。像這種 while(!compareAndSwapInt) 的操作我們稱之為自旋,CAS 是樂(lè)觀的,認(rèn)為大家來(lái)并不都是修改數(shù)據(jù)的,現(xiàn)實(shí)可能出現(xiàn)非常多的線程過(guò)來(lái)都要修改這個(gè)數(shù)據(jù),此時(shí)隨著并發(fā)量的增加會(huì)導(dǎo)致 CAS 操作長(zhǎng)時(shí)間不成功,CPU 也會(huì)有很大的開銷。所以我們要清楚,如果是讀多寫少的情況也就滿足樂(lè)觀,性能是非常好的。

2.ABA 問(wèn)題

提到 CAS 不得不說(shuō) ABA 問(wèn)題,它是說(shuō)假如內(nèi)存的值原來(lái)是 A,被一個(gè)線程修改為了 B,此時(shí)又有一個(gè)線程把它修改為了 A,那么 CAS 肯定是操作成功的。真的這樣做的話代碼可能就有 bug 了,對(duì)于修改數(shù)據(jù)為 B 的那個(gè)線程它應(yīng)該讀取到 B 而不是 A,如果你做過(guò)數(shù)據(jù)庫(kù)相關(guān)的樂(lè)觀鎖機(jī)制可能會(huì)想到我們?cè)诒容^的時(shí)候使用一個(gè)版本號(hào) version 來(lái)進(jìn)行判斷就可以搞定。在 JDK 里提供了一個(gè) AtomicStampedReference 類來(lái)解決這個(gè)問(wèn)題,來(lái)看一個(gè)例子:

`int stamp = 10001;`
`AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference<>(0, stamp);`
`stampedReference.compareAndSet(0, 10, stamp, stamp + 1);`
`System.out.println("value: " + stampedReference.getReference());`
`System.out.println("stamp: " + stampedReference.getStamp());`

它的構(gòu)造函數(shù)是 2 個(gè)參數(shù),多傳入了一個(gè)初始 時(shí)間戳,用這個(gè)戳來(lái)給數(shù)據(jù)加了一個(gè)版本,這樣的話多個(gè)線程來(lái)修改如果提供的戳不同。在修改數(shù)據(jù)的時(shí)候除了提供一個(gè)新的值之外還要提供一個(gè)新的戳,這樣在多線程情況下只要數(shù)據(jù)被修改了那么戳一定會(huì)發(fā)生改變,另一個(gè)線程拿到的是舊的戳所以會(huì)修改失敗。

3.嘗試應(yīng)用

既然 CAS 提供了這么好的 API,我們不妨用它來(lái)實(shí)現(xiàn)一個(gè)簡(jiǎn)易版的獨(dú)占鎖。思路是當(dāng)某個(gè)線程進(jìn)入 lock 方法就比較鎖對(duì)象的內(nèi)存值是否是 false,如果是則代表這把鎖它可以獲取,獲取后將內(nèi)存之修改為 true,獲取不到就自旋。在 unlock 的時(shí)候?qū)?nèi)存值再修改為 false 即可,代碼如下:

`public class SpinLock {`
 `private AtomicBoolean mutex = new AtomicBoolean(false);`
 `public void lock() {`
 `while (!mutex.compareAndSet(false, true)) {`
 `// System.out.println(Thread.currentThread().getName()+ " wait lock release");`
 `}`
 `}`
 `public void unlock() {`
 `while (!mutex.compareAndSet(true, false)) {`
 `// System.out.println(Thread.currentThread().getName()+ " wait lock release");`
 `}`
 `}`
`}`

這里使用了 AtomicBoolean 這個(gè)類,當(dāng)然用 AtomicInteger 也是可以的,因?yàn)槲覀冎槐4嬉粋€(gè)狀態(tài) boolean 占用比較小就用它了。這個(gè)鎖的實(shí)現(xiàn)比較簡(jiǎn)單,缺點(diǎn)非常明顯,由于 while 循環(huán)導(dǎo)致的自旋會(huì)讓其他線程都在占用 CPU,但是也可以使用,關(guān)于鎖的優(yōu)化版本實(shí)現(xiàn)我會(huì)在后續(xù)的文章中進(jìn)行改進(jìn)和說(shuō)明,正因?yàn)檫@些問(wèn)題我們也會(huì)在后續(xù)研究 AQS 這把利器的優(yōu)點(diǎn)。

4.CAS 源碼

看了上面的這些代碼和解釋相信你對(duì) CAS 已經(jīng)理解了,下面我們要說(shuō)的原理是前面的 native 方法中的 C++ 代碼寫了什么,在 openjdk 的 /hotspot/src/share/vm/prims 目錄中有一個(gè) Unsafe.cpp 文件中有這樣一段代碼:

注意:這里以 hotspot 實(shí)現(xiàn)為例

`UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))`
 `UnsafeWrapper("Unsafe_CompareAndSwapInt");`
 `oop p = JNIHandles::resolve(obj);`
 `// 通過(guò)偏移量獲取對(duì)象變量地址`
 `jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);`
 `// 執(zhí)行一個(gè)原子操作`
 `// 如果結(jié)果和現(xiàn)在不同,就直接返回,因?yàn)橛衅渌诵薷牧?;否則會(huì)一直嘗試去修改。直到成功。`
 `return (jint)(Atomic::cmpxchg(x, addr, e)) == e;`
`UNSAFE_ENDC`

到此這篇關(guān)于關(guān)于Java 并發(fā)的 CAS的文章就介紹到這了,更多相關(guān)Java 并發(fā)的 CAS內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Spring Boot整合ElasticSearch實(shí)現(xiàn)多版本兼容的方法詳解

    Spring Boot整合ElasticSearch實(shí)現(xiàn)多版本兼容的方法詳解

    簡(jiǎn)單說(shuō),ElasticSearch(簡(jiǎn)稱 ES)是搜索引擎,是結(jié)構(gòu)化數(shù)據(jù)的分布式搜索引擎。下面這篇文章主要給大家介紹了關(guān)于Spring Boot整合ElasticSearch實(shí)現(xiàn)多版本兼容的相關(guān)資料,需要的朋友可以參考借鑒,下面來(lái)一起看看吧
    2018-05-05
  • 詳解RabbitMq如何做到消息的可靠性投遞

    詳解RabbitMq如何做到消息的可靠性投遞

    這篇文章主要為大家介紹了RabbitMq如何做到消息的可靠性投遞,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-09-09
  • springMVC如何對(duì)輸入數(shù)據(jù)校驗(yàn)實(shí)現(xiàn)代碼

    springMVC如何對(duì)輸入數(shù)據(jù)校驗(yàn)實(shí)現(xiàn)代碼

    數(shù)據(jù)的校驗(yàn)是交互式網(wǎng)站一個(gè)不可或缺的功能,數(shù)據(jù)驗(yàn)證分為客戶端驗(yàn)證和服務(wù)器端驗(yàn)證,這篇文章主要介紹了springMVC如何對(duì)輸入數(shù)據(jù)校驗(yàn),需要的朋友可以參考下
    2020-10-10
  • 基于java中泛型的總結(jié)分析

    基于java中泛型的總結(jié)分析

    本篇文章介紹了,在java中泛型的總結(jié)分析。需要的朋友參考下
    2013-05-05
  • 通過(guò)圖例了解IDEA引入JQuery實(shí)現(xiàn)步驟

    通過(guò)圖例了解IDEA引入JQuery實(shí)現(xiàn)步驟

    這篇文章主要介紹了IDEA引入JQuery實(shí)現(xiàn)步驟圖解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2020-09-09
  • 詳解java代碼中init method和destroy method的三種使用方式

    詳解java代碼中init method和destroy method的三種使用方式

    這篇文章主要介紹了詳解java代碼中init method和destroy method的三種使用方式,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2020-03-03
  • Spring Boot 中的 Spring Cloud Feign的原理解析

    Spring Boot 中的 Spring Cloud Feign的原

    Spring Cloud Feign 是 Spring Cloud 中的一個(gè)組件,它可以幫助我們實(shí)現(xiàn)聲明式的 REST 客戶,這篇文章主要介紹了Spring Boot 中的 Spring Cloud Feign,需要的朋友可以參考下
    2023-07-07
  • 淺談redis key值內(nèi)存消耗以及性能影響

    淺談redis key值內(nèi)存消耗以及性能影響

    這篇文章主要介紹了淺談redis key值內(nèi)存消耗以及性能影響,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧
    2021-02-02
  • Java中的ArrayList、LinkedList、HashSet等容器詳解

    Java中的ArrayList、LinkedList、HashSet等容器詳解

    這篇文章主要介紹了Java中的ArrayList、LinkedList、HashSet等容器詳解,集合表示一組對(duì)象,稱為其元素,有些集合允許重復(fù)元素,而另一些則不允許,有些是有序的,有些是無(wú)序的,需要的朋友可以參考下
    2023-08-08
  • Spring boot使用spring retry重試機(jī)制的方法示例

    Spring boot使用spring retry重試機(jī)制的方法示例

    這篇文章主要介紹了Spring boot使用spring retry重試機(jī)制的方法示例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2020-01-01

最新評(píng)論