java進階之內存模型介紹

Java進階之內存模型介紹

前言

不管在什麼編程語言裡面,讀取和寫入都是我們程序最普遍的操作,在單線程的程序裡面我們可能不關注線程的讀寫問題,但是一旦到多線程的環境下,讀和寫就會變得非常敏感。Java內存模型實際上是定義了在多線程環境下使用讀和寫操作結果一致性的問題。這個模型在JDK5中通過JSR-133議案進行了修訂。

為什麼需要Java內存模型

主要的原因還是在於方便程序員更加關注業務本身還不是底層細節,對程序員來說理解操作系統的內存架構,CPU指令優化,JIT編譯器優化是比較困難的一件事。

變量的可見性問題

在多核的服務器時代CPU一般都會擁有多級cache,為了提升其處理性能,比如在上篇文章提到過的L1,L2,L3級cache。這種服務器架構的問題主要在於程序裡面的共享變量在橫跨多個線程時候的可見性問題。對應到我們寫的程序裡面就是一個線程寫完的變量數據,對於其他線程是否可見。在上面文章中提到過每個線程共享進程的主內存,,同時擁有自己的線程local cache,涉及到變量的讀寫和可見性問題,其實就是線程的local cache與主內存的數據是否一致的問題。在一個多線程累加同一個變量的程序裡面,如果一個線程更新了自己local cache的數據,那麼必須在更新完把local cache的數據flush到主內存,否則其他線程讀取到的數據就有可能是錯誤的,另一方面其他線程知道主內存的數據可能會更新,那麼就必須放棄自己local cache的數據,直接從主內存加載最新版本的數據用來累加,否則就會出現更新結果不正確的情況。(這部分知識現在理解不了,沒關係,後面的文章會慢慢梳理。)

關於代碼指令的重排序問題

為了方便大家理解重排序的概念,我先舉個簡單的例子:

public static void main(String []args){int a=3;int c=4;int d=a+c;System.out.printLn(d);}

上面的代碼我們看到a變量是先聲明的,c變量是後聲明的,但在底層編譯,或者JIT優化執行的時候,有可能c變量先被解析,然後才是a變量,這就叫指令重排序,目的是為了提高執行效率,當然指令重排序是有約定的,不管執行順序如何變動(底層優化導致),在單線程中,它的最終結果必須是和代碼順序執行的結果是一致的。如上面的程序,a和c的位置可以互換,但是和d的順序是不能變的,這就是它的約定,這個在後面的文章會解釋。

那麼什麼是指令重排序,通俗點講就是:

你看到的代碼順序,不一定是它的執行順序。

上面說了,重排序只保證在單線程程序中,不影響最終結果的前提下允許JIT或者硬件指令做一些優化,但是在多線程程序中重排序是可能會導致一些問題的。

Java內存模型

重排序和變量可見性問題是多線程編程裡面的主要問題,Java內存模型主要描述了下面兩種情況的的處理:

(1)重排序是底層編譯器優化的結果,所以在Java內存模型裡面有一些 happens-before 規則來約束重排序,比如說如果前後兩個變量有依賴關係如上面例子中的a和d那麼它是不能被重排序的,否則一旦重排序,是會導致程序邏輯錯誤。

(2)對於共享數據的寫操作,是沒法通過happens-before關係來約束的,如上面說到的累加的例子,此時需要通過Java裡面鎖的機制來避免。

如下圖:

java進階之內存模型介紹

關於同步代碼塊

同步代碼塊主要完成了兩件事情:

(1)對於共享代碼在任何時候只保證有一個線程可以操作,這保證了原子性。

(2)lock和unlock操作會觸發當前線程flush自己的cache的數據到主內存中,這保證了可見性的問題。

關於volatile關鍵字

在Java裡面用volatie關鍵字修飾共享變量僅僅只保證可見性,僅僅適用於任何時候只有一個線程更新,多個線程讀取的業務。所以如果有超過一個線程以上對變量進行修改,那麼必須使用鎖機制來處理。

所以請大家記住volatile只保證了可見性,不保證原子性。

關於final關鍵字

在Java裡面final關鍵字修飾的變量,僅僅會被初始化一次,後面是不能修改的。

JIT編譯器對final的變量會進行優化,如基本類型String,Int,因為這裡不存在修改的問題,那也就沒有可見性的問題,所以final修飾基本類型變量在多線程的cache裡面的是安全的,不需要和主內存有關聯,也就不會有flush或者invalidate的情況。

這也是我們經常說Java裡面的String為什麼是安全的原因,注意使用final修飾的集合框架如List,Set,Map,雖然內存地址不能變,但是裡面的內容是可以變的,這裡也是不安全的,這一點需要注意。

這也是為什麼有一些函數類型的編程語言如Scala裡面嚴格的提供了不可變的集合框架和可變的集合框架,其目的就是為了更加有利於多線程編程。

最後記住final關鍵字和volatile關鍵字是不能修飾同一變量的,在IDE的編譯器裡面是直接會報錯的。

總結

如果在閱讀之前不瞭解進程和線程在操作系統裡面的關係與特點,我建議你先看看前面的兩篇文章再閱讀本文。本篇文章主要介紹了Java的內存模型相關內容,如果掌握和熟悉了這些知識,那麼對於理解和開發併發編程將是非常有幫助的。


分享到:


相關文章: