atomic 包底層實現原理

一、概念介紹

(一)volatile關鍵字

Java 因爲指令重排序,優化我們的代碼,讓程序運行更快,也隨之帶來了多線程下,指令執行順序的不可控。

1.volatile關鍵字的作用:

  • 內存可見性,修飾的變量發生改變之後對所有線程立即可見
  • 禁止指令重排序

volatile的底層是通過內存屏障實現的,第一個作用是禁止指令重排。內存屏障另一個作用是強制更新一次不同 CPU 的緩存

synchronized 看作重量級的鎖,而 volatile 看作輕量級的鎖 。synchronized使用的鎖的層面是在JVM層面,虛擬機處理字節碼文件實現相關指令。volatile 底層使用多核處理器實現的 lock 指令,更底層,消耗代價更小


(二)CAS

CAS 的全稱是 Compare-And-Swap , 它是一條 CPU 併發原語

CAS 並不是一種實際的鎖,它僅僅是實現樂觀鎖的一種思想,java 中的樂觀鎖(如自旋鎖)基本都是通過 CAS 操作實現的,CAS 是一種更新的原子操作,比較當前值跟傳入值是否一樣,一樣則更新,否則失敗。

樂觀鎖一般會使用版本號機制或 CAS 算法實現

1.版本號機制

一般是在數據表中加上一個數據版本號 version 字段,表示數據被修改的次數,當數據被修改時,version 值會加一。當線程 A 要更新數據值時,在讀取數據的同時也會讀取 version 值,在提交更新時,若剛纔讀取到的 version 值爲當前數據庫中的 version 值相等時才更新,否則重試更新操作,直到更新成功。

2.CAS 算法

compare and swap(比較與交換),是一種有名的無鎖算法。無鎖編程,即不使用鎖的情況下實現多線程之間的變量同步,也就是在沒有線程被阻塞的情況下實現變量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。 CAS 算法涉及到三個操作數

  • 需要讀寫的內存值 V
  • 進行比較的值 A
  • 擬寫入的新值 B

當且僅當 V 的值等於 A 時,CAS 通過原子方式用新值 B 來更新 V 的值,否則不會執行任何操作(比較和替換是一個原子操作)。一般情況下是一個自旋操作,即不斷的重試。

3.synchronized與CAS的比較

synchronized涉及線程之間的切換,存在用戶狀態和內核狀態的切換,耗費巨大。CAS只是CPU的一條原語,是一個原子操作,消耗較少。


二、atomic 的實現原理

Atomic 包中的類基本的特性就是在多線程環境下,當有多個線程同時對單個(包括基本類型及引用類型)變量進行操作時,具有排他性,即當多個線程同時對該變量的值進行更新時,僅有一個線程能成功,而未成功的線程可以向自旋鎖一樣,繼續嘗試,一直等到執行成功。

Atomic 系列的類中的核心方法都會調用 unsafe 類中的幾個本地方法。這個類包含了大量的對 C 代碼的操作,包括很多直接內存分配以及原子操作的調用,而它之所以標記爲非安全的,是告訴你這個裏面大量的方法調用都會存在安全隱患。unsafe 是 java 提供的獲得對對象內存地址訪問的類,它的作用就是在更新操作時提供 “比較並替換” 的作用。

CAS 併發原語現在 Java 語言中就是 sun.misc.Unsafe 類的各個方法,調用 Unsafe 類中的 CAS 方法,JVM 會幫我們實現 CAS 彙編指令,這是一種完全依賴硬件的功能,通過它實現了原子操作,由於 CAS 是一種系統原語,原語屬於操作系統用語範疇,是由於諾幹條指令組成的,用於完成某個功能的一個過程,並且原語的執行必須是連續的,在執行過程中不允許被中斷,也就是說 CAS 是一條 CPU 原子指令,不會造成所謂的數據不一致問題。

可以看出atomic證原子性就是通過:自旋 + CAS(樂觀鎖)

仔細分析 concurrent 包的源代碼實現,會發現一個通用化的實現模式:

  • 首先,聲明共享變量爲 volatile

  • 然後,使用 CAS 的原子條件更新來實現線程之間的同步;

  • 同時,配合以 volatile 的讀 / 寫和 CAS 所具有的 volatile 讀和寫的內存語義來實現線程之間的通信。

優缺點:

CAS 相對於其他鎖,不會進行內核態操作,有着一些性能的提升。但同時引入自旋,當鎖競爭較大的時候,自旋次數會增多。cpu 資源會消耗很高。CAS + 自旋適合使用在低併發有同步數據的應用場景


三、atomic 的 ABA問題

多個線程即可以入列也可以出列,也就是數據的操作方向不一致,那麼可能出現 ABA 的情況。
在這裏插入圖片描述

T1 線程準備出棧,對於出棧操作我們只需要將棧頂位置由 sp 通過 CAS 操作更新爲 newSP 即可,如圖 1 所示。但是在 T1 線程執行 tail.compareAndSet (sp,newSP) 之前系統進行了線程調度,T2 線程開始執行。T2 執行了三個操作,A 出棧,B 出棧,然後又將 A 入棧。此時系統又開始調度,T1 線程繼續執行出棧操作,但是在 T1 線程看來,棧頂元素仍然爲 A,(即 T1 仍然認爲 B 還是棧頂 A 的下一個元素),而實際上的情況如圖 2 所示。T1 會認爲棧沒有發生變化,所以 tail.compareAndSet (sp,newSP) 執行成功,棧頂指針被指向了 B 節點。而實際上 B 已經不存在於堆棧中,T1 將 A 出棧後的結果如圖 3 所示,這顯然不是正確的結果。

解決方法:

除了要比較當對象的前值和預期值以外,還要比較當前(操作的)戳值和預期(操作的)戳值,當全部相同時,compareAndSet 方法才能成功。每次更新成功,戳值都會發生變化,戳值的設置是由編程人員自己控制的。


【Java 面試那點事】

這裏致力於分享 Java 面試路上的各種知識,無論是技術還是經驗,你需要的這裏都有!

這裏可以讓你【快速瞭解 Java 相關知識】,並且【短時間在面試方面有跨越式提升

面試路上,你不孤單!
在這裏插入圖片描述

發佈了165 篇原創文章 · 獲贊 852 · 訪問量 29萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章