JMM內存模型

問題一:為什麼要學習JMM內存模型

在我們學習Java的第一天就瞭解到Java是一門通過JVM虛擬機跨平臺的語言,同時Java也是一門具有多個線程同時處理能力的編程語言,那麼在多個線程併發的情況,我們的Java底層是如何對這些線程進行處理,且又如何保證線程的安全性,這就是學習JMM內存模型的原因以及目的

併發編程分類

併發編程解決的問題是多個線程在怎麼樣交互數據,簡單的說,就是多個線程在處理同一個變量時,如何進行信息的溝通,當下流行的一共有兩種通信的機制

1.共享內存

在這種內存模型下,會產生工作內存(將共享數據加載到工作內存中來進行操作,保證數據的高效性)和主內存(存放共享數據),工作內存和主內存之間通過read 和 write 來進行數據的交流

2.消息傳遞

這種模型是沒有共享數據,線程之間必須通過明確的發送消息來顯式進行通信。

內存模型的工作方式

Java採用的是共享內存模型

正如上文所介紹的那樣,Java會將共享的數據放置到主內存中,當多個線程操作這個共享變量時,會將對應的內容加載到自己的工作內存中來,也就是說,有多少個線程就有多少個工作內存

假設現在有內存A和B有主內存中共享變量x的副本。初始時,這三個內存中的x值都為0。線程A在執行時,把更新後的x值(假設值為1)臨時存放在自己的本地內存A中。當線程A和線程B需要通信時,線程A首先會把自己本地內存中修改後的x值刷新到主內存中,此時主內存中的x值變為了1。隨後,線程B到主內存中去讀取線程A更新後的x值,此時線程B的本地內存的x值也變為了1。

從整體來看,這兩個步驟實質上是線程A在向線程B發送消息,而且這個通信過程必須要經過主內存。JMM通過控制主內存與每個線程的本地內存之間的交互,來為java程序員提供內存可見性保證。內存模型中值得注意的三個點

1. 內存可見性

內存可見性的問題指的就是在工作方式中介紹到當A修改了工作內存中的數據,此時B去操作自己工作內存中的數據時, A需要將數據寫回到主內存中,B再次從主內存中讀取數據,但這有可能情況並不是那麼美好,若B沒有從主內存中讀取數據,那麼此時B中的數據和A中的數據此刻將變得沒有意義,所以我們將這重在修改了各自工作內存後,沒有重新從主內存中讀取數據的行為,稱之為內存可見性問題,A和B彼此之間數據不可見

public class VolatileTest {
public static void main(String[] args) {
MyThread mt = new MyThread();
new Thread(mt).start();
while(true){
if(mt.isFlag()){
System.out.println("------");
break;
}
}
}
}
class MyThread implements Runnable{
private volatile boolean flag = false;
public boolean isFlag() {

return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
@Override
public void run() {

try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
System.out.println("flag = "+isFlag());
}
}

在本案例中,我們希望看到的執行結果是,程序會在200毫秒後打印----,同時程序結束,但真正的情況是本程序永遠不會結束,原因是while操作是屬於底層代碼操作,執行效率非常之快,會導致main線程都來不及從主內存中抓取被MyThread修改後的flag,導致main線程一直在讀取自己工作內存中的數據,而自己工作內存中的數據一直是false,故而程序不會結束

解決內存可見性問題的手段有很多synchronized、Lock、final、volitile都能夠解決內存可見性問題

2.重排序問題

重排序問題,指的是一個線程在不影響編譯結果的情況下,會按照JVM喜愛的方式去對程序進行順序上的調整

用一段偽代碼來展示

main(){
int i =10; //1
int j =20; //2
int flag = true; //3
int temp = i * j ;//4
}

在這段程序中,我們按照代碼的順序,編號1,2,3,4 我們能夠看到只要4在1,2 之後,那麼這個程序無論是哪種編譯情況都不會影響最終程序的執行結果 ,如: 2,1,4,3 1,3,2,4 ,在單線程情況,這種重排序的方案並沒有任何問題,但如果出現了多線程程序

class MyExample(){
private boolean flag = false;
int i =5;
int j =5;

read(){
i =10; //1
j = 20; //2
flag = true; //3
}
write(){
while(flag){
int temp = i * j;
}

}
}

我們會發現如果read是一條線程,write是一條線程,那麼在單獨考慮read線程的情況下, read線程無論是按照1,2,3排序,還是3,2,1或者是任意方式排序,都不會對程序產生任何影響,但此時若考慮到write線程,若read線程時1,2,3 那麼write 就是 200,而如果按照3,2,1變成,那麼write線程的結果就是25,所以JMM的重排序是一個我們需要考慮的地方,解決的方案是 synchronized、Lock 、volitile 除此之外 Java 內存模型通過 happens-before 原則如果能推導出來兩個操作的執行順序就能先天保證有序性,否則無法保證, happens-before中定義了8種情況,在滿足這8種情況,JVM不會對代碼進行重排序

具體規則請自行查詢

3.原子性問題

所謂的原子性操作的含義是指該操作是不可分割的,比如我們通常寫的 i = 0; int i = i++;
在底層是由三步組成
int temp = i;
temp = temp + 1;
int i = temp;

使用volitile關鍵字 是無法保證volitile關鍵字的,我們可以使用synchronized關鍵字,以及atomicInteger來解決這樣的問題,而atomicInteger底層採用的是cas算法,不屬於我們本次的討論範圍

總結

相信同學們在學習完本章內容之後一定對JMM內存模型有了一定的瞭解,學習是沒有止境的,光去看表面是不能解決問題的,在學習到一定階段之後,這些內容都需要同學們去思考,謝謝大家。

JMM內存模型


分享到:


相關文章: