"

背景

學習Java併發編程,JMM是繞不過的檻。在Java規範裡面指出了JMM是一個比較開拓性的嘗試,是一種試圖定義一個一致的、跨平臺的內存模型。JMM的最初目的,就是為了能夠支多線程程序設計的,每個線程可以是和其他線程在不同的CPU核心上運行,或者對於多處理器的機器而言,該模型需要實現的就是使得每一個線程就像運行在不同的機器、不同的CPU或者本身就不同的線程上一樣,這種情況實際上在項目開發中是常見的。簡單來說,就是為了屏蔽系統和硬件的差異,讓一套代碼在不同平臺下能到達相同的訪問結果。(當然你要是想做高性能運算,這個還是要和硬件直接打交道的,博主之前搞高性能計算,用的一般都是C/C++,更老的語言還有Fortran,不過現在並行計算也是有很多計算框架和協議的,如MPI協議、基於CPU計算的OpenMp,GPU計算的Cuda、OpenAcc等)當然了,JMM在設計之初也是有不少缺陷的,不過後續也逐漸完善起來,還有一個算不上缺陷的缺陷,就是有點難懂。

什麼是JMM

JMM即為JAVA 內存模型(java memory model)。Java內存模型的主要目標是定義程序中各個變量的訪問規則,即在JVM中將變量存儲到內存和從內存中取出變量這樣的底層細節的實現規則。 它其實就是JVM內部的內存數據的訪問規則,線程進行共享數據讀寫的一種規則,在JVM內部,多線程就是根據這個規則讀寫數據的。 注意 ,此處的變量與Java編程裡面的變量有所不同步,它只是包含了實例字段、靜態字段和構成數組對象的元素,但不包含 局部變量方法參數(局部變量和方法參數線程私有的,不會共享,當然不存在數據競爭問題)(如果局部變量是一個reference引用類型,它引用的對象在Java堆中可被各個線程共享,但是reference引用本身在Java棧的局部變量表中,是線程私有的)。為了獲得較高的執行效能,Java內存模型並沒有限制執行引起使用處理器的特定寄存器或者緩存來和主內存進行交互,也沒有限制即時編譯器進行調整代碼執行順序這類優化措施。

JMM和JVM有什麼區別

  • JVM: Java虛擬機模型 主要描述的是Java虛擬機內部的結構以及各個結構之間的關係,Java虛擬機在執行Java程序的過程中,會把它管理的內存劃分為幾個不同的數據區域,這些區域都有各自的用途、創建時間、銷燬時間。
  • JMM:Java內存模型 主要規定了一些內存和線程之間的關係,簡單的說就是描述java虛擬機如何與計算機內存(RAM)一起工作。

JMM中的主內存、工作內存與jJVM中的Java堆、棧、方法區等並不是同一個層次的內存劃分,

JMM核心知識點

Java線程之間的通信由Java內存模型(JMM)控制,JMM決定一個線程對共享變量的寫入何時對另一個線程可見。從抽象的角度來看,JMM定義了 線程和主內存之間的抽象關係 :JMM規定了所有的變量都存儲在主內存(Main Memory)中。每個線程還有自己的工作內存(Working Memory),線程的工作內存中保存了該線程使用到的變量的主內存的副本拷貝,線程對變量的所有操作(讀取、賦值等)都必須在工作內存中進行,而不能直接讀寫主內存中的變量(volatile變量仍然有工作內存的拷貝,但是由於它特殊的操作順序性規定,所以看起來如同直接在主內存中讀寫訪問一般)。不同的線程之間也無法直接訪問對方工作內存中的變量,線程之間值的傳遞都需要通過主內存來完成。

"

背景

學習Java併發編程,JMM是繞不過的檻。在Java規範裡面指出了JMM是一個比較開拓性的嘗試,是一種試圖定義一個一致的、跨平臺的內存模型。JMM的最初目的,就是為了能夠支多線程程序設計的,每個線程可以是和其他線程在不同的CPU核心上運行,或者對於多處理器的機器而言,該模型需要實現的就是使得每一個線程就像運行在不同的機器、不同的CPU或者本身就不同的線程上一樣,這種情況實際上在項目開發中是常見的。簡單來說,就是為了屏蔽系統和硬件的差異,讓一套代碼在不同平臺下能到達相同的訪問結果。(當然你要是想做高性能運算,這個還是要和硬件直接打交道的,博主之前搞高性能計算,用的一般都是C/C++,更老的語言還有Fortran,不過現在並行計算也是有很多計算框架和協議的,如MPI協議、基於CPU計算的OpenMp,GPU計算的Cuda、OpenAcc等)當然了,JMM在設計之初也是有不少缺陷的,不過後續也逐漸完善起來,還有一個算不上缺陷的缺陷,就是有點難懂。

什麼是JMM

JMM即為JAVA 內存模型(java memory model)。Java內存模型的主要目標是定義程序中各個變量的訪問規則,即在JVM中將變量存儲到內存和從內存中取出變量這樣的底層細節的實現規則。 它其實就是JVM內部的內存數據的訪問規則,線程進行共享數據讀寫的一種規則,在JVM內部,多線程就是根據這個規則讀寫數據的。 注意 ,此處的變量與Java編程裡面的變量有所不同步,它只是包含了實例字段、靜態字段和構成數組對象的元素,但不包含 局部變量方法參數(局部變量和方法參數線程私有的,不會共享,當然不存在數據競爭問題)(如果局部變量是一個reference引用類型,它引用的對象在Java堆中可被各個線程共享,但是reference引用本身在Java棧的局部變量表中,是線程私有的)。為了獲得較高的執行效能,Java內存模型並沒有限制執行引起使用處理器的特定寄存器或者緩存來和主內存進行交互,也沒有限制即時編譯器進行調整代碼執行順序這類優化措施。

JMM和JVM有什麼區別

  • JVM: Java虛擬機模型 主要描述的是Java虛擬機內部的結構以及各個結構之間的關係,Java虛擬機在執行Java程序的過程中,會把它管理的內存劃分為幾個不同的數據區域,這些區域都有各自的用途、創建時間、銷燬時間。
  • JMM:Java內存模型 主要規定了一些內存和線程之間的關係,簡單的說就是描述java虛擬機如何與計算機內存(RAM)一起工作。

JMM中的主內存、工作內存與jJVM中的Java堆、棧、方法區等並不是同一個層次的內存劃分,

JMM核心知識點

Java線程之間的通信由Java內存模型(JMM)控制,JMM決定一個線程對共享變量的寫入何時對另一個線程可見。從抽象的角度來看,JMM定義了 線程和主內存之間的抽象關係 :JMM規定了所有的變量都存儲在主內存(Main Memory)中。每個線程還有自己的工作內存(Working Memory),線程的工作內存中保存了該線程使用到的變量的主內存的副本拷貝,線程對變量的所有操作(讀取、賦值等)都必須在工作內存中進行,而不能直接讀寫主內存中的變量(volatile變量仍然有工作內存的拷貝,但是由於它特殊的操作順序性規定,所以看起來如同直接在主內存中讀寫訪問一般)。不同的線程之間也無法直接訪問對方工作內存中的變量,線程之間值的傳遞都需要通過主內存來完成。

Java併發之內存模型(JMM)淺析

圖:JMM內存模型

這上如可以看見java線程中工作內存是通過cache來和主內存交互的,這是因為計算機的存儲設備與處理器的運算能力之間有幾個數量級的差距,所以現代計算機系統都不得不加入一層或多層讀寫速度儘可能接近處理器運算速度的高速緩存( cache )來作為內存與處理器之間的緩衝:將運算需要使用到的數據複製到緩存中,讓運算能快速進行,當運算結束後再從緩存同步回內存之中沒這樣處理器就無需等待緩慢的內存讀寫了。

線程和線程之間想進行數據的交換一般大致要經歷兩大步驟:1.線程1把工作內存1中的更新過的共享變量刷新到主內存中去;2.線程2到主內存中去讀取線程1刷新過的共享變量,然後copy一份到工作內存2中去。(當然具體實現沒有這麼簡單,具體的操作步驟在下文細講)

三大特徵

Java內存模型是圍繞著併發編程中原子性、可見性、有序性這三個特徵來建立的,那我們依次看一下這三個特徵

1. 原子性

  • 定義: 一個或者多個操作不能被打斷,要麼全部執行完畢,要麼不執行。在這點上有點類似於事務操作,要麼全部執行成功,要麼回退到執行該操作之前的狀態。
  • 注意點: 一般來說在java中基本類型數據的訪問大都是原子操作,但是對於64位的變量如long 和double類型,在32位JVM中,分別處理高低32位,兩個步驟就打破了原子性,這就導致了long、double類型的變量在32位虛擬機中是非原子操作,數據有可能會被破壞,也就意味著多個線程在併發訪問的時候是線程非安全的。所以現在官方建議最好還是使用64JVM,64JVM在安全上和性能上都有所提升。
  • 總結: 對於別的線程而言,他要麼看到的是該線程還沒有執行的情況,要麼就是看到了線程執行後的情況,不會出現執行一半的場景,簡言之,其他線程永遠不會看到中間結果。
  • 解決方案
  • 鎖機制 鎖具有排他性,也就是說它能夠保證一個共享變量在任意一個時刻僅僅被一個線程訪問,這就消除了競爭;
  • CAS( compare-and-swap)

2.可見性

定義:可見性是指當多個線程訪問同一個變量時,當一個線程修改了這個變量的值,其他線程能夠立即獲得修改的值。

實現原理:JMM是通過將在工作內存中的變量修改後的值同步到主內存,在讀取變量前需要從主內存獲取最新值到工作內存中,這種只從主內存的獲取值的方式來實現可見性的 。

存在問題:多線程程序在可見性方面存在問題,這意味著某些線程可能會讀到舊數據,即髒讀。

解決方案

  • volatile 變量:volatile的特殊規則保證了volatile變量值修改後的新值會 立刻 同步到主內存,所以每次獲取的volatile變量都是主內存中最新的值,因此volatile保證了多線程之間的操作變量的可見性
  • synchronized 關鍵字,在同步方法/同步塊開始時(Monitor Enter),使用共享變量時會從主內存中刷新變量值到工作內存中(即從主內存中讀取最新值到線程私有的工作內存中),在同步方法/同步塊結束時(Monitor Exit),會將工作內存中的變量值同步到主內存中去(即將線程私有的工作內存中的值寫入到主內存進行同步)。
  • Lock 接口的最常用的實現ReentrantLock(重入鎖)來實現可見性:當我們在方法的開始位置執行lock.lock()方法,這和synchronized開始位置(Monitor Enter)有相同的語義,即使用共享變量時會從主內存中刷新變量值到工作內存中(即從主內存中讀取最新值到線程私有的工作內存中),在方法的最後finally塊裡執行lock.unlock()方法,和synchronized結束位置(Monitor Exit)有相同的語義,即會將工作內存中的變量值同步到主內存中去(即將線程私有的工作內存中的值寫入到主內存進行同步)。
  • final 關鍵字的可見性是指:被final修飾的變量,在構造函數數一旦初始化完成,並且在構造函數中並沒有把“this”的引用傳遞出去(“this”引用逃逸是很危險的,其他的線程很可能通過該引用訪問到只“初始化一半”的對象),那麼其他線程就可以看到final變量的值。

3.有序性

定義: 即程序執行的順序按照代碼的先後順序執行。這個在單一線程中自然可以保證,但是多線程中就不一定可以保證。

問題原因: 首先處理器為了提高程序運行效率,可能會對目標代碼進行重排序。重排序是對內存訪問操作的一種優化,它可以在不影響 單線程 程序正確性的前提下進行一定的調整,進而提高程序的性能。其保證依據是處理器對涉及依賴關係的數據指令不會進行重排序,沒有依賴關係的則可能進行重排序,即一個指令Instruction 2必須用到Instruction 1的結果,那麼處理器會保證Instruction 1會在Instruction 2之前執行。(PS:並行計算優化中最基本的一項就是去除數據的依賴關係,方法有很多。)但是在多線程中可能會對存在依賴的操作進行重排序,這可能會改變程序的執行結果。

Java有兩種編譯器,一種是Javac靜態編譯器,將源文件編譯為字節碼,代碼編譯階段運行;另一種是動態編譯JIT,會在運行時,動態的將字節碼編譯為本地機器碼(目標代碼),提高java程序運行速度。通常javac不會進行重排序,而JIT則很可能進行重排序

"

背景

學習Java併發編程,JMM是繞不過的檻。在Java規範裡面指出了JMM是一個比較開拓性的嘗試,是一種試圖定義一個一致的、跨平臺的內存模型。JMM的最初目的,就是為了能夠支多線程程序設計的,每個線程可以是和其他線程在不同的CPU核心上運行,或者對於多處理器的機器而言,該模型需要實現的就是使得每一個線程就像運行在不同的機器、不同的CPU或者本身就不同的線程上一樣,這種情況實際上在項目開發中是常見的。簡單來說,就是為了屏蔽系統和硬件的差異,讓一套代碼在不同平臺下能到達相同的訪問結果。(當然你要是想做高性能運算,這個還是要和硬件直接打交道的,博主之前搞高性能計算,用的一般都是C/C++,更老的語言還有Fortran,不過現在並行計算也是有很多計算框架和協議的,如MPI協議、基於CPU計算的OpenMp,GPU計算的Cuda、OpenAcc等)當然了,JMM在設計之初也是有不少缺陷的,不過後續也逐漸完善起來,還有一個算不上缺陷的缺陷,就是有點難懂。

什麼是JMM

JMM即為JAVA 內存模型(java memory model)。Java內存模型的主要目標是定義程序中各個變量的訪問規則,即在JVM中將變量存儲到內存和從內存中取出變量這樣的底層細節的實現規則。 它其實就是JVM內部的內存數據的訪問規則,線程進行共享數據讀寫的一種規則,在JVM內部,多線程就是根據這個規則讀寫數據的。 注意 ,此處的變量與Java編程裡面的變量有所不同步,它只是包含了實例字段、靜態字段和構成數組對象的元素,但不包含 局部變量方法參數(局部變量和方法參數線程私有的,不會共享,當然不存在數據競爭問題)(如果局部變量是一個reference引用類型,它引用的對象在Java堆中可被各個線程共享,但是reference引用本身在Java棧的局部變量表中,是線程私有的)。為了獲得較高的執行效能,Java內存模型並沒有限制執行引起使用處理器的特定寄存器或者緩存來和主內存進行交互,也沒有限制即時編譯器進行調整代碼執行順序這類優化措施。

JMM和JVM有什麼區別

  • JVM: Java虛擬機模型 主要描述的是Java虛擬機內部的結構以及各個結構之間的關係,Java虛擬機在執行Java程序的過程中,會把它管理的內存劃分為幾個不同的數據區域,這些區域都有各自的用途、創建時間、銷燬時間。
  • JMM:Java內存模型 主要規定了一些內存和線程之間的關係,簡單的說就是描述java虛擬機如何與計算機內存(RAM)一起工作。

JMM中的主內存、工作內存與jJVM中的Java堆、棧、方法區等並不是同一個層次的內存劃分,

JMM核心知識點

Java線程之間的通信由Java內存模型(JMM)控制,JMM決定一個線程對共享變量的寫入何時對另一個線程可見。從抽象的角度來看,JMM定義了 線程和主內存之間的抽象關係 :JMM規定了所有的變量都存儲在主內存(Main Memory)中。每個線程還有自己的工作內存(Working Memory),線程的工作內存中保存了該線程使用到的變量的主內存的副本拷貝,線程對變量的所有操作(讀取、賦值等)都必須在工作內存中進行,而不能直接讀寫主內存中的變量(volatile變量仍然有工作內存的拷貝,但是由於它特殊的操作順序性規定,所以看起來如同直接在主內存中讀寫訪問一般)。不同的線程之間也無法直接訪問對方工作內存中的變量,線程之間值的傳遞都需要通過主內存來完成。

Java併發之內存模型(JMM)淺析

圖:JMM內存模型

這上如可以看見java線程中工作內存是通過cache來和主內存交互的,這是因為計算機的存儲設備與處理器的運算能力之間有幾個數量級的差距,所以現代計算機系統都不得不加入一層或多層讀寫速度儘可能接近處理器運算速度的高速緩存( cache )來作為內存與處理器之間的緩衝:將運算需要使用到的數據複製到緩存中,讓運算能快速進行,當運算結束後再從緩存同步回內存之中沒這樣處理器就無需等待緩慢的內存讀寫了。

線程和線程之間想進行數據的交換一般大致要經歷兩大步驟:1.線程1把工作內存1中的更新過的共享變量刷新到主內存中去;2.線程2到主內存中去讀取線程1刷新過的共享變量,然後copy一份到工作內存2中去。(當然具體實現沒有這麼簡單,具體的操作步驟在下文細講)

三大特徵

Java內存模型是圍繞著併發編程中原子性、可見性、有序性這三個特徵來建立的,那我們依次看一下這三個特徵

1. 原子性

  • 定義: 一個或者多個操作不能被打斷,要麼全部執行完畢,要麼不執行。在這點上有點類似於事務操作,要麼全部執行成功,要麼回退到執行該操作之前的狀態。
  • 注意點: 一般來說在java中基本類型數據的訪問大都是原子操作,但是對於64位的變量如long 和double類型,在32位JVM中,分別處理高低32位,兩個步驟就打破了原子性,這就導致了long、double類型的變量在32位虛擬機中是非原子操作,數據有可能會被破壞,也就意味著多個線程在併發訪問的時候是線程非安全的。所以現在官方建議最好還是使用64JVM,64JVM在安全上和性能上都有所提升。
  • 總結: 對於別的線程而言,他要麼看到的是該線程還沒有執行的情況,要麼就是看到了線程執行後的情況,不會出現執行一半的場景,簡言之,其他線程永遠不會看到中間結果。
  • 解決方案
  • 鎖機制 鎖具有排他性,也就是說它能夠保證一個共享變量在任意一個時刻僅僅被一個線程訪問,這就消除了競爭;
  • CAS( compare-and-swap)

2.可見性

定義:可見性是指當多個線程訪問同一個變量時,當一個線程修改了這個變量的值,其他線程能夠立即獲得修改的值。

實現原理:JMM是通過將在工作內存中的變量修改後的值同步到主內存,在讀取變量前需要從主內存獲取最新值到工作內存中,這種只從主內存的獲取值的方式來實現可見性的 。

存在問題:多線程程序在可見性方面存在問題,這意味著某些線程可能會讀到舊數據,即髒讀。

解決方案

  • volatile 變量:volatile的特殊規則保證了volatile變量值修改後的新值會 立刻 同步到主內存,所以每次獲取的volatile變量都是主內存中最新的值,因此volatile保證了多線程之間的操作變量的可見性
  • synchronized 關鍵字,在同步方法/同步塊開始時(Monitor Enter),使用共享變量時會從主內存中刷新變量值到工作內存中(即從主內存中讀取最新值到線程私有的工作內存中),在同步方法/同步塊結束時(Monitor Exit),會將工作內存中的變量值同步到主內存中去(即將線程私有的工作內存中的值寫入到主內存進行同步)。
  • Lock 接口的最常用的實現ReentrantLock(重入鎖)來實現可見性:當我們在方法的開始位置執行lock.lock()方法,這和synchronized開始位置(Monitor Enter)有相同的語義,即使用共享變量時會從主內存中刷新變量值到工作內存中(即從主內存中讀取最新值到線程私有的工作內存中),在方法的最後finally塊裡執行lock.unlock()方法,和synchronized結束位置(Monitor Exit)有相同的語義,即會將工作內存中的變量值同步到主內存中去(即將線程私有的工作內存中的值寫入到主內存進行同步)。
  • final 關鍵字的可見性是指:被final修飾的變量,在構造函數數一旦初始化完成,並且在構造函數中並沒有把“this”的引用傳遞出去(“this”引用逃逸是很危險的,其他的線程很可能通過該引用訪問到只“初始化一半”的對象),那麼其他線程就可以看到final變量的值。

3.有序性

定義: 即程序執行的順序按照代碼的先後順序執行。這個在單一線程中自然可以保證,但是多線程中就不一定可以保證。

問題原因: 首先處理器為了提高程序運行效率,可能會對目標代碼進行重排序。重排序是對內存訪問操作的一種優化,它可以在不影響 單線程 程序正確性的前提下進行一定的調整,進而提高程序的性能。其保證依據是處理器對涉及依賴關係的數據指令不會進行重排序,沒有依賴關係的則可能進行重排序,即一個指令Instruction 2必須用到Instruction 1的結果,那麼處理器會保證Instruction 1會在Instruction 2之前執行。(PS:並行計算優化中最基本的一項就是去除數據的依賴關係,方法有很多。)但是在多線程中可能會對存在依賴的操作進行重排序,這可能會改變程序的執行結果。

Java有兩種編譯器,一種是Javac靜態編譯器,將源文件編譯為字節碼,代碼編譯階段運行;另一種是動態編譯JIT,會在運行時,動態的將字節碼編譯為本地機器碼(目標代碼),提高java程序運行速度。通常javac不會進行重排序,而JIT則很可能進行重排序

Java併發之內存模型(JMM)淺析

圖:java編譯

總結:在本線程內觀察,操作都是有序的;如果在一個線程中觀察另外一個線程,所有的操作都是無序的。這是因為在多線程中JMM的工作內存和主內存之間存在延遲,而且java會對一些指令進行重新排序。

解決方案

  • volatile關鍵字本身通過加入內存屏障來禁止指令的重排序。
  • synchronized關鍵字通過一個變量在同一時間只允許有一個線程對其進行加鎖的規則來實現。
  • happens-before 原則 java有一個內置的有序規則,無需加同步限制;如果可以從這個原則中推測出來順序,那麼將會對他們進行有序性保障;如果不能推導出來,換句話說不與這些要求相違背,那麼就可能會被重排序,JVM不會對有序性進行保障。

八種基本內存交互操作

JMM定義了8種操作來完成主內存與工作內存的交互細節,虛擬機必須保證這8種操作的每一個操作都是原子的,不可再分的。(對於double和long類型的變量來說,load、store、read和write操作在某些平臺上允許例外)

  • lock (鎖定):作用於主內存的變量,把一個變量標識為線程獨佔狀態
  • unlock (解鎖):作用於主內存的變量,它把一個處於鎖定狀態的變量釋放出來,釋放後的變量才可以被其他線程鎖定
  • read (讀取):作用於主內存變量,它把一個變量的值從主內存傳輸到線程的工作內存中,以便隨後的load動作使用
  • load (載入):作用於工作內存的變量,它把read操作從主存中變量放入工作內存中
  • use (使用):作用於工作內存中的變量,它把工作內存中的變量傳輸給執行引擎,每當虛擬機遇到一個需要使用到變量的值,就會使用到這個指令
  • assign (賦值):作用於工作內存中的變量,它把一個從執行引擎中接受到的值放入工作內存的變量副本中
  • store (存儲):作用於主內存中的變量,它把一個從工作內存中一個變量的值傳送到主內存中,以便後續的write使用
  • write (寫入):作用於主內存中的變量,它把store操作從工作內存中得到的變量的值放入主內存的變量中

現在我們模擬一下兩個線程修改數據的操作流程。線程1 讀取主內存中的值oldNum為1,線程2 讀取主內存中的值oldNum,然後修改值為2,流程如下

"

背景

學習Java併發編程,JMM是繞不過的檻。在Java規範裡面指出了JMM是一個比較開拓性的嘗試,是一種試圖定義一個一致的、跨平臺的內存模型。JMM的最初目的,就是為了能夠支多線程程序設計的,每個線程可以是和其他線程在不同的CPU核心上運行,或者對於多處理器的機器而言,該模型需要實現的就是使得每一個線程就像運行在不同的機器、不同的CPU或者本身就不同的線程上一樣,這種情況實際上在項目開發中是常見的。簡單來說,就是為了屏蔽系統和硬件的差異,讓一套代碼在不同平臺下能到達相同的訪問結果。(當然你要是想做高性能運算,這個還是要和硬件直接打交道的,博主之前搞高性能計算,用的一般都是C/C++,更老的語言還有Fortran,不過現在並行計算也是有很多計算框架和協議的,如MPI協議、基於CPU計算的OpenMp,GPU計算的Cuda、OpenAcc等)當然了,JMM在設計之初也是有不少缺陷的,不過後續也逐漸完善起來,還有一個算不上缺陷的缺陷,就是有點難懂。

什麼是JMM

JMM即為JAVA 內存模型(java memory model)。Java內存模型的主要目標是定義程序中各個變量的訪問規則,即在JVM中將變量存儲到內存和從內存中取出變量這樣的底層細節的實現規則。 它其實就是JVM內部的內存數據的訪問規則,線程進行共享數據讀寫的一種規則,在JVM內部,多線程就是根據這個規則讀寫數據的。 注意 ,此處的變量與Java編程裡面的變量有所不同步,它只是包含了實例字段、靜態字段和構成數組對象的元素,但不包含 局部變量方法參數(局部變量和方法參數線程私有的,不會共享,當然不存在數據競爭問題)(如果局部變量是一個reference引用類型,它引用的對象在Java堆中可被各個線程共享,但是reference引用本身在Java棧的局部變量表中,是線程私有的)。為了獲得較高的執行效能,Java內存模型並沒有限制執行引起使用處理器的特定寄存器或者緩存來和主內存進行交互,也沒有限制即時編譯器進行調整代碼執行順序這類優化措施。

JMM和JVM有什麼區別

  • JVM: Java虛擬機模型 主要描述的是Java虛擬機內部的結構以及各個結構之間的關係,Java虛擬機在執行Java程序的過程中,會把它管理的內存劃分為幾個不同的數據區域,這些區域都有各自的用途、創建時間、銷燬時間。
  • JMM:Java內存模型 主要規定了一些內存和線程之間的關係,簡單的說就是描述java虛擬機如何與計算機內存(RAM)一起工作。

JMM中的主內存、工作內存與jJVM中的Java堆、棧、方法區等並不是同一個層次的內存劃分,

JMM核心知識點

Java線程之間的通信由Java內存模型(JMM)控制,JMM決定一個線程對共享變量的寫入何時對另一個線程可見。從抽象的角度來看,JMM定義了 線程和主內存之間的抽象關係 :JMM規定了所有的變量都存儲在主內存(Main Memory)中。每個線程還有自己的工作內存(Working Memory),線程的工作內存中保存了該線程使用到的變量的主內存的副本拷貝,線程對變量的所有操作(讀取、賦值等)都必須在工作內存中進行,而不能直接讀寫主內存中的變量(volatile變量仍然有工作內存的拷貝,但是由於它特殊的操作順序性規定,所以看起來如同直接在主內存中讀寫訪問一般)。不同的線程之間也無法直接訪問對方工作內存中的變量,線程之間值的傳遞都需要通過主內存來完成。

Java併發之內存模型(JMM)淺析

圖:JMM內存模型

這上如可以看見java線程中工作內存是通過cache來和主內存交互的,這是因為計算機的存儲設備與處理器的運算能力之間有幾個數量級的差距,所以現代計算機系統都不得不加入一層或多層讀寫速度儘可能接近處理器運算速度的高速緩存( cache )來作為內存與處理器之間的緩衝:將運算需要使用到的數據複製到緩存中,讓運算能快速進行,當運算結束後再從緩存同步回內存之中沒這樣處理器就無需等待緩慢的內存讀寫了。

線程和線程之間想進行數據的交換一般大致要經歷兩大步驟:1.線程1把工作內存1中的更新過的共享變量刷新到主內存中去;2.線程2到主內存中去讀取線程1刷新過的共享變量,然後copy一份到工作內存2中去。(當然具體實現沒有這麼簡單,具體的操作步驟在下文細講)

三大特徵

Java內存模型是圍繞著併發編程中原子性、可見性、有序性這三個特徵來建立的,那我們依次看一下這三個特徵

1. 原子性

  • 定義: 一個或者多個操作不能被打斷,要麼全部執行完畢,要麼不執行。在這點上有點類似於事務操作,要麼全部執行成功,要麼回退到執行該操作之前的狀態。
  • 注意點: 一般來說在java中基本類型數據的訪問大都是原子操作,但是對於64位的變量如long 和double類型,在32位JVM中,分別處理高低32位,兩個步驟就打破了原子性,這就導致了long、double類型的變量在32位虛擬機中是非原子操作,數據有可能會被破壞,也就意味著多個線程在併發訪問的時候是線程非安全的。所以現在官方建議最好還是使用64JVM,64JVM在安全上和性能上都有所提升。
  • 總結: 對於別的線程而言,他要麼看到的是該線程還沒有執行的情況,要麼就是看到了線程執行後的情況,不會出現執行一半的場景,簡言之,其他線程永遠不會看到中間結果。
  • 解決方案
  • 鎖機制 鎖具有排他性,也就是說它能夠保證一個共享變量在任意一個時刻僅僅被一個線程訪問,這就消除了競爭;
  • CAS( compare-and-swap)

2.可見性

定義:可見性是指當多個線程訪問同一個變量時,當一個線程修改了這個變量的值,其他線程能夠立即獲得修改的值。

實現原理:JMM是通過將在工作內存中的變量修改後的值同步到主內存,在讀取變量前需要從主內存獲取最新值到工作內存中,這種只從主內存的獲取值的方式來實現可見性的 。

存在問題:多線程程序在可見性方面存在問題,這意味著某些線程可能會讀到舊數據,即髒讀。

解決方案

  • volatile 變量:volatile的特殊規則保證了volatile變量值修改後的新值會 立刻 同步到主內存,所以每次獲取的volatile變量都是主內存中最新的值,因此volatile保證了多線程之間的操作變量的可見性
  • synchronized 關鍵字,在同步方法/同步塊開始時(Monitor Enter),使用共享變量時會從主內存中刷新變量值到工作內存中(即從主內存中讀取最新值到線程私有的工作內存中),在同步方法/同步塊結束時(Monitor Exit),會將工作內存中的變量值同步到主內存中去(即將線程私有的工作內存中的值寫入到主內存進行同步)。
  • Lock 接口的最常用的實現ReentrantLock(重入鎖)來實現可見性:當我們在方法的開始位置執行lock.lock()方法,這和synchronized開始位置(Monitor Enter)有相同的語義,即使用共享變量時會從主內存中刷新變量值到工作內存中(即從主內存中讀取最新值到線程私有的工作內存中),在方法的最後finally塊裡執行lock.unlock()方法,和synchronized結束位置(Monitor Exit)有相同的語義,即會將工作內存中的變量值同步到主內存中去(即將線程私有的工作內存中的值寫入到主內存進行同步)。
  • final 關鍵字的可見性是指:被final修飾的變量,在構造函數數一旦初始化完成,並且在構造函數中並沒有把“this”的引用傳遞出去(“this”引用逃逸是很危險的,其他的線程很可能通過該引用訪問到只“初始化一半”的對象),那麼其他線程就可以看到final變量的值。

3.有序性

定義: 即程序執行的順序按照代碼的先後順序執行。這個在單一線程中自然可以保證,但是多線程中就不一定可以保證。

問題原因: 首先處理器為了提高程序運行效率,可能會對目標代碼進行重排序。重排序是對內存訪問操作的一種優化,它可以在不影響 單線程 程序正確性的前提下進行一定的調整,進而提高程序的性能。其保證依據是處理器對涉及依賴關係的數據指令不會進行重排序,沒有依賴關係的則可能進行重排序,即一個指令Instruction 2必須用到Instruction 1的結果,那麼處理器會保證Instruction 1會在Instruction 2之前執行。(PS:並行計算優化中最基本的一項就是去除數據的依賴關係,方法有很多。)但是在多線程中可能會對存在依賴的操作進行重排序,這可能會改變程序的執行結果。

Java有兩種編譯器,一種是Javac靜態編譯器,將源文件編譯為字節碼,代碼編譯階段運行;另一種是動態編譯JIT,會在運行時,動態的將字節碼編譯為本地機器碼(目標代碼),提高java程序運行速度。通常javac不會進行重排序,而JIT則很可能進行重排序

Java併發之內存模型(JMM)淺析

圖:java編譯

總結:在本線程內觀察,操作都是有序的;如果在一個線程中觀察另外一個線程,所有的操作都是無序的。這是因為在多線程中JMM的工作內存和主內存之間存在延遲,而且java會對一些指令進行重新排序。

解決方案

  • volatile關鍵字本身通過加入內存屏障來禁止指令的重排序。
  • synchronized關鍵字通過一個變量在同一時間只允許有一個線程對其進行加鎖的規則來實現。
  • happens-before 原則 java有一個內置的有序規則,無需加同步限制;如果可以從這個原則中推測出來順序,那麼將會對他們進行有序性保障;如果不能推導出來,換句話說不與這些要求相違背,那麼就可能會被重排序,JVM不會對有序性進行保障。

八種基本內存交互操作

JMM定義了8種操作來完成主內存與工作內存的交互細節,虛擬機必須保證這8種操作的每一個操作都是原子的,不可再分的。(對於double和long類型的變量來說,load、store、read和write操作在某些平臺上允許例外)

  • lock (鎖定):作用於主內存的變量,把一個變量標識為線程獨佔狀態
  • unlock (解鎖):作用於主內存的變量,它把一個處於鎖定狀態的變量釋放出來,釋放後的變量才可以被其他線程鎖定
  • read (讀取):作用於主內存變量,它把一個變量的值從主內存傳輸到線程的工作內存中,以便隨後的load動作使用
  • load (載入):作用於工作內存的變量,它把read操作從主存中變量放入工作內存中
  • use (使用):作用於工作內存中的變量,它把工作內存中的變量傳輸給執行引擎,每當虛擬機遇到一個需要使用到變量的值,就會使用到這個指令
  • assign (賦值):作用於工作內存中的變量,它把一個從執行引擎中接受到的值放入工作內存的變量副本中
  • store (存儲):作用於主內存中的變量,它把一個從工作內存中一個變量的值傳送到主內存中,以便後續的write使用
  • write (寫入):作用於主內存中的變量,它把store操作從工作內存中得到的變量的值放入主內存的變量中

現在我們模擬一下兩個線程修改數據的操作流程。線程1 讀取主內存中的值oldNum為1,線程2 讀取主內存中的值oldNum,然後修改值為2,流程如下

Java併發之內存模型(JMM)淺析

從上圖可以看出,實際使用中在一種有可能,其他線程修改完值,線程的Cache還沒有同步到主存中,每個線程中的Cahe中的值副本不一樣,可能會造成"髒讀"。緩存一致性協議,就是為了解決這樣的問題還現,(在這之前還有總線鎖機制,但是由於鎖機制比較消耗性能,最終還是被逐漸取代了)。 它規定每個線程中的Cache使用的共享變量副本是一樣的,採用的是總線嗅探技術,流程大致如下

當CPU寫數據時,如果發現操作的變量式共享變量,它將通知其他CPU該變量的緩存行為無效,所以當其他CPU需要讀取這個變量的時候,發現自己的緩存行為無效,那麼就會從主存中重新獲取。

volatile 會在store時加上一個lock寫完主內存後unlock,這樣保證變量在回寫主內存時保證變量不被別的變量修改,而且鎖的粒度比較小,性能較好。

Volatile

作用

保證了多線程操作下變量的可見性,即某個一個線程修改了被volatile修飾的變量的值,這個被修改變量的新值對其他線程來說是 立即 可見的。

線程池中的許多參數都是採用volatile來修飾的 如線程工廠threadFactory,拒絕策略handler,等到任務的超時時間keepAliveTime,keepAliveTime的開關allowCoreThreadTimeOut,核心池大小corePoolSize,最大線程數maximumPoolSize等。因為在線程池中有若干個線程,這些變量必需保持對所有線程的可見性,不然會引起線程池運行錯誤。

缺點

對任意單個volatile變量的讀/寫具有原子性,但類似於volatile++這種複合操作(自增操作是三個原子操作組合而成的複合操作)不具有原子性,原因就是由於volatile會在store操作時加上lock,其餘線程在執行store時,由於獲取不到鎖而阻塞,會導致當線程對值的修改失效。

原理

底層實現主要是通過彙編的lock的前綴指令,他會鎖定這塊內存區域的緩存(緩存行鎖定)並寫回到主內存,lock前綴指令實際上相當於一個內存屏障(也可以稱為內存柵欄),內存屏障會提供3個功能:

  1. 它確保指令重排序時不會把其後面的指令排到內存屏障之前的位置,也不會把前面的指令排到內存屏障的後面;即在執行到內存屏障這句指令時,在它前面的操作已經全部完成;
  2. 它會強制將對緩存的修改操作立即寫入主存;
  3. 如果是寫操作,它會導致其他CPU中對應的緩存行無效(MESI緩存一直性協議)。

總結

JMM模型則是對於JVM對於內存訪問的一種規範,多線程工作內存與主內存之間的交互原則進行了指示,他是獨立於具體物理機器的一種內存存取模型。
對於多線程的數據安全問題,三個方面,原子性、可見性、有序性是三個相互協作的方面,不是說保障了任何一個就萬事大吉了,另外也並不一定是所有的場景都需要全部都保障才能夠線程安全。
參考資料
https://www.cnblogs.com/lewis0077/p/5143268.html
《java併發編程》
"

相關推薦

推薦中...