Java內存模型描述了Java程序中各類變量(共享變量)的訪問規則,以及在JVM中將變量存儲到內存和從內存中讀取這些變量的底層細節。程序員
在開始併發編程時,咱們須要思考兩個關鍵的問題:1.線程之間如何通訊?2.線程之間如何同步?編程
在命令式編程中,線程之間有兩種通訊方式:數組
同步是指程序用於控制線程發生相對順序執行的機制。在共享內存模型裏,程序員須要給代碼加上制定的互斥操做來顯式進行;在消息傳遞模型中,通訊是對程序員透明的,是隱式進行的。緩存
全部的實例域、靜態域、數組元素是儲存在堆中的,線程之間能夠共享,能夠將它們稱爲「共享變量」,他們可能會在併發編程時出現「可見性」問題;而局部變量、方法參數、異常處理參數不會在線程之間共享,不受內存模型的影響。bash
假如一個變量被多個線程使用到,那麼這個共享變量會在多個線程的工做內存中都存在副本。多線程
當一個共享變量被一個線程修改,可以及時被其餘線程看到,這叫作可見性。併發
若是兩個操做訪問同一個變量,並且這兩個操做中有一個爲寫操做,那麼這兩個操做之間就存在了數據依賴性。數據依賴性存在如下三種狀況:性能
操做 | 示例 |
---|---|
先寫,後讀 | a=1;b=a; |
先寫,後寫 | a=1;a=2; |
先讀,後寫 | b=a;a=1; |
不難發現,上面的三種狀況,只要重排序其指令,結果都會產生變化。優化
因此編譯器和處理器在進行重排序時,必須遵照數據依賴性。不能對存在數據依賴性的兩個操做進行重排序。ui
看下面一段代碼
if(flag){ //操做1
int num=a+b; //操做2
}
複製代碼
能夠看到,操做1和操做2並不存在數據依賴,可是存在控制依賴。當代碼中出現控制依賴時,會影響程序的並行度。所以,編譯器和處理器會採用一種「猜想執行」來克服控制依賴性對並行度的影響(並行是爲了效率和性能)處理器可能提早執行操做2,將a+b計算出來,並放置到一個叫「重排序緩存」的緩存中,假如得知操做1中的flag爲真,再將結果寫入num中。
在單線程程序中,對存在控制依賴關係的重排序,不會影響結果;不過在多線程中,可能會影響到結果。
實際執行的代碼順序和程序員書寫的順序是不同的,編譯器或處理器爲了提升性能,在不影響程序結果的前提下,會進行執行順序的優化。
經過互斥鎖來實現。
可以保證volatile變量的可見性,可是不能保證volatile變量複合操做的原子性。 volatile
經過加入內存屏障和禁止指令重排序來實現可見性的。對volatile
變量執行寫操做時,會在寫入後加一條store
的屏障指令;對volatile
變量執行讀操做時,會在讀操做前加入一條load
屏障指令。
因爲處理器的速度很快,爲了不處理器停頓下來等待內存(內存確定跟不上處理器的速度)而產生的延遲,現代的處理器使用緩存區來臨時保存處理器向內存寫入的數據,而後再提供給內存,以保證接二連三地高效運行。
雖然緩存區存在諸多好處,可是它是僅對處理器可見的。這將會產生一個重要的問題:處理器堆內存的獨寫操做的順序,可能與內存中實際發生讀寫操做順序不一致。(由於現代的處理器大都容許使用重排序)
因此,爲了保證可見性,Java編譯器會在生成指令序列時,插入內存屏障來禁止特定的處理器進行重排序。
volatile
變量的過程:volatile
變量副本的值volatile
變量的過程:volatile
變量的最新的值到工做內存中。volatile
不能保證volatile
變量符合操做的原子性:舉一個例子:
public class VolatileDemo{
private volatile int num=0;
public int getNumber(){
return this.num;
}
public void increase(){
this.num++;
}
public static void main(String[] args){
final VolatileDemo v=new VolatileDemo();
for(int i;i<500;i++){
new Thread(new Runnable(){
public void run(){
v.increase();
}
}).start();
}
//主線程主動讓出資源讓500個子線程運行。這個‘1’指的是主線程
while(Thread.activeCount()>1){
Thread.yield();
}
System.out.println(v.getNumber());
}
}
複製代碼
這個程序是開啓500個線程,每一個線程執行一次increase()
操做,給變量num
加一。運行這個程序屢次,發現並非每次輸出結果都是500。
發生了什麼問題?
由於this.num++
這條語句,實際上是三步操做,不具有原子性。假設一個運行場景:
可見,進行了兩次加1操做,可是主存中的num只增長了1。怎麼解決呢?咱們要保證num自增操做的原子性。