線程是CPU調度的最小單元,線程中的字節碼最終是放到CPU中執行的,CPU執行的時候伴隨著數據的讀寫,在Java中所有的數據都是放在主內存(RAM)中的,這一過程如下所示:
隨著CPU技術的發展,CPU的執行速度越來越快,但是內存技術並沒有太大的改變,這就導致內存中數據的讀寫速度和CPU處理數據的速度差距越來越大,CPU需要較長時間等待內存的讀寫,這就意味著CPU會出現空轉的情況。為了解決性能的瓶頸,進一步釋放CPU的運算能力,在CPU中添加了高速緩存(cache)作為數據的緩衝。
在執行任務之前,CPU會首先將數據從主內存中複製到高速緩存中,讓運算能夠快速進行,當運算完成之後,再將緩存中的結果刷回到主內存中,這樣CPU就不用等待主內存中數據的讀寫了。
目前市面上有的手機有多個CPU、一些CPU還有多核,每個CPU都可以運行一個線程,這就意味著主內存中的數據同時可能被多個線程同時讀寫,而CPU的高速緩存也是相互獨立的,這就會導致主內存中數據的不一致的問題。
另外來自於硬體的指令重排也會導致數據的不一致。CPU內部的運算單元為了儘量被充分利用,處理器會對字節碼進行指令重排。
比如下面的代碼(b的賦值不影響a的運算):
編譯之後的字節碼為:
上面的指令中可以看到,指令7(對應最後a + 1)並不影響指令2和指令3,這種情況下,CPU會對指令的順序進行調整:
從Java語言的角度,調整後的代碼順序:
上面的指令重排在多線程的情況下,由於指令的重拍,單個線程內並沒有影響,但可能影響多線程中數據的讀寫操作,從而導致一些意想不到的結果。
Java的內存模型
如果任由CPU進行優化或多線程的操作,會導致Java程序運行結果出乎意料,為了解決這種問題,JVM推出了Java內存模型來解決。
在java內存模型中,我們統一使用工作內存(working memory)來當作CPU中的寄存器或高速緩存的抽象。線程之間的共享變量存儲在主內存(main memory)中,每個線程有自己的工作內存,線程的工作內存中存儲了共享內存中變量的副本。
在JMM規範中,又一個重要的規則happens-before。
happens-before先行發生原則
happens-before用於描述兩個操作在內存中的可見性,通過保證數據的可見性,從而讓應用程式免於數據競爭的幹擾。
會發生指令重排的情況:
下面的代碼:
int a = 10; // 1b = b + 1; // 2由於操作2和操作1之間並不會相互影響,這種情況下CPU為了提高計算單元的利用率,一般會進行指令重排。
但是我們要是把代碼改成下面這種:
int a = 10; // 1b = b + a; // 2由於操作2依賴操作1的執行,這種情況下就不會發生指令重排了。
在Java內存模型(JMM)中,有以下的一些情況會自動符合happens-before規則:
1. 程序次序規則
在單線程中,一段代碼中邏輯順序靠前的字節碼一定是對後續邏輯字節碼可見的。
2. 鎖定規則
無論是單線程還是多線程環境中,一個鎖處於鎖定狀態,那麼必須首先執行unlock才做,這個所才能被其他的線程獲得並重新lock。
3. 變量可見規則
volatile關鍵字保證了變量的可見性。如果一個線程寫了volatile的變量,另外一個線程讀取這個變量,那麼這個寫操作一定是happens-before讀操作的。
4. 線程啟動規則
Thread對象的start方法先行發生於此線程的每一個動作。假設線程A在執行的過程中通過執行ThreadB.start()來啟動線程B,那麼線程A中對共享變量的修改,在線程B開始執行後對線程B可見。
5. 線程終結規則
假如線程A在執行的過程中,通過調用ThreadB.join()方法等待線程B終止,那麼線程B在終止之前對共享變量的修改在線程A等到返回之後可見。
6. 對象終結規則
一個對象的初始化完成發生在它的finalize()方法之前,也就是對象初始化的數據對它的finalize方法可見。
兩種happens-before化的方式
1. 使用volatile關鍵字
2. 使用synchronized關鍵字
通過上面兩種方式,在一個線程中調用setValue設置的value對其他的線程可見,再setValue之後,其他的線程調用getValue獲取到的value一定是1.