Java併發系列之Java併發機制的底層實現原理

前言

文本已收錄至我的GitHub倉庫,歡迎Star:https://github.com/bin392328206/six-finger
種一棵樹最好的時間是十年前,其次是現在
我知道很多人不玩qq了,但是懷舊一下,歡迎加入六脈神劍Java菜鳥學習羣,羣聊號碼:549684836 鼓勵大家在技術的路上寫博客

絮叨

昨天從大的方向上介紹了Java併發的一個全局觀,瞭解了JDK的JUC,那麼今天我們從最底層的原理來探索這些併發,這也是面試問的最多的地方之一,問底層,如果能理解當然是好的啦,前面的內容在下面的鏈接:

Java代碼 編譯之後 得到 Java字節碼,被 類加載器加載到JVM中,最終 轉化爲彙編指令。Java中的併發機制依賴於JVM的實現和CPU的指令

併發編程的3個基本概念

原子性

定義:即一個操作或者多個操作 要麼全部執行並且執行的過程不會被任何因素打斷,要麼就都不執行。

原子性是拒絕多線程操作的,不論是多核還是單核,具有原子性的量,同一時刻只能有一個線程來對它進行操作。簡而言之,在整個操作過程中不會被線程調度器中斷的操作,都可認爲是原子性。例如 a=1是原子性操作,但是a++和a +=1就不是原子性操作。Java中的原子性操作包括:

a. 基本類型的讀取和賦值操作,且賦值必須是數字賦值給變量,變量之間的相互賦值不是原子性操作。

b.所有引用reference的賦值操作

c.java.concurrent.Atomic.* 包中所有類的一切操作

可見性

定義:指當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。

在多線程環境下,一個線程對共享變量的操作對其他線程是不可見的。Java提供了volatile來保證可見性,當一個變量被volatile修飾後,表示着線程本地內存無效,當一個線程修改共享變量後他會立即被更新到主內存中,其他線程讀取共享變量時,會直接從主內存中讀取。當然,synchronize和Lock都可以保證可見性。synchronized和Lock能保證同一時刻只有一個線程獲取鎖然後執行同步代碼,並且在釋放鎖之前會將對變量的修改刷新到主存當中。因此可以保證可見性。

有序性

定義:即程序執行的順序按照代碼的先後順序執行。

Java內存模型中的有序性可以總結爲:如果在本線程內觀察,所有操作都是有序的;如果在一個線程中觀察另一個線程,所有操作都是無序的。前半句是指“線程內表現爲串行語義”,後半句是指“指令重排序”現象和“工作內存主主內存同步延遲”現象。

在Java內存模型中,爲了效率是允許編譯器和處理器對指令進行重排序,當然重排序不會影響單線程的運行結果,但是對多線程會有影響。Java提供volatile來保證一定的有序性。最著名的例子就是單例模式裏面的DCL(雙重檢查鎖)。另外,可以通過synchronized和Lock來保證有序性,synchronized和Lock保證每個時刻是有一個線程執行同步代碼,相當於是讓線程順序執行同步代碼,自然就保證了有序性。

鎖的互斥和可見性

鎖提供了兩種主要特性:互斥(mutual exclusion) 和可見性(visibility)。

  • 互斥即一次只允許一個線程持有某個特定的鎖,一次就只有一個線程能夠使用該共享數據。

  • 可見性要更加複雜一些,它必須確保釋放鎖之前對共享數據做出的更改對於隨後獲得該鎖的另一個線程是可見的。也即當一條線程修改了共享變量的值,新值對於其他線程來說是可以立即得知的。如果沒有同步機制提供的這種可見性保證,線程看到的共享變量可能是修改前的值或不一致的值,這將引發許多嚴重問題。要使 volatile 變量提供理想的線程安全,必須同時滿足下面兩個條件:

    • 對變量的寫操作不依賴於當前值。

    • 該變量沒有包含在具有其他變量的不變式中。

實際上,這些條件表明,可以被寫入 volatile 變量的這些有效值獨立於任何程序的狀態,包括變量的當前狀態。事實上就是保證操作是原子性操作,才能保證使用volatile關鍵字的程序在併發時能夠正確執行。

Java的內存模型JMM以及共享變量的可見性

JMM決定一個線程對共享變量的寫入何時對另一個線程可見,JMM定義了線程和主內存之間的抽象關係:共享變量存儲在主內存(Main Memory)中,每個線程都有一個私有的本地內存(Local Memory),本地內存保存了被該線程使用到的主內存的副本拷貝,線程對變量的所有操作都必須在工作內存中進行,而不能直接讀寫主內存中的變量。

對於普通的共享變量來講,線程A將其修改爲某個值發生在線程A的本地內存中,此時還未同步到主內存中去;而線程B已經緩存了該變量的舊值,所以就導致了共享變量值的不一致。解決這種共享變量在多線程模型中的不可見性問題,較粗暴的方式自然就是加鎖,但是此處使用synchronized或者Lock這些方式太重量級了,比較合理的方式其實就是volatile。

需要注意的是,JMM是個抽象的內存模型,所以所謂的本地內存,主內存都是抽象概念,並不一定就真實的對應cpu緩存和物理內存

存儲器的內存結構

每次讀一個數據都是先從上一直找到下 如果cpu高速緩存 1 2有的話,那麼速度會快很多,越靠近CPU的地方 速度越快 成本越高
如果是寫入一個數據,它也會一層一層的寫到各個緩存中去 (這裏還涉及到一個高級概念 緩存行 就是CPU做高速緩存的時候 他並不是說一個字節去緩存的,而是一行去緩存,並且這個大小是64個字節 所以JDK源碼 中 爲了避免 緩存行裏面其他數據的更新,你可以定義64個字節的數據,去做緩存,可能比你做一個8個字節的要快)

Volatile原理

Java語言提供了一種稍弱的同步機制,即volatile變量,用來確保將變量的更新操作通知到其他線程。當把變量聲明爲volatile類型後,編譯器與運行時都會注意到這個變量是共享的,因此不會將該變量上的操作與其他內存操作一起重排序。volatile變量不會被緩存在寄存器或者對其他處理器不可見的地方,因此在讀取volatile類型的變量時總會返回最新寫入的值。

在訪問volatile變量時不會執行加鎖操作,因此也就不會使執行線程阻塞,因此volatile變量是一種比sychronized關鍵字更輕量級的同步機制。因爲它不會引起線程上下文的切換和調度

當對非 volatile 變量進行讀寫的時候,每個線程先從內存拷貝變量到CPU緩存中。如果計算機有多個CPU,每個線程可能在不同的CPU上被處理,這意味着每個線程可以拷貝到不同的 CPU cache 中。

而聲明變量是 volatile 的,JVM 保證了每次讀變量都從JMM中讀,跳過 CPU cache(線程的本地緩存) 這一步。

volatile是如何保證可見性的呢

把有volatile關鍵字修改的代碼 變成彙編代碼的時候發現,它的代碼前面多了一個lock 關鍵字,這個前綴的指令在多核處理器下會引發2件事情

  • 將當前處理器緩存行的數據回寫到系統內存中

  • 這個回寫操作,會導致其他線程的本地緩存無效(內部是通過緩存一致性協議,通過在總線上的傳播數據來檢查自己的緩存是否有效)

  • MESI Cache 緩存一致性協議

    • Modified 修改的

    • Exclusive 獨佔的

    • Shared 共享的

    • Invalid 無效的

synchronized

在多線程中,synchronized 一直是一個元老級別的角色,很多人會稱呼他爲重量級的鎖,但是1.6對它的優化之後,並不那麼重量了。

synchronized實現同步

Java中的每個對象都可以作爲鎖,它有以下三種表現形式

  • 對於 普通同步方法,鎖是 當前實例對象。

  • 對於 靜態同步方法,鎖是 當前類的Class對象。

  • 對於 同步方法塊,鎖是 Synchonized括號裏配置的對象(可能是實例對象。也可能是Class對象)。

Java對象頭

synchronized用的鎖是存在Java對象頭裏的。在32位 虛擬機中,1字寬 等於4字節,即32bit。

  • 數組類型,虛擬機用3個字寬存儲對象頭。

  • 非數組類型,虛擬機用2字寬存儲對象頭。

我們知道的Java的鎖是存在每個對象裏面,那具體是存在哪裏呢?
在Java對象頭裏面有一個叫 Mark Word的區域,裏面存着HashCode 分代年齡,鎖標記位。

鎖的4種狀態

級別從低到高依次是:

  • 無鎖狀態

  • 偏向鎖狀態

  • 輕量級鎖狀態

  • 重量級鎖狀態

這邊說一下偏向鎖的原理吧?自己總結的也不一定對,就是說當一個線程去獲得一個偏向鎖要走的幾步

  • 第一步,先判斷再對象頭裏面是否存儲了當前線程的id和判斷一下鎖標誌位的狀態,

  • 第二步,如果是有當前線程id,就直接省去了CAS操作來加鎖,解鎖,如果沒有則就行下一步

  • 第三步,通過CAS 獲得鎖,然後把當前線程id存到對象頭裏面,然後把同步代碼塊執行完成,第四步就是接下來,要釋放偏下鎖

  • 第四步,偏向鎖的釋放機制是當有下一個線程來競爭鎖的時候,發現CAS不成功,那麼就釋放鎖,然後再去競爭鎖

至於 輕量級鎖 我認爲就是還是處於自旋 線程還沒掛起的狀態

原子操作的實現原理

原子(atomic)本意是“不能被進一步分割的最小粒子”,而原子操作(atomic operation)意爲“不可被中斷的一個或一系列操作”。

處理器如何實現原子操作

  • 使用總線鎖保證原子性:所謂總線鎖就是使用處理器提供的一個LOCK#信號,當一個處理器在總線上輸出此信號時,其他處理器的請求將被阻塞住,那麼該處理器可以獨佔共享內存。

  • 使用緩存鎖保證原子性:這個的意思是在每個線程的本地緩存中,我不會去管你,但是最後會寫到內存中的時候,我會用緩存一致性原理讓你只能有一個線程能回寫內存成功,然後告知其他線程的本地緩存失效,讓他們重新去更新本地緩存,再去操作,

以下兩種情況不會使用緩存鎖:

  • 當處理器不支持緩存鎖定。

  • 當操作的數據不能被緩存在處理器內部,或操作的數據跨多個緩存行時,則處理器會調用總線鎖定。

Java如何實現原子操作

Java 提供了2種原子性的方法

  • Java使用鎖來保證原子性操作,鎖機制保證了只有獲得鎖的線程才能夠操作鎖定的內存區域。

  • 使用循環CAS實現原子性操作

什麼是CAS?

在計算機科學中,比較和交換(Conmpare And Swap)是用於實現多線程同步的原子指令。它將內存位置的內容與給定值進行比較,只有在相同的情況下,將該內存位置的內容修改爲新的給定值。這是作爲單個原子操作完成的。原子性保證新值基於最新信息計算; 如果該值在同一時間被另一個線程更新,則寫入將失敗。操作結果必須說明是否進行替換;這可以通過一個簡單的布爾響應(這個變體通常稱爲比較和設置),或通過返回從內存位置讀取的值來完成(摘自維基本科)

CAS有3個操作數,內存值V,舊的預期值A,要修改的新值B。當且僅當預期值A和內存值V相同時,將內存值V修改爲B,否則什麼都不做。至於底層是unsafe包實現的 裏面是調用的native方法(底層c++實現),操作cpu的,這邊就不往下深入了,不是不想,是博主太菜了

CAS操作的三大問題

  • ABA問題,這個是最常見的問題之一了,這個也簡單就是A變成了B 最後變成了A,那麼內存值 和預期值是相當的,所以他認爲這個操作是原子的,其實不是,

  • CAS是循環的去操作,如果長時間不成功,對於cou的消耗比較大

  • 只能保證對於一個共享變量的原子性操作,如果是多個建議用鎖

結尾

第二章,介紹了併發機制的底層實現原理,valatile synchronized的實現原理,CAS 的優缺點,原子性問題等,後面的很多東西,但是要基於這個來實現的,今天就到這了吧

因爲博主也是一個開發萌新 我也是一邊學一邊寫 我有個目標就是一週 二到三篇 希望能堅持個一年吧 希望各位大佬多提意見,讓我多學習,一起進步。

日常求贊

好了各位,以上就是這篇文章的全部內容了,能看到這裏的人呀,都是神人

創作不易,各位的支持和認可,就是我創作的最大動力,我們下篇文章見

六脈神劍 | 文 【原創】如果本篇博客有任何錯誤,請批評指教,不勝感激 !

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章