Java内存模型JMM
簡介
計算機存儲結構,從本地磁盤到主存到CPU緩存,也就是從硬盤到內存,到CPU。
一般對于的程序的操作就是從數據庫查數據到內存然后到CPU進行計算
因為有這么多級的緩存(cpu和物理主內存的速度是不一致的)
CPU的運行并不是直接操作內存而是先把內存里邊的數據讀到緩存,而內存的讀和寫操作的時候就會造成不一致的問題
JVM規范中試圖定義一種Java內存模型(Java Memory Model,簡稱 JMM)來屏蔽掉各種硬件和操作系統的內存訪問差異,以實現讓Java程序在各種平臺下都能達到一致的內存訪問效果。
Java Memory Model
JMM本身是一種抽象的概念并不真實存在,它僅僅描述的是一組約定或規范,通過這組規范定義了程序中(尤其是多線程)各個變量的讀寫訪問方式并決定一個線程對共享變量的寫入何時以及如何變成對另一個線程可見,關鍵技術點都是圍繞線程的原子性、可見性和有序性展開的。
原則:
JMM的關鍵技術點都是圍繞多線程的原子性、可見性和有序性展開的
作用:
三大特性
可見性
是指當一個線程修改了某一個共享變量的值,其他線程是否能夠立即知道該變更,JMM規定了所有的變量都存儲在主內存中。
系統主內存 共享變量數據修改被寫入的時機是不確定的, 多線程并發下很可能出現“臟讀”,所以每個線程都有自己的工作內存 ,線程自己的工作內存中保存了該線程使用到的變量的主內存副本拷貝 ,線程對變量的所有操作(讀取、賦值等)都必須在線程自己的工作內存中進行,而不能夠直接讀寫主內存中的變量, 不同線程之間也無法直接訪問對方工作內存中的變量,線程間變量值的傳遞均需要通過主內存來完成
假設有個user類,new user對象(對象的屬性存在主內存 共享變量中),同時兩個線程去操作這個對象,線程需要先讀取貢獻變量到本地內存,再寫入(修改)變量值,再放回主內存。線程只能操作自己的,不可以直接操作主內存
線程臟讀
原子性
指同一個操作是不可打斷的,即多線程環境下,操作不能被其他線程干擾
有序性
有序性不是平常寫的main方法,從方法入口一行一行往下執行。尤其在多線程高并發的環境,有序性就復雜了
對于一個線程的執行代碼而言,我們總是習慣性認為代碼的執行總是從上到下,有序執行。但是為了性能,編譯器和處理器通常會對指令序列進行重新排序。Java規范規定JVM線程內部維持順序話語義,即只要程序的最終結果與它順序化執行的結果相等,那么指令的執行順序可以與代碼順序不一致,此過程叫執行的重排序。
優缺點
JVM能根據處理器特性(CPU多級緩存系統、多核處理器等)適當的對機器指令進行重排序,使機器指令能夠更符合CPU的執行特性,最大限度的發揮機器性能但是,
指令重排 可以保證串行語義一致,但沒有義務保證 多線程的語義也一致(即可能產生“臟讀”),簡單說就是
兩行以上不相干的代碼在執行的時候有可能先執行的不是第一條,不見得是從上到下順序執行,執行順序 會被優化
從源碼到最終執行示例圖
單線程環境里面確保程序最終執行結果和代碼順序執行的結果一致。
處理器在進行重排序時必須要考慮 指令之間的數據依賴性
多線程環境中線程交替執行,由于編譯器優化重排的存在,兩個線程中使用的變量能否保證一致性是無法確定的,結果無法預測。
在某些特殊場景,我們需要禁止指令重排而保證程序執行的有序性
多線程對變量的讀寫過程
由于JVM運行程序的實體是線程,而每個線程創建時JVM都會為其創建一個工作內存(也稱為棧空間),工作內存是每個線程的私有數據區域,而Java內存模型中規定所有的變量都存儲在主內存,主內存是共享內存區域,所有線程都可以訪問,但線程對變量的操作(讀取賦值等)必須在工作內存中進行,首先要將變量從主內存拷貝到線程自己的工作內存空間,然后對變量進行操作,操作完成后再將變量寫回主內存,不能直接操作主內存中的變量,各個線程中的工作內存中存儲著主內存中的變量副本拷貝,因此不同的線程間無法訪問對方的工作內存,線程間的通信(傳值)必須通過主內存來完成
JMM定義了線程和主內存之間的抽象關系
總結
我們定義的所有共享變量都存儲在物理主內存中
每個線程都有自己獨立的工作內存,里面保存該線程使用到的變量的副本(主內存中該變量的一份拷貝)
線程對共享變量所有的操作都必須先在線程自己的工作內存中進行后寫回主內存,不能直接從主內存中讀寫(不能越級)
不同線程之間也無法直接訪問其他線程的工作內存中的變量,線程間變量值的傳遞需要通過主內存來進行(同級不能相互訪問)
多線程先行發生原則之happens-before
在JVM中,如果一個操作執行的結果需要對另一個操作可見性,或者代碼重排序,那么這兩個操作之間必須存在happens-before(先行發生)原則。
邏輯上的先后關系
| y=a | 線程B執行 |
| 上述稱之為:寫后讀 |
y是否等于5?
如果線程A的操作(x=5)happens-before(先行發生)線程B的操作(y=x),那么可以確定線程B執行后y=5一定成立
如果他們不存在happens-before原則,那么y=5不一定成立。
這就是happens-before原則 -----》包含可見性和有序性的約束
如果Java內存模型中所有的有序性都僅依靠volatile和synchronized來完成,那么有很多操作都將會變的非常啰嗦,但是我們在編寫Java并發代碼的時候并沒有察覺到這一點。
我們沒有 時時、處處、此次,添加volatile和synchronized來完成程序,這是因為Java語言中JMM原則下有一個“先行發生”(Happens-Before)的原則限制和規矩
這個原則非常重要
它是判斷數據是否存在競爭,線程是否安全的非常有用的手段。依賴這個原則,我們可以通過幾條簡單規則一下子解決并發環境下兩個操作之間是否可能存在沖突的所有問題 ,而不需要陷入Java內存模型苦澀難懂的底層編譯原理之中。
happens-before原則
如果一個操作happens-before另一個操作,那么第一個操作的執行結果將對第二個操作可見,而且第一個操作的執行順序排在第二個操作之前。
兩個操作之間存在happens-before關系,并不意味著一定要按照happens-before原則制定的順序來執行。如果重排序之后的執行結果與按照happens-before關系來執行的結果一致,那么這種重排序 并不非法
例如: 1+2+3 = 3+2+1,最終結果是一致的
happens-before之8條
次序規則
一個線程內,按照代碼順序,寫在前面的操作先行發生于寫在后面的操作
前一個操作的結果可以被后續的操作獲取,例如:前面一個操作把變量 x 賦值為1,那后面的一個操作肯定能知道 x 已經變成了1
鎖定規則
一個unLock操作 先行發生于后面(這里“后面”是指時間上的先后)對同一個鎖的 lock操作
volatile變量原則
對一個volatile變量的寫操作先行發生于后面對這個變量的讀操作,前面的寫對后面的讀是可見的,這里的“后面“同樣是指時間上的先后。
傳遞規則
如果操作A先行發生于操作B,而操作B又先行發生于操作C,則可以得出操作A先行發生于操作C
線程啟動規則(Thread Start Rule)
Thread對象的start()方法先行發生于此線程的每一個動作
線程中斷規則(Thread Interruption Rule)
對線程interrupt()方法的調用先行發生于被中斷線程的代碼檢測到中斷事件的發生;
可以通過Thread.interrupted()檢測到是否發生中斷
解釋: 也就是說要先調用interrupt()方法設置過中斷標志位,才能檢測到中斷發送
線程終止規則(Thread Termination Rule)
線程中的所有操作都先行發生于對此線程的終止檢測,可以通過isAlive()等手段檢測線程是否已經終止執行。
對象終結規則(Finalizer Rule)
一個對象的初始化完成(構造函數執行結束)先行發生于它的finalize()方法的開始
finalize(): JVM中一個對象要被回收的時候,最終要執行的一個方法
對象沒有完成初始化之前,是不能調用finalized()方法的
也就是說:要先new一個對象(先生 ),才能被當作垃圾清理回收( 后死 )
在Java語言里面,Happens-Before的語義本質上是一種可見性
A Happens-Before B 意味著A發生過的事情對B來說是可見的,無論A事件和B時間是否發生在同一個線程里
JMM的設計分為兩個部分:
案例
private int value = 0;public int getValue(){return value; } public int setValue(){return ++value; }假設存在線程A和B,線程A先(時間上的先后)調用了setValue(), 然后線程B調用了同一個對象的getValue(),那么線程B收到的返回值是什么?注意:是兩個不同的線程
分析:
無法通過happens-before原則推到出 線程A happens-befores 線程B,雖然可以確認 在時間上線程A優先于線程B指定,但就是無法確認線程B獲得的結果是什么 ,所以這段代碼不是線程安全的。
解決方案:
①
private int value = 0;public synchronized int getValue(){return value; } public synchronized int setValue(){return ++value; }②把value定義為volatile變量,由于setter方法對value的修改不依賴value的原值(每次都會++),滿足volatile關鍵字的使用場景
/*** 使用:把value定義為volatile變量,由于setter方法對value的修改不依賴value的原值,滿足volatile關鍵字的使用場景* 理由:利用volatile保證讀取操作的可見性;利用synchronized保證復合操作的原子性結合使用鎖和volatile變量來減少同步的開銷*/ private volatile int value = 0;public int getValue(){return value; //利用volatile保證讀取操作的可見性 } public synchronized int setValue(){return ++value; //利用synchronized保證復合操作的原子性 }總結
以上是生活随笔為你收集整理的Java内存模型JMM的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 不用安装Oracle Client如何使
- 下一篇: 简述 Java 垃圾回收机制