Yige

Yige

Build

Java並發的基本概念

Java 並發的基本概念#

硬體層次#

由於 CPU 執行指令的速度是很快的,但是內存訪問的速度就慢了很多,相差的不是一個數量級,於是為了不讓內存成為計算機程序處理的瓶頸,透過在 CPU 和內存之間增加高速緩存的方式來解決

緩存一致性#

在 CPU 和主存之間增加緩存,在多線程場景下就可能存在緩存一致性問題,也就是說,在多核 CPU 中,每個核的自己的緩存中,關於同一個數據的緩存內容可能不一致

解決緩存一致性的兩種方案

  • 透過在總線加 LOCK# 鎖的方式 (現代計算機都是多核 CPU,總線加鎖會導致其他 CPU 也無法訪問內存,效率低下)
  • 透過緩存一致性協議(Cache Coherence Protocol)

MESI 緩存一致性協議#

緩存一致性協議(Cache Coherence Protocol),最出名的就是 Intel 的 MESI 協議,MESI 協議保證了每個緩存中使用的共享變量的副本是一致的

MESI 的核心的思想是:當 CPU 寫數據時,如果發現操作的變量是共享變量,即在其他 CPU 中也存在該變量的副本,會發出信號通知其他 CPU 將該變量的緩存行置為無效狀態,因此當其他 CPU 需要讀取這個變量時,發現自己緩存中緩存該變量的緩存行是無效的,那麼它就會從內存重新讀取

在 MESI 協議中,每個緩存可能有 4 個狀態,它們分別是:

  • M(Modified):這行數據有效,數據被修改了,和內存中的數據不一致,數據只存在於本 Cache 中。
  • E(Exclusive):這行數據有效,數據和內存中的數據一致,數據只存在於本 Cache 中。
  • S(Shared):這行數據有效,數據和內存中的數據一致,數據存在於很多 Cache 中。
  • I(Invalid):這行數據無效

MESI 協議,可以保證緩存的一致性,但是無法保證實時性

處理器優化和指令重排#

  • 處理器優化: 為了使處理器內部的運算單元能夠盡量的被充分利用,處理器可能會對輸入代碼進行亂序執行處理
  • 指令重排: 除了現在很多流行的處理器會對代碼進行優化亂序處理,很多編程語言的編譯器也會有類似的優化,比如 Java 虛擬機的即時編譯器(JIT)也會做指令重排
  • 聯想記憶到:
    1. Spark 中不存在依賴關係的 task 並發執行優化計算
    2. 不存在資源競爭的程序並發執行,比如某個程序搶佔 IO 資源,那麼可以先去執行其他不搶佔 IO 資源的程序,省卻等待時間

並發編程中的三個概念#

原子性#

原子性是指在一個操作中就是 cpu 不可以在中途暫停然後再調度,既不被中斷操作,要不執行完成,要不就不執行 (聯想記憶到數據庫事務處理的原子性)

可見性#

可見性是指當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值

有序性#

有序性即程序執行的順序按照代碼的先後順序執行

其實,原子性問題,可見性問題和有序性問題。是人們抽象定義出來的。而這個抽象的底層問題就是前面提到的緩存一致性問題、處理器優化問題和指令重排問題等。
緩存一致性問題其實就是可見性問題,而處理器優化可能會造成導致原子性問題的,指令重排即會導致有序性問題

Java 的內存模型#

基本概念#

  • Java 的並發採用 “共享內存” 模型,線程之間透過讀寫內存的公共狀態進行通訊。多個線程之間是不能透過直接傳遞數據交互的,它們之間交互只能透過共享變量實現。

  • JMM 本身是一種抽象的概念,並不真實存在,它描述的是一組規則或規範,規定所有變量都是存在主存中的,類似於普通內存,每個線程又包含自己的工作內存,類比高速緩存。所以線程的操作都是以工作內存為主,它們只能訪問自己的工作內存,且工作前後都要把值在同步回主內存

ps: 聯想到有點類似於緩存 + DB 的系統架構,所有變量都是存在主存中的,類似於DB, 每個線程又包含自己的工作內存,類比高速緩存

Java 內存模型的實現#

image.png

  • Java 內存模型 (JMM) 規定所有變量都存儲在主內存中,每個線程還有自己的工作內存:

    1. 線程的工作內存中保存了被該線程使用到的變量的拷貝(從主內存中拷貝過來),線程對變量的所有操作都必須在工作內存中執行,而不能直接訪問主內存中的變量。
    2. 不同線程之間無法直接訪問對方工作內存的變量,線程間變量值的傳遞都要透過主內存來完成。
  • Java 線程之間的通信由內存模型 JMM(Java Memory Model)控制:

    1. JMM 決定一個線程對變量的寫入何時對另一個線程可見。
    2. 線程之間共享變量存儲在主內存中
    3. 每個線程有一個私有的本地內存,裡面存儲了讀 / 寫共享變量的副本。
    4. JMM 透過控制每個線程的本地內存之間的交互,來為程序員提供內存可見性保證。
  • 內存間交互操作:

    1. lock(鎖定):作用於主內存的變量,把一個變量標識為一條線程獨占狀態。
    2. unlock(解鎖):作用於主內存的變量,把一個處於鎖定狀態的變量釋放出來,釋放後的變量才可以被其他線程鎖定。
    3. read(讀取):作用於主內存變量,把主內存的一個變量讀取到工作內存中。
    4. load(載入):作用於工作內存,把 read 操作讀取到工作內存的變量載入到工作內存的變量副本中
    5. use(使用):作用於工作內存的變量,把工作內存中的變量值傳遞給一個執行引擎。
    6. assign(賦值):作用於工作內存的變量。把執行引擎接收到的值賦值給工作內存的變量。
    7. store(存儲):把工作內存的變量的值傳遞給主內存
    8. write(寫入):把 store 操作的值入到主內存的變量中

注意:主內存和工作內存與 JVM 內存結構中的 Java 堆、棧、方法區等並不是同一個層次的內存劃分

Java 中並發的實現#

原子性的實現#

可見性的實現#

參考: 並發三特性 - 可見性定義、可見性問題與可見性保證技術

  • 透過 volatile 關鍵字標記內存屏障保證可見性。

  • 透過 synchronized 關鍵字定義同步代碼塊或者同步方法保障可見性。

  • 透過 Lock 接口保障可見性。

  • 透過 Atomic 類型保障可見性

  • 透過 final 關鍵字實現

    被 final 修飾的字段一旦初始化完成 (靜態變量或者在構造函數中初始化),並且構造器沒有把 “this” 的引用傳遞出去(this 引用逃逸是一件很危險的事情,其它線程有可能透過這個引用訪問到 “初始化了一半” 的對象),那在其他線程中就能看見 final 字段的值

有序性的實現#

在 Java 中,可以使用 synchronized 和 volatile 來保證多線程之間操作的有序性

  • volatile 關鍵字會禁止指令重排
  • synchronized 關鍵字保證同一時刻只允許一條線程操作

happens-before原則#

JMM 具備一些先天的有序性,即不需要透過任何手段就可以保證的有序性,通常稱為 happens-before 原則. 《JSR-133:Java Memory Model and Thread Specification》定義了如下 happens-before 規則:

  • 程序順序規則: 即在一個線程內必須保證語義串行性,也就是說按照代碼順序執行
  • 監視器鎖規則:對一個線程的解鎖,happens-before 於隨後對這個線程的加鎖
  • volatile變量規則: 對一個 volatile 域的寫,happens-before 於後續對這個 volatile 域的讀
  • 傳遞性:如果 A happens-before B , 且 B happens-before C, 那麼 A happens-before C
  • start()規則: 線程的 start () 方法先於它的每一個動作,即如果線程 A 在執行線程 B 的 start 方法之前修改了共享變量的值,那麼當線程 B 執行 start 方法時,線程 A 對共享變量的修改對線程 B 可見
  • join()線程終止原則: 線程的所有操作先於線程的終結,如果 A 執行 ThreadB.join () 並且成功返回,那麼線程 B 中的任意操作 happens-before 於線程 A 從 ThreadB.join () 操作成功返回。
  • interrupt()線程中斷原則: 對線程 interrupt () 方法的調用先行發生於被中斷線程代碼檢測到中斷事件的發生,可以透過 Thread.interrupted () 方法檢測是否有中斷發生
  • finalize()對象終結原則:一個對象的初始化完成先行發生於它的 finalize () 方法的開始

參考鏈接#

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。