国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

Java CAS 原理分析

ralap / 2272人閱讀

摘要:現(xiàn)在兩個核心同時執(zhí)行該條指令。至于這樣做的原因可以參考知乎的一個回答比較并交換。那么表示內(nèi)存地址為的內(nèi)存單元這一條指令的意思就是,將寄存器中的值與雙字內(nèi)存單元中的值進行對比,如果相同,則將寄存器中的值存入內(nèi)存單元中。

1.簡介

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

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

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

2.背景介紹

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


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

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

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

根據(jù)上面的說明,我們可總結(jié)出,Intel 處理器可以保證單次訪問內(nèi)存對齊的指令以原子的方式執(zhí)行。但如果是兩次訪存的指令呢?答案是無法保證。比如遞增指令inc dword ptr [...],等價于DEST = DEST + 1。該指令包含三個操作讀->改->寫,涉及兩次訪存。考慮這樣一種情況,在內(nèi)存指定位置處,存放了一個為1的數(shù)值。現(xiàn)在 CPU 兩個核心同時執(zhí)行該條指令。兩個核心交替執(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)存中的最終值時2,而我們期待的是3,這就出問題了。要處理這個問題,就要避免兩個或多個核心同時操作同一片內(nèi)存區(qū)域。那么怎樣避免呢?這就要引入本文的主角 - lock 前綴。關(guān)于該指令的詳細描述,可以參考 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.

上面描述的重點已經(jīng)用黑體標出了,在多處理器環(huán)境下,LOCK# 信號可以確保處理器獨占使用某些共享內(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 前綴,即可讓該指令具備原子性。多個核心同時執(zhí)行同一條 inc 指令時,會以串行的方式進行,也就避免了上面所說的那種情況。那么這里還有一個問題,lock 前綴是怎樣保證核心獨占某片內(nèi)存區(qū)域的呢?答案如下:

在 Intel 處理器中,有兩種方式保證處理器的某個核心獨占某片內(nèi)存區(qū)域。第一種方式是通過鎖定總線,讓某個核心獨占使用總線,但這樣代價太大。總線被鎖定后,其他核心就不能訪問內(nèi)存了,可能會導致其他核心短時內(nèi)停止工作。第二種方式是鎖定緩存,若某處內(nèi)存數(shù)據(jù)被緩存在處理器緩存中。處理器發(fā)出的 LOCK# 信號不會鎖定總線,而是鎖定緩存行對應的內(nèi)存區(qū)域。其他處理器在這片內(nèi)存區(qū)域鎖定期間,無法對這片內(nèi)存區(qū)域進行相關(guān)操作。相對于鎖定總線,鎖定緩存的代價明顯比較小。關(guān)于總線鎖和緩存鎖,更詳細的描述請參考 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 方法進行分析,相關(guān)分析如下:

public class AtomicInteger extends Number implements java.io.Serializable {

    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            // 計算變量 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 實際上只是一個殼子,主要的邏輯封裝在 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);
    // ......
}
// unsafe.cpp
/*
 * 這個看起來好像不像一個函數(shù),不過不用擔心,不是重點。UNSAFE_ENTRY 和 UNSAFE_END 都是宏,
 * 在預編譯期間會被替換成真正的代碼。下面的 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ù)偏移量,計算 value 的地址。這里的 offset 就是 AtomaicInteger 中的 valueOffset
  jint* 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ù),這個在預編譯期間編譯器會決定調(diào)用哪個平臺下的重載
   * 函數(shù)。相關(guān)的預編譯邏輯如下:
   * 
   * 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ù)實現(xiàn)
   */
  return (unsigned int)Atomic::cmpxchg((jint)exchange_value, (volatile jint*)dest,
                                       (jint)compare_value);
}

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

// 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 InterlockedCompareExchange
  int mp = os::is_MP();
  __asm {
    mov edx, dest
    mov ecx, exchange_value
    mov eax, compare_value
    LOCK_IF_MP(mp)
    cmpxchg dword ptr [edx], ecx
  }
}

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

inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
  // 判斷是否是多核 CPU
  int mp = os::is_MP();
  __asm {
    // 將參數(shù)值放入寄存器中
    mov edx, dest    // 注意: dest 是指針類型,這里是把內(nèi)存地址存入 edx 寄存器中
    mov ecx, exchange_value
    mov eax, compare_value
    
    // LOCK_IF_MP
    cmp mp, 0
    /*
     * 如果 mp = 0,表明是線程運行在單核 CPU 環(huán)境下。此時 je 會跳轉(zhuǎn)到 L0 標記處,
     * 也就是越過 _emit 0xF0 指令,直接執(zhí)行 cmpxchg 指令。也就是不在下面的 cmpxchg 指令
     * 前加 lock 前綴。
     */
    je L0
    /*
     * 0xF0 是 lock 前綴的機器碼,這里沒有使用 lock,而是直接使用了機器碼的形式。至于這樣做的
     * 原因可以參考知乎的一個回答:
     *     https://www.zhihu.com/question/50878124/answer/123099923
     */ 
    _emit 0xF0
L0:
    /*
     * 比較并交換。簡單解釋一下下面這條指令,熟悉匯編的朋友可以略過下面的解釋:
     *   cmpxchg: 即“比較并交換”指令
     *   dword: 全稱是 double word,在 x86/x64 體系中,一個 
     *          word = 2 byte,dword = 4 byte = 32 bit
     *   ptr: 全稱是 pointer,與前面的 dword 連起來使用,表明訪問的內(nèi)存單元是一個雙字單元
     *   [edx]: [...] 表示一個內(nèi)存單元,edx 是寄存器,dest 指針值存放在 edx 中。
     *          那么 [edx] 表示內(nèi)存地址為 dest 的內(nèi)存單元
     *          
     * 這一條指令的意思就是,將 eax 寄存器中的值(compare_value)與 [edx] 雙字內(nèi)存單元中的值
     * 進行對比,如果相同,則將 ecx 寄存器中的值(exchange_value)存入 [edx] 內(nèi)存單元中。
     */
    cmpxchg dword ptr [edx], ecx
  }
}

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

4.ABA 問題

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

時刻1:線程1執(zhí)行讀取操作,獲取原值 A,然后線程被切換走

時刻2:線程2執(zhí)行完成 CAS 操作將原值由 A 修改為 B

時刻3:線程2再次執(zhí)行 CAS 操作,并將原值由 B 修改為 A

時刻4:線程1恢復運行,將比較值(compareValue)與原值(oldValue)進行比較,發(fā)現(xiàn)兩個值相等。然后用新值(newValue)寫入內(nèi)存中,完成 CAS 操作

如上流程,線程1并不知道原值已經(jīng)被修改過了,在它看來并沒什么變化,所以它會繼續(xù)往下執(zhí)行流程。對于 ABA 問題,通常的處理措施是對每一次 CAS 操作設(shè)置版本號。java.util.concurrent.atomic 包下提供了一個可處理 ABA 問題的原子類 AtomicStampedReference,具體的實現(xiàn)這里就不分析了,有興趣的朋友可以自己去看看。

5.總結(jié)

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

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

參考

Compare-and-swap - wikipedia

多核環(huán)境下的內(nèi)存屏障指令 - 云風

Intel? 64 and IA-32 Architectures Software Developer’s Manual

一條C語言語句不一定是原子操作,但是一個匯編指令是原子操作嗎?- 知乎

下面這個宏中的emit指令是干什么的?- 知乎

消失的北橋 - txwm8905

附錄

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

文件名 路徑
Unsafe.java openjdk/jdk/src/share/classes/sun/misc/Unsafe.java
unsafe.cpp openjdk/hotspot/src/share/vm/prims/unsafe.cpp
atomic.cpp openjdk/hotspot/src/share/vm/runtime/atomic.cpp
atomic_windows_x86.inline.hpp openjdk/hotspot/src/os_cpu/windows_x86/vm/atomic_windows_x86.inline.hpp
本文在知識共享許可協(xié)議 4.0 下發(fā)布,轉(zhuǎn)載需在明顯位置處注明出處
作者:coolblog
本文同步發(fā)布在我的個人博客:http://www.coolblog.xyz


本作品采用知識共享署名-非商業(yè)性使用-禁止演繹 4.0 國際許可協(xié)議進行許可。

文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/69389.html

相關(guān)文章

  • 原理剖析(第 004 篇)CAS工作原理分析

    摘要:原理剖析第篇工作原理分析一大致介紹關(guān)于多線程競爭鎖方面,大家都知道有個和,也正是這兩個東西才引申出了大量的線程安全類,鎖類等功能而隨著現(xiàn)在的硬件廠商越來越高級,在硬件層面提供大量并發(fā)原語給我們層面的開發(fā)帶來了莫大的利好本章節(jié)就和大家分享分 原理剖析(第 004 篇)CAS工作原理分析 - 一、大致介紹 1、關(guān)于多線程競爭鎖方面,大家都知道有個CAS和AQS,也正是這兩個東西才引申出了大...

    leanote 評論0 收藏0
  • Treiber Stack簡單分析

    摘要:在添加新項目時使用堆棧,將堆棧的頂部放在新項目之后。當從堆棧中彈出一個項目時,在返回項目之前,您必須檢查另一個線程自操作開始以來沒有添加其他項目。比較和交換將堆棧的頭部設(shè)置為堆棧中舊的第二個元素,混合完整的數(shù)據(jù)結(jié)構(gòu)。 Abstract Treiber Stack Algorithm是一個可擴展的無鎖棧,利用細粒度的并發(fā)原語CAS來實現(xiàn)的,Treiber Stack在 R. Kent T...

    junfeng777 評論0 收藏0
  • i++ 是線程安全的嗎?

    摘要:例子先來看下面的示例來驗證下到底是不是線程安全的。上面的例子我們期望的結(jié)果應該是,但運行遍,你會發(fā)現(xiàn)總是不為,至少你現(xiàn)在知道了操作它不是線程安全的了。它的性能比較好也是因為避免了使線程進入內(nèi)核態(tài)的阻塞狀態(tài)。 例子 先來看下面的示例來驗證下 i++ 到底是不是線程安全的。 1000個線程,每個線程對共享變量 count 進行 1000 次 ++ 操作。 showImg(https://s...

    RyanQ 評論0 收藏0
  • 深入分析AQS實現(xiàn)原理

    摘要:更新成功返回,否則返回這個操作是原子的,不會出現(xiàn)線程安全問題,這里面涉及到這個類的操作,一級涉及到這個屬性的意義。 簡單解釋一下J.U.C,是JDK中提供的并發(fā)工具包,java.util.concurrent。里面提供了很多并發(fā)編程中很常用的實用工具類,比如atomic原子操作、比如lock同步鎖、fork/join等。 從Lock作為切入點 我想以lock作為切入點來講解AQS,畢竟...

    sewerganger 評論0 收藏0
  • (五)Synchronized原理分析

    摘要:而導致這個問題的原因是線程并行執(zhí)行操作并不是原子的,存在線程安全問題。表示自旋鎖,由于線程的阻塞和喚醒需要從用戶態(tài)轉(zhuǎn)為核心態(tài),頻繁的阻塞和喚醒對來說性能開銷很大。 文章簡介 synchronized想必大家都不陌生,用來解決線程安全問題的利器。同時也是Java高級程序員面試比較常見的面試題。這篇文正會帶大家徹底了解synchronized的實現(xiàn)。 內(nèi)容導航 什么時候需要用Synchr...

    greatwhole 評論0 收藏0

發(fā)表評論

0條評論

最新活動
閱讀需要支付1元查看
<