計算機中執行程序時,每條指令都是在CPU中執行,執行指令的過程必然會涉及到數據的讀取和寫入。而程序運行時的數據是存放在主存(物理內存)中,因爲CPU的讀寫速度遠遠高於內存的速度,若是CPU直接和內存交互,會大大下降指令的執行速度,因此CPU裏面就引入了高速緩存。html
腦補當初學習OS時的圖 CPU->內存 CPU->寄存器->內存java
也就是說程序運行時,會將運算所須要的數據從主存中複製一份到高速緩存,CPU進行計算的時候能夠直接從高速緩存讀取和寫入,當運算結束時,在將高速緩存中的數據刷新到主存。編程
可是若是那樣必需要考慮,在多核CPU下數據的一致性問題怎麼保證?好比i=i+1
,當線程執行這條時,會先從主存中讀取i的值,而後複製一份到高速緩存,而後CPU執行指令對i進行加1操做,而後將數據寫入高速緩存,最後將高速緩存中i最新的值刷新到主存當中。在單線程下這段代碼運行不會存在問題,但若是在多線程下多核CPU中,每一個CPU都有本身的高速緩存,可能存在下面一種狀況:初始時,兩個線程分別讀取i的值存入各自所在的CPU的高速緩存當中,而後線程1進行加1操做,而後把i的最新值1寫入到內存。此時線程2的高速緩存當中i的值仍是0,進行加1操做以後,i的值爲1,而後線程2把i的值寫入內存。最終結果i的值是1,而不是2。這就是著名的緩存一致性問題。一般稱這種被多個線程訪問的變量爲共享變量。緩存
爲了解決緩存不一致性問題,一般來講有如下2種解決方法:多線程
經過在總線加LOCK#鎖的方式架構
經過緩存一致性協議併發
併發編程中,一般會考慮的三個問題原子性問題、可見性問題、有序性問題。app
(1)原子性:程序中的單步操做或多步操做要麼所有執行而且執行的過程當中不能被打斷,要麼都不執行。性能
若是程序中不具有原子性會出現哪些問題?學習
轉帳操做就是一個很好的表明,若是轉帳的過程當中被中斷,錢轉出去了,因爲中斷,收帳方卻沒有收到。
(2)可見性:內存可見性,指的是線程之間的可見性,當一個線程修改了共享變量時,另外一個線程能夠讀取到這個修改後的值。
//線程1執行的代碼 int i = 0; i = 10; //線程2執行的代碼 j = i;
假若線程1從主存中讀取了i的值並複製到CPU高速緩存,而後對i修改成10,這時CPU高速緩存中的i值爲10,在沒有將高速緩存中的值刷新到主存中時,線程2讀取到的值仍是0,它看不到i值的變化,這就是可見性問題。
Java提供了Volatile關鍵字來保證可見性,當一個共享變量被volatile修飾時,它會保證修改的值會當即被更新到主存,當有其餘線程須要讀取時,它會去內存中讀取新值。
而普通的共享變量不能保證可見性,由於普通共享變量被修改以後,何時被寫入主存是不肯定的,當其餘線程去讀取時,此時內存中可能仍是原來的舊值,所以沒法保證可見性。
另外,經過synchronized和Lock也可以保證可見性,synchronized和Lock能保證同一時刻只有一個線程獲取鎖而後執行同步代碼,而且在釋放鎖以前會將對變量的修改刷新到主存當中。所以能夠保證可見性。
(3)有序性:程序執行的順序按照代碼的前後順序執行。
實際是這樣嗎?
int i = 0; //[1] int a,b; //[2]
[2]必定會在[1]以後執行嗎?不必定,在JVM中,有可能會發生指令重排序(Instruction Reorder)。若是[1]、[2]中有相互依賴,好比[2]中的數據依賴於[1]的結果,那麼則不會發生指令重排序。
什麼是指令重排序?
通常來講,處理器爲了提升程序運行效率,可能會對輸入代碼進行優化,它不保證程序中各個語句的執行前後順序同代碼中的順序一致,可是它會保證程序最終執行結果和代碼順序執行的結果是一致的。
指令重排對於提⾼CPU處理性能⼗分必要。雖然由此帶來了亂序的問題,可是這點犧牲是值得的。
指令重排能夠保證串⾏語義⼀致,可是沒有義務保證多線程間的語義也⼀致。因此在多線程下,指令重排序可能會致使⼀些問題。
JVM能夠看作是一個有OS架構的處理機,他也有本身的內存和處理器,它的內存和以前討論的沒有什麼太大的差別。
Java運行時內存的劃分以下:
對於每⼀個線程來講,棧都是私有的,而堆是共有的。也就是說在棧中的變量(局部變量、⽅法定義參數、異常處理器參數)不會在線程之間共享,也就不會有內存可⻅性(下⽂會說到)的問題,也不受內存模型的影
響。⽽在堆中的變量是共享的,本⽂稱爲共享變量。因此內存可見性針對的是共享變量。
一、既然堆是共享的,爲何在堆中會有內存不可⻅問題?
Java內存模型規定全部的變量都是存在主存當中(相似於前面說的物理內存),每一個線程都有本身的工做內存(相似於前面的高速緩存)。線程對變量的全部操做都必須在工做內存中進行,而不能直接對主存進行操做。而且每一個線程不能訪問其餘線程的工做內存。
線程之間的共享變量存在主內存中,每一個線程都有⼀個私有的本地內存,存儲了該線程以讀、寫共享變量的副本。本地內存是Java內存模型的⼀個抽象概念,並不真實存在。它涵蓋了緩存、寫緩衝區、寄存器等。
Java線程之間的通訊由Java內存模型(簡稱JMM)控制,從抽象的⻆度來講,JMM定義了線程和主內存之間的抽象關係。JMM的抽象示意圖如圖所示:
從圖中能夠看出:
二、JMM與Java內存區域劃分的區別與聯繫
區別
JMM是抽象的,他是⽤來描述⼀組規則,經過這個規則來控制各個變量的訪問⽅式,圍繞原⼦性、有序性、可⻅性等展開的。⽽Java運⾏時內存的劃分是具體的,是JVM運⾏Java程序時,必要的內存劃分。
聯繫
都存在私有數據區域和共享數據區域。⼀般來講,JMM中的主內存屬於共享數據區域,他是包含了堆和⽅法區;一樣,JMM中的本地內存屬於私有數據區域,包含了程序計數器、本地⽅法棧、虛擬機棧。
原子性、可見性、有序性
Java內存模型具有一些先天的「有序性」,即不須要經過任何手段就可以獲得保證的有序性,這個一般也稱爲 happens-before 原則。若是兩個操做的執行次序沒法從happens-before原則推導出來,那麼它們就不能保證它們的有序性,虛擬機能夠隨意地對它們進行重排序。
在Java虛擬機規範中試圖定義一種Java內存模型(Java Memory Model,JMM)來屏蔽各個硬件平臺和操做系統的內存訪問差別,以實現讓Java程序在各類平臺下都能達到一致的內存訪問效果。那麼Java內存模型規定了哪些東西呢,它定義了程序中變量的訪問規則,往大一點說是定義了程序執行的次序。注意,爲了得到較好的執行性能,Java內存模型並無限制執行引擎使用處理器的寄存器或者高速緩存來提高指令執行速度,也沒有限制編譯器對指令進行重排序。也就是說,在java內存模型中,也會存在緩存一致性問題和指令重排序的問題。
在Java中,volatile關鍵字有特殊的內存語義。volatile主要有如下兩個功能:
內存可見性
所謂內存可見性,指的是當一個線程對volatile
修飾的變量進行過寫操做時,JMM會當即把線程對應的本地內存中的共享變量的值刷新到主內存;當一個線程對volatile
修飾的變量進行讀操做時,JMM會當即把該線程對應的本地內存置爲無效,從內存中重新讀取共享變量的值。
禁止重排序
JMM是經過內存屏障來限制處理器對指令的重排序的。
什麼是內存屏障?硬件層面,內存屏障分兩種:讀屏障(Load Barrier)和寫屏障(Store Barrier)。內存屏障有兩個做用:
通俗說,經過內存屏障,能夠防止指令重排序時,不會將屏障後面的指令排到以前,也不會將屏障以前的指令排到以後。
單例模式下的Double-Check(雙重鎖檢查)
public class Singleton { public static Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance == null) { //[1] synchronized (Singleton.class) { instance = new Singleton(); //[2] } } return instance; } }
若是這裏的變量沒有使用volatile關鍵字,那麼有可能就會發生錯誤。
[2]實例化對象的過程能夠分爲分配內存、初始化對象、引用賦值。
instance = new Singleton(); // [1] // 能夠分解爲如下三個步驟 1 memory=allocate();// 分配內存 至關於c的malloc 2 ctorInstanc(memory) //初始化對象 3 s=memory //設置s指向剛分配的地址 // 上述三個步驟可能會被重排序爲 1-3-2,也就是: 1 memory=allocate();// 分配內存 至關於c的malloc 3 s=memory //設置s指向剛分配的地址 2 ctorInstanc(memory) //初始化對象
若是一旦發生了上述的重排序,當程序執行了1和3,這時線程A執行了if判斷,斷定instance不爲空,而後直接返回了一個未初始化的instance。