java基礎(chǔ)教程欄目介紹分析Java CAS
推薦(免費):java基礎(chǔ)教程
1、簡介
CAS 全稱是 compare and swap,是一種用于在多線程環(huán)境下實現(xiàn)同步功能的機制。CAS 操作包含三個操作數(shù) -- 內(nèi)存位置、預(yù)期數(shù)值和新值。CAS 的實現(xiàn)邏輯是將內(nèi)存位置處的數(shù)值與預(yù)期數(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ù)?。在多核心時代下,多個核心通過同一條總線和內(nèi)存以及其他硬件進行通信。如下圖:
圖片出處:《深入理解計算機系統(tǒng)》
上圖是一個較為簡單的計算機結(jié)構(gòu)圖,雖然簡單,但足以說明問題。在上圖中,CPU 通過兩個藍色箭頭標(biāo)注的總線與內(nèi)存進行通信。大家考慮一個問題,CPU 的多個核心同時對同一片內(nèi)存進行操作,若不加以控制,會導(dǎo)致什么樣的錯誤?這里簡單說明一下,假設(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ù)必定是混亂的。
不過對于這個問題,實際上不用擔(dān)心。通過 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)用黑體標(biāo)出了,在多處理器環(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)存了,可能會導(dǎo)致其他核心短時內(nèi)停止工作。第二種方式是鎖定緩存,若某處內(nèi)存數(shù)據(jù)被緩存在處理器緩存中。處理器發(fā)出的 LOCK# 信號不會鎖定總線,而是鎖定緩存行對應(yīng)的內(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ù),不過不用擔(dā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ù)偏移量,計算?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ù),這個在預(yù)編譯期間編譯器會決定調(diào)用哪個平臺下的重載 ???*?函數(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ù)實現(xiàn) ???*/ ??return?(unsigned?int)Atomic::cmpxchg((jint)exchange_value,?(volatile?jint*)dest, ???????????????????????????????????????(jint)compare_value); }
上面的分析看起來比較多,不過主流程并不復(fù)雜。如果不糾結(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 預(yù)編譯標(biāo)識符和 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?標(biāo)記處, ?????*?也就是越過?_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恢復(fù)運行,將比較值(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)都不是很難,但是寫起來真的不太好寫。這里面涉及到了一些底層的知識,雖然能看懂,但想說明白,還是有點難度的。由于我底層的知識比較欠缺,上面的一些分析難免會出錯。所以如有錯誤,請輕噴,當(dāng)然最好能說明怎么錯的,感謝。
好了,本篇文章就到這里。感謝閱讀,再見。
附錄
在前面源碼分析一節(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 |
Atas ialah kandungan terperinci 介紹Java CAS 原理分析. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

Alat AI Hot

Undress AI Tool
Gambar buka pakaian secara percuma

Undresser.AI Undress
Apl berkuasa AI untuk mencipta foto bogel yang realistik

AI Clothes Remover
Alat AI dalam talian untuk mengeluarkan pakaian daripada foto.

Clothoff.io
Penyingkiran pakaian AI

Video Face Swap
Tukar muka dalam mana-mana video dengan mudah menggunakan alat tukar muka AI percuma kami!

Artikel Panas

Alat panas

Notepad++7.3.1
Editor kod yang mudah digunakan dan percuma

SublimeText3 versi Cina
Versi Cina, sangat mudah digunakan

Hantar Studio 13.0.1
Persekitaran pembangunan bersepadu PHP yang berkuasa

Dreamweaver CS6
Alat pembangunan web visual

SublimeText3 versi Mac
Perisian penyuntingan kod peringkat Tuhan (SublimeText3)

Untuk mengendalikan transaksi JDBC dengan betul, anda mesti terlebih dahulu mematikan mod komit automatik, kemudian melakukan pelbagai operasi, dan akhirnya melakukan atau mengembalikan semula hasilnya; 1. Panggil Conn.SetAutOcommit (palsu) untuk memulakan transaksi; 2. Melaksanakan pelbagai operasi SQL, seperti memasukkan dan mengemaskini; 3. Panggil Conn.Commit () jika semua operasi berjaya, dan hubungi conn.rollback () jika pengecualian berlaku untuk memastikan konsistensi data; Pada masa yang sama, cuba-dengan-sumber harus digunakan untuk menguruskan sumber, mengendalikan pengecualian dengan betul dan menutup sambungan untuk mengelakkan kebocoran sambungan; Di samping itu, adalah disyorkan untuk menggunakan kolam sambungan dan menetapkan mata simpan untuk mencapai rollback separa, dan menyimpan urus niaga sesingkat mungkin untuk meningkatkan prestasi.

Gunakan kelas dalam pakej Java.Time untuk menggantikan kelas lama dan kelas kalendar; 2. Dapatkan tarikh dan masa semasa melalui LocalDate, LocalDateTime dan Tempatan Tempatan; 3. Buat tarikh dan masa tertentu menggunakan kaedah (); 4. Gunakan kaedah tambah/tolak untuk meningkatkan dan mengurangkan masa; 5. Gunakan zoneddatetime dan zonid untuk memproses zon waktu; 6. Format dan parse date string melalui DateTimeFormatter; 7. Gunakan segera untuk bersesuaian dengan jenis tarikh lama apabila perlu; pemprosesan tarikh di java moden harus memberi keutamaan untuk menggunakan java.timeapi, yang memberikan jelas, tidak berubah dan linear

Pra-formancetartuptimemoryusage, quarkusandmicronautleadduetocompile-timeprocessingandgraalvsupport, withquarkusoftenperforminglightbetterine serverless scenarios.tyvelopecosyste,

NetworkPortsandFireWallSworkTogethertoenableCommunicationWileensuringsecurity.1.networkportsarevirtualendpointsNumbered0-655 35, Withwell-KnownportsLike80 (http), 443 (https), 22 (ssh), dan25 (smtp) identitispecificservices.2.portsoperateovertcp (boleh dipercayai, c

Koleksi Sampah Java (GC) adalah mekanisme yang secara automatik menguruskan ingatan, yang mengurangkan risiko kebocoran ingatan dengan menuntut semula objek yang tidak dapat dicapai. 1.GC menghakimi kebolehcapaian objek dari objek akar (seperti pembolehubah stack, benang aktif, medan statik, dan lain -lain), dan objek yang tidak dapat dicapai ditandakan sebagai sampah. 2. Berdasarkan algoritma penandaan tanda, tandakan semua objek yang dapat dicapai dan objek yang tidak ditandai. 3. Mengamalkan strategi pengumpulan generasi: Generasi Baru (Eden, S0, S1) sering melaksanakan MinorGC; Orang tua melakukan kurang tetapi mengambil masa lebih lama untuk melakukan MajorGC; Metaspace Stores Metadata kelas. 4. JVM menyediakan pelbagai peranti GC: SerialGC sesuai untuk aplikasi kecil; ParallelGC meningkatkan throughput; CMS mengurangkan

GradleisthebetterChoiceFormostNewProjectSduetoitSsuperiorflexibility, Prestasi, danModernToolingSupport.1.Gradle'sGroovy/KOT lindslismoreconciseandexpressivethanmaven'sverbosexml.2.GradleOutPerformsMaveninBuildSpeedWithIncrementalcompilation, BuildCac

Defer digunakan untuk melaksanakan operasi tertentu sebelum fungsi pulangan, seperti sumber pembersihan; Parameter dinilai dengan serta-merta apabila menangguhkan, dan fungsi-fungsi dilaksanakan mengikut urutan terakhir (LIFO); 1. Pelbagai penahanan dilaksanakan dalam urutan terbalik pengisytiharan; 2. Biasanya digunakan untuk pembersihan yang selamat seperti penutupan fail; 3. Nilai pulangan yang dinamakan boleh diubah suai; 4. Ia akan dilaksanakan walaupun panik berlaku, sesuai untuk pemulihan; 5. Elakkan penyalahgunaan menangguhkan gelung untuk mengelakkan kebocoran sumber; Penggunaan yang betul boleh meningkatkan keselamatan kod dan kebolehbacaan.

Memilih jenis htmlinput yang betul dapat meningkatkan ketepatan data, meningkatkan pengalaman pengguna, dan meningkatkan kebolehgunaan. 1. Pilih jenis input yang sepadan mengikut jenis data, seperti teks, e -mel, tel, nombor dan tarikh, yang secara automatik boleh menyemak dan menyesuaikan diri dengan papan kekunci; 2. Gunakan HTML5 untuk menambah jenis baru seperti URL, Warna, Julat dan Carian, yang dapat memberikan kaedah interaksi yang lebih intuitif; 3. Gunakan pemegang tempat dan sifat -sifat yang diperlukan untuk meningkatkan kecekapan dan ketepatan pengisian bentuk, tetapi harus diperhatikan bahawa pemegang tempat tidak dapat menggantikan label.
