日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程语言 > java >内容正文

java

Java CAS 原理分析

發(fā)布時(shí)間:2025/3/21 java 52 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Java CAS 原理分析 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

1.簡介

CAS 全稱是 compare and swap,是一種用于在多線程環(huán)境下實(shí)現(xiàn)同步功能的機(jī)制。CAS 操作包含三個(gè)操作數(shù) – 內(nèi)存位置、預(yù)期數(shù)值和新值。CAS 的實(shí)現(xiàn)邏輯是將內(nèi)存位置處的數(shù)值與預(yù)期數(shù)值想比較,若相等,則將內(nèi)存位置處的值替換為新值。若不相等,則不做任何操作。

在 Java 中,Java 并沒有直接實(shí)現(xiàn) CAS,CAS 相關(guān)的實(shí)現(xiàn)是通過 C++ 內(nèi)聯(lián)匯編的形式實(shí)現(xiàn)的。Java 代碼需通過 JNI 才能調(diào)用。關(guān)于實(shí)現(xiàn)上的細(xì)節(jié),我將會在第3章進(jìn)行分析。

前面說了 CAS 操作的流程,并不是很難。但僅有上面的說明還不夠,接下來我將會再介紹一點(diǎn)其他的背景知識。有這些背景知識,才能更好的理解后續(xù)的內(nèi)容。

?2.背景介紹

我們都知道,CPU 是通過總線和內(nèi)存進(jìn)行數(shù)據(jù)傳輸?shù)摹T诙嗪诵臅r(shí)代下,多個(gè)核心通過同一條總線和內(nèi)存以及其他硬件進(jìn)行通信。如下圖:


圖片出處:《深入理解計(jì)算機(jī)系統(tǒng)》

上圖是一個(gè)較為簡單的計(jì)算機(jī)結(jié)構(gòu)圖,雖然簡單,但足以說明問題。在上圖中,CPU 通過兩個(gè)藍(lán)色箭頭標(biāo)注的總線與內(nèi)存進(jìn)行通信。大家考慮一個(gè)問題,CPU 的多個(gè)核心同時(shí)對同一片內(nèi)存進(jìn)行操作,若不加以控制,會導(dǎo)致什么樣的錯(cuò)誤?這里簡單說明一下,假設(shè)核心1經(jīng)32位帶寬的總線向內(nèi)存寫入64位的數(shù)據(jù),核心1要進(jìn)行兩次寫入才能完成整個(gè)操作。若在核心1第一次寫入32位的數(shù)據(jù)后,核心2從核心1寫入的內(nèi)存位置讀取了64位數(shù)據(jù)。由于核心1還未完全將64位的數(shù)據(jù)全部寫入內(nèi)存中,核心2就開始從該內(nèi)存位置讀取數(shù)據(jù),那么讀取出來的數(shù)據(jù)必定是混亂的。

不過對于這個(gè)問題,實(shí)際上不用擔(dān)心。通過 Intel 開發(fā)人員手冊,我們可以了解到自奔騰處理器開始,Intel 處理器會保證以原子的方式讀寫按64位邊界對齊的四字(quadword)。

根據(jù)上面的說明,我們可總結(jié)出,Intel 處理器可以保證單次訪問內(nèi)存對齊的指令以原子的方式執(zhí)行。但如果是兩次訪存的指令呢?答案是無法保證。比如遞增指令inc dword ptr [...],等價(jià)于DEST = DEST + 1。該指令包含三個(gè)操作讀->改->寫,涉及兩次訪存。考慮這樣一種情況,在內(nèi)存指定位置處,存放了一個(gè)為1的數(shù)值。現(xiàn)在 CPU 兩個(gè)核心同時(shí)執(zhí)行該條指令。兩個(gè)核心交替執(zhí)行的流程如下:

  • 核心1 從內(nèi)存指定位置出讀取數(shù)值1,并加載到寄存器中
  • 核心2 從內(nèi)存指定位置出讀取數(shù)值1,并加載到寄存器中
  • 核心1 將寄存器中值遞減1
  • 核心2 將寄存器中值遞減1
  • 核心1 將修改后的值寫回內(nèi)存
  • 核心2 將修改后的值寫回內(nèi)存
  • 經(jīng)過執(zhí)行上述流程,內(nèi)存中的最終值時(shí)2,而我們期待的是3,這就出問題了。要處理這個(gè)問題,就要避免兩個(gè)或多個(gè)核心同時(shí)操作同一片內(nèi)存區(qū)域。那么怎樣避免呢?這就要引入本文的主角 - lock 前綴。關(guān)于該指令的詳細(xì)描述,可以參考 Intel 開發(fā)人員手冊 Volume 2 Instruction Set Reference,Chapter 3 Instruction Set Reference A-L。我這里引用其中的一段,如下:

    LOCK—Assert LOCK# Signal Prefix
    Causes the processor’s LOCK# signal to be asserted during execution of the accompanying instruction (turns the instruction into an atomic instruction). In a multiprocessor environment, the LOCK# signal?ensures that the processor has exclusive use of any shared memory?while the signal is asserted.

    上面描述的重點(diǎn)已經(jīng)用黑體標(biāo)出了,在多處理器環(huán)境下,LOCK# 信號可以確保處理器獨(dú)占使用某些共享內(nèi)存。lock 可以被添加在下面的指令前:

    ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG, CMPXCH8B, CMPXCHG16B, DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD, and XCHG.

    通過在 inc 指令前添加 lock 前綴,即可讓該指令具備原子性。多個(gè)核心同時(shí)執(zhí)行同一條 inc 指令時(shí),會以串行的方式進(jìn)行,也就避免了上面所說的那種情況。那么這里還有一個(gè)問題,lock 前綴是怎樣保證核心獨(dú)占某片內(nèi)存區(qū)域的呢?答案如下:

    在 Intel 處理器中,有兩種方式保證處理器的某個(gè)核心獨(dú)占某片內(nèi)存區(qū)域。第一種方式是通過鎖定總線,讓某個(gè)核心獨(dú)占使用總線,但這樣代價(jià)太大。總線被鎖定后,其他核心就不能訪問內(nèi)存了,可能會導(dǎo)致其他核心短時(shí)內(nèi)停止工作。第二種方式是鎖定緩存,若某處內(nèi)存數(shù)據(jù)被緩存在處理器緩存中。處理器發(fā)出的 LOCK# 信號不會鎖定總線,而是鎖定緩存行對應(yīng)的內(nèi)存區(qū)域。其他處理器在這片內(nèi)存區(qū)域鎖定期間,無法對這片內(nèi)存區(qū)域進(jìn)行相關(guān)操作。相對于鎖定總線,鎖定緩存的代價(jià)明顯比較小。關(guān)于總線鎖和緩存鎖,更詳細(xì)的描述請參考 Intel 開發(fā)人員手冊 Volume 3 Software Developer’s Manual,Chapter 8 Multiple-Processor Management。

    ?3.源碼分析

    有了上面的背景知識,現(xiàn)在我們就可以從容不迫的閱讀 CAS 的源碼了。本章的內(nèi)容將對 java.util.concurrent.atomic 包下的原子類 AtomicInteger 中的 compareAndSet 方法進(jìn)行分析,相關(guān)分析如下:

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public class AtomicInteger extends Number implements java.io.Serializable {// setup to use Unsafe.compareAndSwapInt for updatesprivate static final Unsafe unsafe = Unsafe.getUnsafe();private static final long valueOffset;static {try {// 計(jì)算變量 value 在類對象中的偏移valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));} catch (Exception ex) { throw new Error(ex); }}private volatile int value;public final boolean compareAndSet(int expect, int update) {/** compareAndSet 實(shí)際上只是一個(gè)殼子,主要的邏輯封裝在 Unsafe 的 * compareAndSwapInt 方法中*/return unsafe.compareAndSwapInt(this, valueOffset, expect, update);}// ...... }public final class Unsafe {// compareAndSwapInt 是 native 類型的方法,繼續(xù)往下看public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x);// ...... }
    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 // unsafe.cpp /** 這個(gè)看起來好像不像一個(gè)函數(shù),不過不用擔(dān)心,不是重點(diǎn)。UNSAFE_ENTRY 和 UNSAFE_END 都是宏,* 在預(yù)編譯期間會被替換成真正的代碼。下面的 jboolean、jlong 和 jint 等是一些類型定義(typedef):* * jni.h* typedef unsigned char jboolean;* typedef unsigned short jchar;* typedef short jshort;* typedef float jfloat;* typedef double jdouble;* * jni_md.h* typedef int jint;* #ifdef _LP64 // 64-bit* typedef long jlong;* #else* typedef long long jlong;* #endif* typedef signed char jbyte;*/ 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);// 根據(jù)偏移量,計(jì)算 value 的地址。這里的 offset 就是 AtomaicInteger 中的 valueOffsetjint* addr = (jint *) index_oop_from_field_offset_long(p, offset);// 調(diào)用 Atomic 中的函數(shù) cmpxchg,該函數(shù)聲明于 Atomic.hpp 中return (jint)(Atomic::cmpxchg(x, addr, e)) == e; UNSAFE_END// atomic.cpp unsigned Atomic::cmpxchg(unsigned int exchange_value,volatile unsigned int* dest, unsigned int compare_value) {assert(sizeof(unsigned int) == sizeof(jint), "more work to do");/** 根據(jù)操作系統(tǒng)類型調(diào)用不同平臺下的重載函數(shù),這個(gè)在預(yù)編譯期間編譯器會決定調(diào)用哪個(gè)平臺下的重載* 函數(shù)。相關(guān)的預(yù)編譯邏輯如下:* * atomic.inline.hpp:* #include "runtime/atomic.hpp"* * // Linux* #ifdef TARGET_OS_ARCH_linux_x86* # include "atomic_linux_x86.inline.hpp"* #endif* * // 省略部分代碼* * // Windows* #ifdef TARGET_OS_ARCH_windows_x86* # include "atomic_windows_x86.inline.hpp"* #endif* * // BSD* #ifdef TARGET_OS_ARCH_bsd_x86* # include "atomic_bsd_x86.inline.hpp"* #endif* * 接下來分析 atomic_windows_x86.inline.hpp 中的 cmpxchg 函數(shù)實(shí)現(xiàn)*/return (unsigned int)Atomic::cmpxchg((jint)exchange_value, (volatile jint*)dest,(jint)compare_value); }

    上面的分析看起來比較多,不過主流程并不復(fù)雜。如果不糾結(jié)于代碼細(xì)節(jié),還是比較容易看懂的。接下來,我會分析 Windows 平臺下的 Atomic::cmpxchg 函數(shù)。繼續(xù)往下看吧。

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 // atomic_windows_x86.inline.hpp #define LOCK_IF_MP(mp) __asm cmp mp, 0 \__asm je L0 \__asm _emit 0xF0 \__asm L0:inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {// alternative for InterlockedCompareExchangeint mp = os::is_MP();__asm {mov edx, destmov ecx, exchange_valuemov eax, compare_valueLOCK_IF_MP(mp)cmpxchg dword ptr [edx], ecx} }

    上面的代碼由 LOCK_IF_MP 預(yù)編譯標(biāo)識符和 cmpxchg 函數(shù)組成。為了看到更清楚一些,我們將 cmpxchg 函數(shù)中的 LOCK_IF_MP 替換為實(shí)際內(nèi)容。如下:

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {// 判斷是否是多核 CPUint mp = os::is_MP();__asm {// 將參數(shù)值放入寄存器中mov edx, dest // 注意: dest 是指針類型,這里是把內(nèi)存地址存入 edx 寄存器中mov ecx, exchange_valuemov eax, compare_value// LOCK_IF_MPcmp mp, 0/** 如果 mp = 0,表明是線程運(yùn)行在單核 CPU 環(huán)境下。此時(shí) je 會跳轉(zhuǎn)到 L0 標(biāo)記處,* 也就是越過 _emit 0xF0 指令,直接執(zhí)行 cmpxchg 指令。也就是不在下面的 cmpxchg 指令* 前加 lock 前綴。*/je L0/** 0xF0 是 lock 前綴的機(jī)器碼,這里沒有使用 lock,而是直接使用了機(jī)器碼的形式。至于這樣做的* 原因可以參考知乎的一個(gè)回答:* https://www.zhihu.com/question/50878124/answer/123099923*/ _emit 0xF0 L0:/** 比較并交換。簡單解釋一下下面這條指令,熟悉匯編的朋友可以略過下面的解釋:* cmpxchg: 即“比較并交換”指令* dword: 全稱是 double word,在 x86/x64 體系中,一個(gè) * word = 2 byte,dword = 4 byte = 32 bit* ptr: 全稱是 pointer,與前面的 dword 連起來使用,表明訪問的內(nèi)存單元是一個(gè)雙字單元* [edx]: [...] 表示一個(gè)內(nèi)存單元,edx 是寄存器,dest 指針值存放在 edx 中。* 那么 [edx] 表示內(nèi)存地址為 dest 的內(nèi)存單元* * 這一條指令的意思就是,將 eax 寄存器中的值(compare_value)與 [edx] 雙字內(nèi)存單元中的值* 進(jìn)行對比,如果相同,則將 ecx 寄存器中的值(exchange_value)存入 [edx] 內(nèi)存單元中。*/cmpxchg dword ptr [edx], ecx} }

    到這里 CAS 的實(shí)現(xiàn)過程就講完了,CAS 的實(shí)現(xiàn)離不開處理器的支持。以上這么多代碼,其實(shí)核心代碼就是一條帶lock 前綴的 cmpxchg 指令,即lock cmpxchg dword ptr [edx], ecx。

    ?4.ABA 問題

    談到 CAS,基本上都要談一下 CAS 的 ABA 問題。CAS 由三個(gè)步驟組成,分別是“讀取->比較->寫回”。考慮這樣一種情況,線程1和線程2同時(shí)執(zhí)行 CAS 邏輯,兩個(gè)線程的執(zhí)行順序如下:

  • 時(shí)刻1:線程1執(zhí)行讀取操作,獲取原值 A,然后線程被切換走
  • 時(shí)刻2:線程2執(zhí)行完成 CAS 操作將原值由 A 修改為 B
  • 時(shí)刻3:線程2再次執(zhí)行 CAS 操作,并將原值由 B 修改為 A
  • 時(shí)刻4:線程1恢復(fù)運(yùn)行,將比較值(compareValue)與原值(oldValue)進(jìn)行比較,發(fā)現(xiàn)兩個(gè)值相等。
    然后用新值(newValue)寫入內(nèi)存中,完成 CAS 操作
  • 如上流程,線程1并不知道原值已經(jīng)被修改過了,在它看來并沒什么變化,所以它會繼續(xù)往下執(zhí)行流程。對于 ABA 問題,通常的處理措施是對每一次 CAS 操作設(shè)置版本號。java.util.concurrent.atomic 包下提供了一個(gè)可處理 ABA 問題的原子類 AtomicStampedReference,具體的實(shí)現(xiàn)這里就不分析了,有興趣的朋友可以自己去看看。

    ?5.總結(jié)

    寫到這里,這篇文章總算接近尾聲了。雖然 CAS 本身的原理,包括實(shí)現(xiàn)都不是很難,但是寫起來真的不太好寫。這里面涉及到了一些底層的知識,雖然能看懂,但想說明白,還是有點(diǎn)難度的。由于我底層的知識比較欠缺,上面的一些分析難免會出錯(cuò)。所以如有錯(cuò)誤,請輕噴,當(dāng)然最好能說明怎么錯(cuò)的,感謝。

    好了,本篇文章就到這里。感謝閱讀,再見。

    ?參考

    • Compare-and-swap - wikipedia
    • 多核環(huán)境下的內(nèi)存屏障指令 - 云風(fēng)
    • Intel? 64 and IA-32 Architectures Software Developer’s Manual
    • 一條C語言語句不一定是原子操作,但是一個(gè)匯編指令是原子操作嗎?- 知乎
    • 下面這個(gè)宏中的emit指令是干什么的?- 知乎
    • 消失的北橋 - txwm8905

    ?附錄

    在前面源碼分析一節(jié)中用到的幾個(gè)文件,這里把路徑貼出來。有助于大家進(jìn)行索引,如下:

    文件名路徑
    Unsafe.javaopenjdk/jdk/src/share/classes/sun/misc/Unsafe.java
    unsafe.cppopenjdk/hotspot/src/share/vm/prims/unsafe.cpp
    atomic.cppopenjdk/hotspot/src/share/vm/runtime/atomic.cpp
    atomic_windows_x86.inline.hppopenjdk/hotspot/src/os_cpu/windows_x86/vm/atomic_windows_x86.inline.hpp
    • 本文鏈接:?https://www.tianxiaobo.com/2018/05/15/Java-中的-CAS-原理分析/

    from:?http://www.tianxiaobo.com/2018/05/15/Java-%E4%B8%AD%E7%9A%84-CAS-%E5%8E%9F%E7%90%86%E5%88%86%E6%9E%90/?

    總結(jié)

    以上是生活随笔為你收集整理的Java CAS 原理分析的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。