valitile這個關鍵詞,不侷限於java中,其實很多語言中都有這個關鍵詞。由於自己之前對於多線程的編程接觸比較少,而且對於java的內存模型不是很瞭解,所以今天做一個總結。
內存模型
計算機的主要運算是由cpu,內存之間交互的,而他們之間的交互靠的是總線。但是由於cpu的速度遠遠大於內存的,所以在cpu旁邊往往會設計一級二級緩衝,作用就是中和cpu與內存之間的速度。
但是讓我們想想,如果程序是單線程的,基本沒啥問題,因爲數據不會存放着多個緩衝中,也就不涉及一致性的問題,但是當我們的程序裏面有多線程的時候,可能不同的cpu會執行不同的線程,這樣可能不同cpu的緩衝會持有同一個變量的不同副本,這樣就有問題了。不同線程操作同一個變量,如何做到同步。
爲了解決這個問題,那篇文章裏面提到有兩種方式。
1.在訪問的時候,總線加鎖,也就是相當於人爲將多線程,變爲單線程,這樣不會出現數據不一致的問題,但是這樣帶來了很大問題,就是效率大打折扣,體現不出多線程的優勢。
2.緩存一致性協議:
這裏我就引用別人的一段話:“緩存一致性協議,最出名的就是Intel 的MESI協議,MESI協議保證了每個緩存中使用的共享變量的副本是一致的。它核心的思想是:當CPU寫數據時,如果發現操作的變
量是共享變量,即在其他CPU中也存在該變量的副本,會發出信號通知其他CPU將該變量的緩存行置爲無效狀態,因此當其他CPU需要讀取這個變量時,發現自己緩存中緩存該變量的
緩存行是無效的,那麼它就會從內存重新讀取。”
最後附上一張圖,來總結下上面說的。
多線程編程的概念
1.原子性:
這個概念在數據庫裏面也有,意思就是保證一個操作,要麼完成,要不不做,而不能是做了一半。想必大家對這個最熟悉的應該就是銀行的例子了吧。轉賬的時候,從我賬戶
扣款和給對方賬戶加款,應該是原子操作,不能說從我賬戶扣了,但是對方賬戶沒有增加。這是在多線程裏面必須要避免的問題。
2.可見性:
這個詞的意思就是當一個線程在修改了某一個變量之後,可以馬上將改變刷新到別的線程,也就是說如果別的線程需要訪問的時候,是訪問的修改過後的值。
3.有序性:
程序執行的順序,不一定會按照代碼書寫的順序進行執行。而是編譯器會對代碼進行指令的優化,這樣做的目的是爲了保證程序執行的效率。這樣做基本上對於單線程沒啥問
題,編譯器保證做過優化後的代碼和沒做優化的代碼,執行的結果是一樣的。但是如果是多線程呢,這點編譯器就無法保證。
綜上所述,一個多線程如果要正確的執行,就必須滿足上面三個條件。如果不滿足,則執行的結果就有可能出錯。
那麼java裏面通過什麼樣的方式來確保符合三種原則呢?
java保證多線程(java內存模型)
1.原子性:
在java中,對基本數據類型的讀取與賦值是原子操作的,要不操作成功,要麼失敗。
那麼怎麼樣的操作算原子操作呢,下面舉兩個例子:
int x = 3;
int y = x;
y++;
第一條語句,是直接將值複製給x變量。第二條語句,首先讀取x的值,然後再賦值給y。第三條語句,首先讀取y的值,然後加一操作後,再賦值給y。
所以上面三條語句只有第一條語句是原子操作。這也就解釋了下面這段代碼結果爲啥不是人們的預期。
public volatile static int count = 0;
public static void main(String[] args) {
for(int i=0;i<10000;i++)
{
new Thread(new Runnable() {
@Override
public void run() {
count++;
}//加入Java開發交流君樣:756584822一起吹水聊天
}).start();
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(count);
}
這段代碼執行的結果有時候會小於10000,原因就是count++不是原子操作,雖然用volatile修飾,但是也不起作用。
//加入Java開發交流君樣:756584822一起吹水聊天
那麼上面這段代碼如何正確運行呢,答案很顯然,把count++變成原子操作即可,那麼修改count的類型,如下
public static AtomicInteger count =new AtomicInteger(0);
將count++變爲count.getAndIncrement();
這樣保證了原子操作。結果也就是10000了。
2.可見性:
對於可見性,java提供了volatile關鍵詞來修飾,上面已經用到過了。這個關鍵詞保證他修改後的值,會馬上更新到java的主存中,當其他線程再要讀取的時候,就是讀取的新的值,但是用這個關鍵詞的時候,也得多多注意,就跟上面說的那種情況,也是不行的。當然上面的情況可以用加鎖,或者 synchronized方式進行同步。保證結果。
3.有序性:
Java裏面也是通過volatile來保證一定程度上的有序性。也可以通過 synchonized來保證多線程下的有序性。
在《深入理解Java虛擬機》有這麼一段話“
觀察加入volatile關鍵字和沒有加入volatile關鍵字時所生成的彙編代碼發現,加入volatile關鍵字時,會多出一個lock前綴指令”
lock前綴指令實際上相當於一個內存屏障(也成內存柵欄),內存屏障會提供3個功能:
- 1)它確保指令重排序時不會把其後面的指令排到內存屏障之前的位置,也不會把前面的指令排到內存屏障的後面;即在執行到內存屏障這句指令時,在它前面的操作已經全部完成;
- 2)它會強制將對緩存的修改操作立即寫入主存;
- 3)如果是寫操作,它會導致其他CPU中對應的緩存行無效。”
所以以後遇到問題的時候,還是得多從原理裏面找答案。
雖然volatile的性能比synchronized性能高,但是volatile的使用場景有所限制。因爲它無法保證多線程下的原子性。