Java內存模型
JMM(java內存模型)編程
java虛擬機有本身的內存模型(Java Memory Model,JMM),JMM能夠屏蔽掉各類硬件和操做系統的內存訪問差別,以實現讓java程序在各類平臺下都能達到一致的內存訪問效果。緩存
JMM決定一個線程對共享變量的寫入什麼時候對另外一個線程可見,JMM定義了線程和主內存之間的抽象關係:共享變量存儲在主內存(Main Memory)中,每一個線程都有一個私有的本地內存(Local Memory),本地內存保存了被該線程使用到的主內存的副本拷貝,線程對變量的全部操做都必須在工做內存中進行,而不能直接讀寫主內存中的變量。這三者之間的交互關係以下多線程
計算機在執行程序時,每條指令都是在CPU中執行的,而執行指令過程當中,勢必涉及到數據的讀取和寫入。因爲程序運行過程當中的臨時數據是存放在主存(物理內存)當中的,這時就存在一個問題,因爲CPU執行速度很快,而從內存讀取數據和向內存寫入數據的過程跟CPU執行指令的速度比起來要慢的多,所以若是任什麼時候候對數據的操做都要經過和內存的交互來進行,會大大下降指令執行的速度。所以在CPU裏面就有了高速緩存。併發
也就是,當程序在運行過程當中,會將運算須要的數據從主存複製一份到CPU的高速緩存當中,那麼CPU進行計算時就能夠直接從它的高速緩存讀取數據和向其中寫入數據,當運算結束以後,再將高速緩存中的數據刷新到主存當中。舉個簡單的例子,好比下面的這段代碼:ide
i = i + 1;
當線程執行這個語句時,會先從主存當中讀取i的值,而後複製一份到高速緩存當中,而後CPU執行指令對i進行加1操做,而後將數據寫入高速緩存,最後將高速緩存中i最新的值刷新到主存當中。post
這個代碼在單線程中運行是沒有任何問題的,可是在多線程中運行就會有問題了。在多核CPU中,每條線程可能運行於不一樣的CPU中,所以每一個線程運行時有本身的高速緩存(對單核CPU來講,其實也會出現這種問題,只不過是以線程調度的形式來分別執行的)。本文咱們以多核CPU爲例。性能
好比同時有2個線程執行這段代碼,假如初始時i的值爲0,那麼咱們但願兩個線程執行完以後i的值變爲2。可是事實會是這樣嗎?優化
可能存在下面一種狀況:初始時,兩個線程分別讀取i的值存入各自所在的CPU的高速緩存當中,而後線程1進行加1操做,而後把i的最新值1寫入到內存。此時線程2的高速緩存當中i的值仍是0,進行加1操做以後,i的值爲1,而後線程2把i的值寫入內存。this
最終結果i的值是1,而不是2。這就是著名的緩存一致性問題。一般稱這種被多個線程訪問的變量爲共享變量。
併發編程中的三個概念
在併發編程中,咱們一般會遇到如下三個問題:原子性問題,可見性問題,有序性問題。咱們先看具體看一下這三個概念:
1.原子性
原子性:即一個操做或者多個操做 要麼所有執行而且執行的過程不會被任何因素打斷,要麼就都不執行。
一個很經典的例子就是銀行帳戶轉帳問題:
好比從帳戶A向帳戶B轉1000元,那麼必然包括2個操做:從帳戶A減去1000元,往帳戶B加上1000元。
試想一下,若是這2個操做不具有原子性,會形成什麼樣的後果。假如從帳戶A減去1000元以後,操做忽然停止。而後又從B取出了500元,取出500元以後,再執行 往帳戶B加上1000元 的操做。這樣就會致使帳戶A雖然減去了1000元,可是帳戶B沒有收到這個轉過來的1000元。
因此這2個操做必需要具有原子性才能保證不出現一些意外的問題。
2.可見性
可見性是指當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其餘線程可以當即看獲得修改的值。
舉個簡單的例子,看下面這段代碼:
1 //線程1執行的代碼 2 int i = 0; 3 i = 10; 4 5 //線程2執行的代碼 6 j = i;
倘若執行線程1的是CPU1,執行線程2的是CPU2。由上面的分析可知,當線程1執行 i =10這句時,會先把i的初始值加載到CPU1的高速緩存中,而後賦值爲10,那麼在CPU1的高速緩存當中i的值變爲10了,卻沒有當即寫入到主存當中。
此時線程2執行 j = i,它會先去主存讀取i的值並加載到CPU2的緩存當中,注意此時內存當中i的值仍是0,那麼就會使得j的值爲0,而不是10。
這就是可見性問題,線程1對變量i修改了以後,線程2沒有當即看到線程1修改的值。
3.有序性
有序性:即程序執行的順序按照代碼的前後順序執行。舉個簡單的例子,看下面這段代碼:
int i = 0; boolean flag = false; i = 1; //語句1 flag = true; //語句2
從代碼順序上看,語句1是在語句2前面的,那麼JVM在真正執行這段代碼的時候會保證語句1必定會在語句2前面執行嗎?不必定,爲何呢?這裏可能會發生指令重排序(Instruction Reorder)。
通常來講,處理器爲了提升程序運行效率,可能會對輸入代碼進行優化,它不保證程序中各個語句的執行前後順序同代碼中的順序一致,可是它會保證程序最終執行結果和代碼順序執行的結果是一致的。
好比上面的代碼中,語句1和語句2誰先執行對最終的程序結果並無影響,那麼就有可能在執行過程當中,語句2先執行而語句1後執行。
可是重排序也須要遵照必定規則:
1.重排序操做不會對存在數據依賴關係的操做進行重排序。
好比:a=1;b=a; 這個指令序列,因爲第二個操做依賴於第一個操做,因此在編譯時和處理器運行時這兩個操做不會被重排序。
2.重排序是爲了優化性能,可是無論怎麼重排序,單線程下程序的執行結果不能被改變
好比:a=1;b=2;c=a+b這三個操做,第一步(a=1)和第二步(b=2)因爲不存在數據依賴關係,因此可能會發生重排序,可是c=a+b這個操做是不會被重排序的,由於須要保證最終的結果必定是c=a+b=3。
volatile關鍵字
volatile是Java提供的一種輕量級的同步機制。同synchronized相比(synchronized一般稱爲重量級鎖),volatile更輕量級。
一旦一個共享變量(類的成員變量、類的靜態成員變量)被volatile修飾以後,那麼就具有了兩層語義:
1)保證了不一樣線程對這個變量進行操做時的可見性,即一個線程修改了某個變量的值,這新值對其餘線程來講是當即可見的。
2)禁止進行指令重排序。
一、共享變量的可見性
public class TestVolatile { public static void main(String[] args) { ThreadDemo td = new ThreadDemo(); new Thread(td).start(); while(true){ if(td.isFlag()){ System.out.println("------------------"); break; } } } } class ThreadDemo implements Runnable { private boolean flag = false; @Override public void run() { try { Thread.sleep(200); } catch (InterruptedException e) { } flag = true; System.out.println("flag=" + isFlag()); } public boolean isFlag() { return flag; } }
上面這個例子,開啓一個多線程去改變flag爲true,main 主線程中能夠輸出"------------------"嗎?
答案是NO!
這個結論會讓人有些疑惑,能夠理解。開啓的線程雖然修改了flag 的值爲true,可是還沒來得及寫入主存當中,此時main裏面的 td.isFlag()仍是false,可是因爲 while(true) 是底層的指令來實現,速度很是之快,一直循環都沒有時間去主存中更新td的值,因此這裏會形成死循環!運行結果以下:
此時線程是沒有中止的,一直在循環。
如何解決呢?只需將 flag 聲明爲volatile,便可保證在開啓的線程A將其修改成true時,main主線程能夠馬上得知:
第一:使用volatile關鍵字會強制將修改的值當即寫入主存;
第二:使用volatile關鍵字的話,當開啓的線程進行修改時,會致使main線程的工做內存中緩存變量flag的緩存行無效(反映到硬件層的話,就是CPU的L1緩存中對應的緩存行無效);
第三:因爲線程main的工做內存中緩存變量flag的緩存行無效,因此線程main再次讀取變量flag的值時會去主存讀取。
volatile具有兩種特性,第一就是保證共享變量對全部線程的可見性。將一個共享變量聲明爲volatile後,會有如下效應:
1.當寫一個volatile變量時,JMM會把該線程對應的本地內存中的變量強制刷新到主內存中去;
2.這個寫會操做會致使其餘線程中的緩存無效。
二、禁止進行指令重排序
這裏咱們引用上篇文章單例裏面的例子
1 class Singleton{ 2 private volatile static Singleton instance = null; 3 4 private Singleton() { 5 } 6 7 public static Singleton getInstance() { 8 if(instance==null) { 9 synchronized (Singleton.class) { 10 if(instance==null) 11 instance = new Singleton(); 12 } 13 } 14 return instance; 15 } 16 }
instance = new Singleton(); 這段代碼能夠分爲三個步驟:
一、memory = allocate() 分配對象的內存空間
二、ctorInstance() 初始化對象
三、instance = memory 設置instance指向剛分配的內存
可是此時有可能發生指令重排,CPU 的執行順序可能爲:
一、memory = allocate() 分配對象的內存空間
三、instance = memory 設置instance指向剛分配的內存
二、ctorInstance() 初始化對象
在單線程的狀況下,1->3->2這種順序執行是沒有問題的,可是若是是多線程的狀況則有可能出現問題,線程A執行到11行代碼,執行了指令1和3,此時instance已經有值了,值爲第一步分配的內存空間地址,可是尚未進行對象的初始化;
此時線程B執行到了第8行代碼處,此時instance已經有值了則return instance,線程B 使用instance的時候,就會出現異常。
這裏能夠使用 volatile 來禁止指令重排序。
從上面知道volatile關鍵字保證了操做的可見性和有序性,可是volatile能保證對變量的操做是原子性嗎?
下面看一個例子:
package com.mmall.concurrency.example.count; import java.util.concurrent.CountDownLatch; /** * @author: ChenHao * @Description: * @Date: Created in 15:05 2018/11/16 * @Modified by: */ public class CountTest { // 請求總數 public static int clientTotal = 5000; public static volatile int count = 0; public static void main(String[] args) throws Exception { //使用CountDownLatch來等待計算線程執行完 final CountDownLatch countDownLatch = new CountDownLatch(clientTotal); //開啓clientTotal個線程進行累加操做 for(int i=0;i<clientTotal;i++){ new Thread(){ public void run(){ count++;//自加操做 countDownLatch.countDown(); } }.start(); } //等待計算線程執行完 countDownLatch.await(); System.out.println(count); } }
執行結果:
針對這個示例,一些同窗可能會以爲疑惑,若是用volatile修飾的共享變量能夠保證可見性,那麼結果不該該是5000麼?
問題就出在count++這個操做上,由於count++不是個原子性的操做,而是個複合操做。咱們能夠簡單講這個操做理解爲由這三步組成:
1.讀取count
2.count 加 1
3.將count 寫到主存
因此,在多線程環境下,有可能線程A將count讀取到本地內存中,此時其餘線程可能已經將count增大了不少,線程A依然對過時的本地緩存count進行自加,從新寫到主存中,最終致使了count的結果不合預期,而是小於5000。
那麼如何來解決這個問題呢?下面咱們來看看
Atomic包
在java 1.5的java.util.concurrent.atomic包下提供了一些原子操做類,即對基本數據類型的 自增(加1操做),自減(減1操做)、以及加法操做(加一個數),減法操做(減一個數)進行了封裝,保證這些操做是原子性操做。atomic是利用CAS來實現原子性操做的(Compare And Swap)
package com.mmall.concurrency.example.count; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; /** * @author: ChenHao * @Description: * @Date: Created in 15:05 2018/11/16 * @Modified by: */ public class CountTest { // 請求總數 public static int clientTotal = 5000; public static AtomicInteger count = new AtomicInteger(0); public static void main(String[] args) throws Exception { //使用CountDownLatch來等待計算線程執行完 final CountDownLatch countDownLatch = new CountDownLatch(clientTotal); //開啓clientTotal個線程進行累加操做 for(int i=0;i<clientTotal;i++){ new Thread(){ public void run(){ count.incrementAndGet();//先加1,再get到值 countDownLatch.countDown(); } }.start(); } //等待計算線程執行完 countDownLatch.await(); System.out.println(count); } }
執行結果:
下面咱們來看看原子類操做的基本原理
1 public final int incrementAndGet() { 2 return unsafe.getAndAddInt(this, valueOffset, 1) + 1; 3 } 4 5 public final int getAndAddInt(Object var1, long var2, int var4) { 6 int var5; 7 do { 8 var5 = this.getIntVolatile(var1, var2); 9 } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); 10 11 return var5; 12 } 13 14 /*** 15 * 獲取obj對象中offset偏移地址對應的整型field的值。 16 * @param obj 包含須要去讀取的field的對象 17 * @param obj中整型field的偏移量 18 */ 19 public native int getIntVolatile(Object obj, long offset); 20 21 /** 22 * 比較obj的offset處內存位置中的值和指望的值,若是相同則更新。此更新是不可中斷的。 23 * 24 * @param obj 須要更新的對象 25 * @param offset obj中整型field的偏移量 26 * @param expect 但願field中存在的值 27 * @param update 若是指望值expect與field的當前值相同,設置filed的值爲這個新值 28 * @return 若是field的值被更改返回true 29 */ 30 public native boolean compareAndSwapInt(Object obj, long offset, int expect, int update);
首先介紹一下什麼是Compare And Swap(CAS)?簡單的說就是比較並交換。
CAS 操做包含三個操做數 —— 內存位置(V)、預期原值(A)和新值(B)。若是內存位置的值與預期原值相匹配,那麼處理器會自動將該位置值更新爲新值。不然,處理器不作任何操做。不管哪一種狀況,它都會在 CAS 指令以前返回該位置的值。CAS 有效地說明了「我認爲位置 V 應該包含值 A;若是包含該值,則將 B 放到這個位置;不然,不要更改該位置,只告訴我這個位置如今的值便可。」 Java併發包(java.util.concurrent)中大量使用了CAS操做,涉及到併發的地方都調用了sun.misc.Unsafe類方法進行CAS操做。
咱們來分析下incrementAndGet的邏輯:
1.先獲取當前的value值
2.調用compareAndSet方法來來進行原子更新操做,這個方法的語義是:
先檢查當前value是否等於obj中整型field的偏移量處的值,若是相等,則意味着obj中整型field的偏移量處的值 沒被其餘線程修改過,更新並返回true。若是不相等,compareAndSet則會返回false,而後循環繼續嘗試更新。
第一次count 爲0時線程A調用incrementAndGet時,傳參爲 var1=AtomicInteger(0),var2爲var1 裏面 0 的偏移量,好比爲8090,var4爲須要加的數值1,var5爲線程工做內存值,do裏面會先執行一次,經過getIntVolatile 獲取obj對象中offset偏移地址對應的整型field的值此時var5=0;while 裏面compareAndSwapInt 比較obj的8090處內存位置中的值和指望的值var5,若是相同則更新obj的值爲(var5+var4=1),此時更新成功,返回true,則 while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));結束循環,return var5。
當count 爲0時,線程B 和線程A 同時讀取到 count ,進入到第 8 行代碼處,線程B 也是取到的var5=0,當線程B 執行到compareAndSwapInt時,線程A已經執行完compareAndSwapInt,已經將內存地址爲8090處的值修改成1,此時線程B 執行compareAndSwapInt返回false,則繼續循環執行do裏面的語句,再次取內存地址偏移量爲8090處的值爲1,再去執行compareAndSwapInt,更新obj的值爲(var5+var4=2),返回爲true,結束循環,return var5。
CAS的ABA問題
固然CAS也並不完美,它存在"ABA"問題,倘若一個變量初次讀取是A,在compare階段依然是A,但其實可能在此過程當中,它先被改成B,再被改回A,而CAS是沒法意識到這個問題的。CAS只關注了比較先後的值是否改變,而沒法清楚在此過程當中變量的變動明細,這就是所謂的ABA漏洞。