你們都知道,線程會存在安全性問題,那接下來咱們從原理層面去了解線程爲編程
什麼會存在安全性問題,而且咱們應該怎麼去解決這類的問題。數組
其實線程安全問題能夠總結爲: 可見性、原子性、有序性這幾個問題,咱們搞緩存
懂了這幾個問題而且知道怎麼解決,那麼多線程安全性問題也就不是問題了安全
CPU 高速緩存多線程
線程是 CPU 調度的最小單元,線程涉及的目的最終仍然是更充分的利用計算機併發
處理的效能,可是絕大部分的運算任務不能只依靠處理器「計算」就能完成,處編程語言
理器還須要與內存交互,好比讀取運算數據、存儲運算結果,這個 I/O 操做是ide
很難消除的。而因爲計算機的存儲設備與處理器的運算速度差距很是大,因此oop
現代計算機系統都會增長一層讀寫速度儘量接近處理器運算速度的高速緩存性能
來做爲內存和處理器之間的緩衝:將運算須要使用的數據複製到緩存中,讓運
算能快速進行,當運算結束後再從緩存同步到內存之中。
高速緩存從下到上越接近 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 指令,會產生兩個做用
Lock 前綴指令會引發引發處理器緩存回寫到內存,在 P6 之後的處理器中,LOCK 信號通常不鎖總線,而是鎖緩存
一個處理器的緩存回寫到內存會致使其餘處理器的緩存無效
緩存一致性協議
處理器上有一套完整的協議,來保證 Cache 的一致性,比較經典的應該就是
MESI 協議(梅西協議)了,它的方法是在 CPU 緩存中保存一個標記位,這個標記爲有四種狀態:
M(Modified) 修改緩存,當前 CPU 緩存已經被修改,表示已經和內存中的數據不一致了
I(Invalid) 失效緩存,說明 CPU 的緩存已經不能使用了
E(Exclusive) 獨佔緩存,當前 cpu 的緩存和內存中數據保持一直,並且其餘處理器沒有緩存該數據
S(Shared) 共享緩存,數據和內存中數據一致,而且該數據存在多個 cpu緩存中
每一個 Core 的 Cache 控制器不只知道本身的讀寫操做,也監聽其它 Cache 的讀
寫操做,嗅探(snooping)"協議
CPU 的讀取會遵循幾個原則:
若是緩存的狀態是 I(失效緩存),那麼就從內存中讀取,不然直接從緩存讀取
若是緩存處於 M 或者 E 的 CPU 嗅探到其餘 CPU 有讀的操做,就把本身的緩存寫入到內存,並把本身的狀態設置爲 S
只有緩存狀態是 M 或 E 的時候,CPU 才能夠修改緩存中的數據,修改後,緩存狀態變爲 MC
CPU 的優化執行
除了增長高速緩存覺得,爲了更充分利用處理器內內部的運算單元,處理器可
能會對輸入的代碼進行亂序執行優化,處理器會在計算以後將亂序執行的結果
充足,保證該結果與順序執行的結果一直,但並不保證程序中各個語句計算的
前後順序與輸入代碼中的順序一致,這個是處理器的優化執行;還有一個就是
編程語言的編譯器也會有相似的優化,好比作指令重排來提高性能。
併發編程的問題
前面說的和硬件有關的概念你可能聽得有點蒙,還不知道他到底和軟件有啥關
系,其實原子性、可見性、有序性問題,是咱們抽象出來的概念,他們的核心
本質就是剛剛提到的緩存一致性問題、處理器優化問題致使的指令重排序問
題。
好比緩存一致性就致使可見性問題、處理器的亂序執行會致使原子性問題、指
令重排會致使有序性問題。爲了解決這些問題,因此在 JVM 中引入了 JMM 的
概念
內存模型
內存模型定義了共享內存系統中多線程程序讀寫操做行爲的規範,來屏蔽各類
硬件和操做系統的內存訪問差別,來實現 Java 程序在各個平臺下都能達到一致
的內存訪問效果。Java 內存模型的主要目標是定義程序中各個變量的訪問規
則,也就是在虛擬機中將變量存儲到內存以及從內存中取出變量(這裏的變
量,指的是共享變量,也就是實例對象、靜態字段、數組對象等存儲在堆內存
中的變量。而對於局部變量這類的,屬於線程私有,不會被共享)這類的底層
細節。經過這些規則來規範對內存的讀寫操做,從而保證指令執行的正確性。
它與處理器有關、與緩存有關、與併發有關、與編譯器也有關。他解決了 CPU
多級緩存、處理器優化、指令重排等致使的內存訪問問題,保證了併發場景下
的可見性、原子性和有序性,。內存模型解決併發問題主要採用兩種方式:限
制處理器優化和使用內存屏障。
Java 內存模型定義了線程和內存的交互方式,在 JMM (Java Memory Model)抽象模型中,分爲主內存、工做內存。主內存是全部線程共享的,工做內存是每一個線程獨有的。線程對變量的全部操做(讀取、賦值)都必須在工做內存中進行,不能直接讀寫主內存中的變量。而且不一樣的線程之間沒法訪問對方工做內存中的變量,線程間的變量值的傳遞都須要經過主內存來完成,他們三者的交互關係以下:
因此,總的來講,JMM 是一種規範,目的是解決因爲多線程經過共享內存進行
通訊時,存在的本地內存數據不一致、編譯器會對代碼指令重排序、處理器會
對代碼亂序執行等帶來的問題。目的是保證併發編程場景中的原子性、可見性
和有序性。
因爲邊寫是在電腦上,因此可能會致使手機端看起來排版不太美觀,後期我會把這一塊優化一下,儘可能作到兼容
參考:Java併發編程的藝術