記憶體模型的相關概念:
程式的執行過程為:主存->複製到cache(CPU的快取記憶體)->CPU->重新整理cache->回寫到主存中 共享變數:被多個執行緒同時訪問的變數 特殊情況:主存中i=0,程式A和B分別讀i=0至核心1和核心2(多核CPU)的cache,進行i=i+1,則對這個共享變數雖然操作了兩次,但是最後寫回主存還是i=1。具體過程如下:
|-------主存-------|----核心1的cache----|----核心2的cache----|
|-------i=0-------|-----i=0-----------|------i=0-----------|
|-------i=0-------|-----i=1-----------|------i=1-----------|
|-------i=1-------|-----i=1-----------|------i=1-----------|
複製程式碼
這就是著名的快取一致性問題 問題的解決方案MESI協議:當CPU寫資料時,如果發現這個變數是共享變數,則通知其他的CPU設定該共享變數的快取行為無效,當其他CPU需要讀取這個變數時,發現自己的快取中快取該變數的快取行是無效的,它就會從記憶體中讀取
原子性:
要麼全部執行,要麼全部不執行,不會被打斷。
eg.對一個32位的int型變數賦值:
eg.i=3分為兩步:1.對低16位賦值。2.對高16位賦值。這時就必須要原子性操作,要麼全部成功,要麼全部不成功
複製程式碼
可見性:
多個執行緒訪問同一個變數的時候,一個執行緒修改了值,另一個執行緒能立即看到修改的值
eg.執行緒1:int i = 0; i=10, 執行緒2:j=i。
CPU1執行執行緒1,CPU2執行執行緒2
當執行緒1將i=0讀到cache中並且設定為10(執行緒修改了值),但是未寫入主存,執行緒2執行j=i時還是取出主存中i=0的值(未看到最新的值)
複製程式碼
有序性:
以下指令可能會發生指令重排序
語句1:int a=10;
語句2:int r=2;
語句3:a=a+3;
語句4:r=a*a;
考慮資料依賴性之後的指令執行順序可能是2->1->3->4
複製程式碼
多執行緒下的有序性問題:
執行緒1:(語句1)context = loadContext();
(語句2)intited=true;
執行緒2:(語句1)while(!inited){
sleep()
}
(語句2)doSomethingwithconfig(context)
因為執行緒1的兩個語句之間沒有依賴,所以可能發生不恰當的指令重排列:如下:
執行緒1先執行了語句2,導致執行緒2的intied誤認為初始化完成,執行緒2跳過語句1,進行了語句2的doSomethingwithconfig(context)工作
由此可見,指令重排列不會影響當個執行緒的執行,但會影響到程式併發的執行
複製程式碼
java記憶體模型:
在JVM規範中,試圖定義了一種Java記憶體模型,(java memeory model,JMM)來保證各個平臺下的記憶體訪問差異,以實現個平臺下的JVM都能達到一致的訪問記憶體的效果,但是JVM沒有限制CPU或者暫存器,更沒有禁止指令重排列,所以也會存在一致性問題。
Java記憶體模型規定所有的變數都是存在主存當中(類似於實體記憶體),每個執行緒都有自己的工作記憶體(類似於快取記憶體)。執行緒對變數的所有操作都必須在工作記憶體中進行,而不能直接對主存進行操作。並且每個執行緒不能訪問其他執行緒的工作記憶體。
原子性:java記憶體模型只對基本資料型別的讀取和賦值(int i = 0)保證了原子性,剩下的需要由synchronized和Lock來實現
可見性:volatile修飾的變數,一旦發生修改,就會更新主存,synchronized和Lock一樣可以保證可見性
synchronized和Lock可以保證在釋放鎖之前將會對變數的修改重新整理到主存中
案例分析:
volatile的意義:
1.保證了不同執行緒對這個變數進行操作的可見性
2.禁止指令重排序
複製程式碼
//執行緒1
boolean isStop = false;
while(!stop){
doSomething()
}
//執行緒2
stop = true;
複製程式碼
每個執行緒都有自己的工作記憶體,每個執行緒對變數的操作都要在工作記憶體中進行,不能直接對主存進行操作,執行緒之間的工作記憶體相互隔離
執行緒1將isStop讀取到自己的工作記憶體,如果執行緒2在自己的工作記憶體中對isStop進行了修改,但是執行緒1還是沒有進行重新整理,所以會一直執行下去
對執行緒1的isStop加上volatile之後就保證了可見性:
1.當執行緒2修改isStop時,會做兩件事,一就是對isStop立即更新到主存,二就是置執行緒1的快取行為無效
2.當執行緒1再次讀取isStop時,發現自己的快取行無效,就會去讀主存最新的值
volatile可以保證共享變數的可見性
複製程式碼
案例分析2:
public class ThreadVolatile {
public volatile int inc = 0;
public void increase() {
inc++;
}
public static void main(String[] args) {
final ThreadVolatile test = new ThreadVolatile();
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
test.increase();
}
}).start();
}
while (Thread.activeCount() > 1) {
//保證前面的執行緒都執行完
Thread.yield();
}
out.println("inc=" + test.inc);
}
}
複製程式碼
輸出:
inc=999452
複製程式碼
因為自增操作不是原子性的,所以雖然保證了可見性,但還是不夠,每次操作的數都會小於10000
eg.假設i=10此時,執行緒1取出i=10,還未進行(i=i+1,寫入主存)這兩個操作時就阻塞了
執行緒2取出i=10,完成了i=i+1,寫入了主存i=11
執行緒1執行i=i+1,寫入主存i=11;
volatile不可以保證原子性
使用AtomicInteger,是JDK中新增的一種利用CAS鎖原理實現的基本資料型別的原子操作
參考下面的程式碼:
複製程式碼
public class ThreadVolatile {
public AtomicInteger anInt = new AtomicInteger(0);
public void increase() {
anInt.incrementAndGet();
}
public static void main(String[] args) {
final ThreadVolatile test = new ThreadVolatile();
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
test.increase();
}
}).start();
}
while (Thread.activeCount() > 1) {
//保證前面的執行緒都執行完
Thread.yield();
}
out.println("inc=" + test.anInt.get());
}
}
複製程式碼
輸出:
inc=1000000
複製程式碼
有序性:
因為volatile不可以保證變數的原子性,但是可以保證變數的可見性,那麼可以部分保證有序性,如下:
複製程式碼
//執行緒1:
context = loadContext(); //語句1
volatile inited = true; //語句2
//執行緒2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
複製程式碼
這裡如果用volatile關鍵字對inited變數進行修飾,就不會出現這種問題了,因為當執行到語句2時,必定能保證context已經初始化完畢。
複製程式碼
使用場景:
具備條件:
1.對變數的寫操作不依賴於當前值
2.該變數沒有包含在其他變數的不變式中
也就是說,必須保證操作是原子性操作(i++就不可以),才能保證volatile關鍵字的程式在併發的時候能夠正確執行
例如: 狀態標記量:
volatile boolean flag = false;
while(!flag){
doSomething()
}
public void setFlag(){
flag = true
}
複製程式碼
//使用於double check機制
class Singleton{
private volatile static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if(instance==null) {
synchronized (Singleton.class) {
if(instance==null)
instance = new Singleton();
}
}
return instance;
}
}
複製程式碼