Java進階以內存模型介紹

Java進階以內存模型介紹

前言

無論在什麼編程語言裏面,讀取和寫入都是咱們程序最廣泛的操做,在單線程的程序裏面咱們可能不關注線程的讀寫問題,可是一旦到多線程的環境下,讀和寫就會變得很是敏感。Java內存模型其實是定義了在多線程環境下使用讀和寫操做結果一致性的問題。這個模型在JDK5中經過JSR-133議案進行了修訂。java

爲何須要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內存模型主要描述了下面兩種狀況的的處理:app

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

(2)對於共享數據的寫操做,是無法經過happens-before關係來約束的,如上面說到的累加的例子,此時須要經過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的內存模型相關內容,若是掌握和熟悉了這些知識,那麼對於理解和開發併發編程將是很是有幫助的。

相關文章
相關標籤/搜索