在現代計算機操做系統中,多任務處理幾乎是一項必備的功能,由於嵌入了多核處理器,計算機系統真正作到了同一時間執行若干個任務,是名副其實的多核系統。在多核系統中,爲了提高CPU與內存的交互效率,通常都設置了一層 「高速緩存區」 做爲內存與處理器之間的緩衝,使得CPU在運算的過程當中直接從高速緩存區讀取數據,必定程度上解決了性能的問題。可是,這樣也帶來了一個新問題,就是「緩存一致性」的問題。好比,多核的狀況下,每一個處理器都有本身的緩存區,數據如何保持一致性。針對這個問題,現代的計算機系統引入多處理器的數據一致性的協議,包括MOSI、Synapse、Firely、DragonProtocol等。java
當處理器經過高速緩存區與主內存發生交互時,對數據的讀寫必須遵循協議規定的標準,用一張關係圖表示的話大概以下:編程
Java內存模型規定了全部的變量都存儲在主內存中,每一個線程擁有本身的工做內存,工做內存中保存了被該線程使用的變量的主內存副本拷貝,線程只能操做本身工做內存的變量副本,操做完變量後會更新到主內存,經過主內存來完成與其餘線程間變量值的傳遞。此模型的交互關係以下圖所示:緩存
舉一個例子,在程序中對一個共享變量作自增操做:安全
i++;
複製代碼
假設初始化的時候i=0,當跑到此程序時,線程首先從主內存讀取i的值,而後複製到本身的工做內存,進行i++操做,最後將操做後的結果從工做內存複製到主內存中。若是是兩個線程執行i++的程序,預期的結果是2。但真的是這樣嗎?答案是否認的。bash
假設線程1讀取主內存的i=0,複製到本身的工做內存,在進行i++的操做後還沒來得及更新到主內存,這時線程2也讀取i=0,作了一樣的操做,那麼最終獲得的結果爲1,而不是2。多線程
這是典型的關於多線程併發安全例子,也是Java併發編程中最值得探討的話題之一,通常來講,處理這種問題有兩種手段:併發
由於本文主要是探究 JMM 和 volatile 關鍵字的知識,具體怎麼實現併發處理就不作深刻探討了,改天看看抽個時間再寫篇博文專門講解好了。ide
初步瞭解完什麼是JMM後,咱們來進一步瞭解它的重要特徵。值得說明的是,在Java多線程開發中,遵循着三個基本特性,分別是原子性、可見性和有序性,而Java的內存模型正是圍繞着在併發過程當中如何處理這三個特徵創建的。函數
原子性是指操做是原子性的,不可中斷的。舉個例子:高併發
String s="abc";
複製代碼
這個操做是直接賦值,是原子性操做。而相似下面這段代碼就不是原子性了:
i++;
複製代碼
當執行i++時,須要先獲取i的值,而後再執行i+1,至關於包含了兩個操做,因此不是原子性。
可見性是指共享數據的時候,一個線程修改了數據,其餘線程知道數據被修改,會從新讀取最新的主存的數據。就像前面說的兩個線程處理i++的問題,線程1改完後沒有更新到主內存,因此線程2是不知道的。
是指代碼執行的有序性,對於一個線程執行的代碼,咱們能夠認爲代碼是依次執行的,但併發中可能就會出現亂序,由於代碼有可能發生指令重排序(Instruction Reorder),重排後的指令與原指令的順序未必一致。
編譯器可以自由的以優化的名義去改變指令順序。在特定的環境下,處理器可能會次序顛倒的執行指令。是爲指令的重排序,尤爲是併發的狀況下。
java提供了volatile和synchronized來保證線程之間操做的有序性。volatile含有禁止指令重排序的語義(即它的第二個語義),synchronized規定一個變量在同一時刻只容許一條線程對其lock操做,也就是說同一個鎖的兩個同步塊只能串行進入。禁止了指令的重排序。
說到了volatile,咱們就有必要了解一下這個關鍵字是作什麼的。
準確來講,volatile是java提供的輕量的同步機制。它有兩個特性:
簡單寫段代碼說明一下:
public class VolatileDemo {
private static boolean isReady;
private static int number;
private static class ReaderThread extends Thread{
@Override
public void run() {
while (!isReady);
System.out.println("number = "+number);
}
}
public static void main(String[] args) {
new ReaderThread().start();
try {
Thread.sleep(1000);
number = 42;
isReady = true;
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
複製代碼
在上面的代碼中,ReaderThread
只有在isReady
爲 true 時纔會打印出 number
的值,然而,真實的狀況有多是打印不出來(可能性比較小,但仍是有),由於線程ReaderThread線程沒法看到主線程中對isReady
的修改,致使while循環永遠沒法退出,同時,由於有可能發生指令重排,致使下面的代碼不能按順序執行:
number = 42;
isReady = true;
複製代碼
也就是能打印的話,number值多是0,不是42。若是在變量加上volatile關鍵字,告訴Java虛擬機這兩個變量可能會被不一樣的線程修改,那麼就能夠防止上述兩種不正常的狀況的發生。
volatile能保證可見性和有序性,但沒法保證原子性,好比下面的例子:
public class VolatileDemo {
public static volatile int i = 0;
public static void increase() {
i++;
}
public static void main(String[] args) throws InterruptedException {
VolatileDemo test = new VolatileDemo();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++)
test.increase();
}).start();
}
Thread.sleep(1000);
System.out.println(test.i);
}
}
複製代碼
正常狀況下,咱們指望上面的main函數執行完後輸出的結果是10000,但你會發現,結果老是會小於10000,由於increase()方法中的i++
操做不是原子性的,分紅了讀和寫兩個操做。假設當線程1讀取了 i 的值,尚未修改,線程2這時也進行了讀取。而後,線程1修改完了,通知線程2從新讀取 i 的值,可這時它不須要讀取 i,它仍執行寫操做,而後賦值給主線程,這時數據就會出現問題。
因此,通常針對共享變量的讀寫操做,仍是須要用鎖來保證結果,例如加上 synchronized關鍵字。
參考:
《Java高併發程序設計》
《深刻理解Java虛擬機》