Java併發編程(二)線程的安全性問題

線程的安全性問題

總所周知,多線程提高了系統的性能,但令人頭痛的是線程會存在的安全性問題.

爲什麼會存在安全性問題,並且我們應該怎麼去解決這類的問題。 其實線程安全問題可以總結爲: 可見性、原子性、有序性這幾個問題,我們搞 懂了這幾個問題並且知道怎麼解決,那麼多線程安全性問題也就不是問題

CPU 高速緩存

線程是 CPU 調度的最小單元,線程涉及的目的最終仍然是更充分的利用計算機處理的效能,但是絕大部分的運算任務不能只依靠處理器“計算”就能完成,處理器還需要與內存交互,比如讀取運算數據、存儲運算結果,這個 I/O 操作是很難消除的。而由於計算機的存儲設備與處理器的運算速度差距非常大,所以 現代計算機系統都會增加一層讀寫速度儘可能接近處理器運算速度的高速緩存來作爲內存和處理器之間的緩衝:將運算需要使用的數據複製到緩存中,讓運算能快速進行,當運算結束後再從緩存同步到內存之中

高速緩存從下到上越接近 CPU 速度越快,同時容量也越小。現在大部分的處理器都有二級或者三級緩存,從下到上依次爲 L3 cache, L2 cache, L1 cache.

L1 Cache 一級緩存 本地 core 的緩存 分成 32K 的數據緩存 L1d 和 32k 指令緩存 L1i

訪問 L1 需要 3cycles,

耗時大約 1ns;

L2 Cache 二級緩存 本地 core 的緩存 被設計爲 L1 緩存與共享的 L3 緩存之間的緩衝,大小爲 256K

訪問 L2 需要 12cycles,

耗時大約 3ns;

L3 Cache 三級緩存 在同插槽的所有 core 共享 L3 緩存 分爲多個 2M 的段

訪問 L3 需要 38cycles,

耗時大約 12ns

 

緩存一致性問題

CPU-0 讀取主存的數據,緩存到 CPU-0 的高速緩存中,CPU-1 也做了同樣的事情,而 CPU-1 把 count 的值修改成了 2,並且同步到 CPU-1 的高速緩存,但是這個修改以後的值並沒有寫入到主存中,CPU-0 訪問該字節,由於緩存沒有 更新,所以仍然是之前的值,就會導致數據不一致的問題

引發這個問題的原因是因爲多核心 CPU 情況下存在指令並行執行,而各個CPU 核心之間的數據不共享從而導致緩存一致性問題,爲了解決這個問題,CPU 生產廠商提供了相應的解決方

總線鎖

當一個 CPU 對其緩存中的數據進行操作的時候,往總線中發送一個 Lock 信號。其他處理器的請求將會被阻塞,那麼該處理器可以獨佔共享內存。總線鎖 相當於把 CPU 和內存之間的通信鎖住了,所以這種方式會導致 CPU 的性能下降,所以 P6 系列以後的處理器,出現了另外一種方式,就是緩存鎖

緩存鎖

如果緩存在處理器緩存行中的內存區域在 LOCK 操作期間被鎖定,當它執行鎖操作回寫內存時,處理不在總線上聲明 LOCK 信號,而是修改內部的緩存地址,然後通過緩存一致性機制來保證操作的原子性,因爲緩存一致性機制會阻 止同時修改被兩個以上處理器緩存的內存區域的數據,當其他處理器回寫已經 被鎖定的緩存行的數據時會導致該緩存行無效。 所以如果聲明瞭 CPU 的鎖機制,會生成一個 LOCK 指令,會產生兩個作用

  1. Lock 前綴指令會引起引起處理器緩存回寫到內存,在 P6 以後的處理器中, LOCK 信號一般不鎖總線,而是鎖緩存

  2. 一個處理器的緩存回寫到內存會導致其他處理器的緩存無效

緩存一致性協議 MESI

處理器上有一套完整的協議,來保證 Cache 的一致性,比較經典的應該就是 MESI 協議了,它的方法是在 CPU 緩存中保存一個標記位,這個標記爲有四種 狀態

緩存行(Cache line):緩存存儲數據的單元。

狀態 描述 監聽任務
M(Modified) 修改 該Cache line有效,數據被修改了,和內存中的數據不一致,數據只存在於本Cache中 緩存行必須時刻監聽所有試圖讀該緩存行相對就主存的操作,這種操作必須在緩存將該緩存行寫回主存並將狀態變成S(共享)狀態之前被延遲執行。
E(Exclusive) 獨佔互斥 該Cache line有效,數據和內存中的數據一致,數據只存在於本Cache中。 緩存行也必須監聽其它緩存讀主存中該緩存行的操作,一旦有這種操作,該緩存行需要變成S(共享)狀態。
S(Shared) 共享 該Cache line有效,數據和內存中的數據一致,數據存在於很多Cache中。 緩存行也必須監聽其它緩存使該緩存行無效或者獨享該緩存行的請求,並將該緩存行變成無效(Invalid)。
I(Invalid) 失效 該Cache line無效  

對於M和E狀態而言總是精確的,他們在和該緩存行的真正狀態是一致的,而S狀態可能是非一致的。如果一個緩存將處於S狀態的緩存行作廢了,而另一個緩存實際上可能已經獨享了該緩存行,但是該緩存卻不會將該緩存行升遷爲E狀態,這是因爲其它緩存不會廣播他們作廢掉該緩存行的通知,同樣由於緩存並沒有保存該緩存行的copy的數量,因此(即使有這種通知)也沒有辦法確定自己是否已經獨享了該緩存行。

從上面的意義看來E狀態是一種投機性的優化:如果一個CPU想修改一個處於S狀態的緩存行,總線事務需要將所有該緩存行的copy變成invalid狀態,而修改E狀態的緩存不需要使用總線事務。

每個 Core 的 Cache 控制器不僅知道自己的讀寫操作,也監聽其它 Cache 的讀寫操作,嗅探(snooping)"協議 CPU 的讀取會遵循幾個原則

  1. 如果緩存的狀態是 I,那麼就從內存中讀取,否則直接從緩存讀取

  2. 如果緩存處於 M 或者 E 的 CPU 嗅探到其他 CPU 有讀的操作,就把自己的緩存寫入到內存,並把自己的狀態設置爲 S

  3. 只有緩存狀態是 M 或 E 的時候,CPU 纔可以修改緩存中的數據,修改後,緩存狀態變爲 MC

MESI狀態轉換

對於MESI更深入理解可以去 併發研究之CPU緩存一致性協議(MESI) 這位朋友做了更詳細的闡述

CPU 的優化執行

除了增加高速緩存以爲,爲了更充分利用處理器內內部的運算單元,處理器可 能會對輸入的代碼進行亂序執行優化,處理器會在計算之後將亂序執行的結果 重組,保證該結果與順序執行的結果一致,但並不保證程序中各個語句計算的 先後順序與輸入代碼中的順序一致,這個是處理器的優化執行;還有一個就是 編程語言的編譯器也會有類似的優化,比如做 指令重排 來提升性能

 

併發編程的問題

前面說的和硬件有關的概念你可能聽得有點蒙,還不知道他到底和軟件有啥關 系,其實原子性、可見性、有序性問題,是我們抽象出來的概念,他們的核心 本質就是剛剛提到的緩存一致性問題、處理器優化問題導致的指令重排序問 題。 比如緩存一致性就導致可見性問題、處理器的亂序執行會導致原子性問題、指 令重排會導致有序性問題。爲了解決這些問題,所以在 JVM 中引入了 JMM 的 概念

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