全面理解Java內存模型

Java內存模型即Java Memory Model,簡稱JMM。JMM定義了Java 虛擬機(JVM)在計算機內存(RAM)中的工作方式。JVM是整個計算機虛擬模型,所以JMM是隸屬于JVM的。

如果我們要想深入了解Java并發編程,就要先理解好Java內存模型。Java內存模型定義了多線程之間共享變量的可見性以及如何在需要的時候對共享變量進行同步。原始的Java內存模型效率并不是很理想,因此Java1.5版本對其進行了重構,現在的Java8仍沿用了Java1.5的版本。

關于并發編程

在并發編程領域,有兩個關鍵問題:線程之間的通信同步

線程之間的通信

線程的通信是指線程之間以何種機制來交換信息。在命令式編程中,線程之間的通信機制有兩種共享內存消息傳遞

共享內存的并發模型里,線程之間共享程序的公共狀態,線程之間通過寫-讀內存中的公共狀態來隱式進行通信,典型的共享內存通信方式就是通過共享對象進行通信。

消息傳遞的并發模型里,線程之間沒有公共狀態,線程之間必須通過明確的發送消息來顯式進行通信,在java中典型的消息傳遞方式就是wait()notify()

關于Java線程之間的通信,可以參考線程之間的通信(thread signal)。

線程之間的同步

同步是指程序用于控制不同線程之間操作發生相對順序的機制。

在共享內存并發模型里,同步是顯式進行的。程序員必須顯式指定某個方法或某段代碼需要在線程之間互斥執行。

在消息傳遞的并發模型里,由于消息的發送必須在消息的接收之前,因此同步是隱式進行的。

Java的并發采用的是共享內存模型

Java線程之間的通信總是隱式進行,整個通信過程對程序員完全透明。如果編寫多線程程序的Java程序員不理解隱式進行的線程之間通信的工作機制,很可能會遇到各種奇怪的內存可見性問題。

Java內存模型

上面講到了Java線程之間的通信采用的是過共享內存模型,這里提到的共享內存模型指的就是Java內存模型(簡稱JMM),JMM決定一個線程對共享變量的寫入何時對另一個線程可見。從抽象的角度來看,JMM定義了線程和主內存之間的抽象關系:線程之間的共享變量存儲在主內存(main memory)中,每個線程都有一個私有的本地內存(local memory),本地內存中存儲了該線程以讀/寫共享變量的副本。本地內存是JMM的一個抽象概念,并不真實存在。它涵蓋了緩存,寫緩沖區,寄存器以及其他的硬件和編譯器優化。

這里寫圖片描述

從上圖來看,線程A與線程B之間如要通信的話,必須要經歷下面2個步驟:

1. 首先,線程A把本地內存A中更新過的共享變量刷新到主內存中去。
2. 然后,線程B到主內存中去讀取線程A之前已更新過的共享變量。 
  • 1
  • 2
  • 3

下面通過示意圖來說明這兩個步驟:?
這里寫圖片描述

如上圖所示,本地內存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程序員提供內存可見性保證。

上面也說到了,Java內存模型只是一個抽象概念,那么它在Java中具體是怎么工作的呢?為了更好的理解上Java內存模型工作方式,下面就JVM對Java內存模型的實現、硬件內存模型及它們之間的橋接做詳細介紹。

JVM對Java內存模型的實現

在JVM內部,Java內存模型把內存分成了兩部分:線程棧區和堆區,下圖展示了Java內存模型在JVM中的邏輯視圖:?
這里寫圖片描述?
JVM中運行的每個線程都擁有自己的線程棧,線程棧包含了當前線程執行的方法調用相關信息,我們也把它稱作調用棧。隨著代碼的不斷執行,調用棧會不斷變化。

線程棧還包含了當前方法的所有本地變量信息。一個線程只能讀取自己的線程棧,也就是說,線程中的本地變量對其它線程是不可見的。即使兩個線程執行的是同一段代碼,它們也會各自在自己的線程棧中創建本地變量,因此,每個線程中的本地變量都會有自己的版本。

所有原始類型(boolean,byte,short,char,int,long,float,double)的本地變量都直接保存在線程棧當中,對于它們的值各個線程之間都是獨立的。對于原始類型的本地變量,一個線程可以傳遞一個副本給另一個線程,當它們之間是無法共享的。

堆區包含了Java應用創建的所有對象信息,不管對象是哪個線程創建的,其中的對象包括原始類型的封裝類(如Byte、Integer、Long等等)。不管對象是屬于一個成員變量還是方法中的本地變量,它都會被存儲在堆區。

下圖展示了調用棧和本地變量都存儲在棧區,對象都存儲在堆區:?
這里寫圖片描述?
一個本地變量如果是原始類型,那么它會被完全存儲到棧區。?
一個本地變量也有可能是一個對象的引用,這種情況下,這個本地引用會被存儲到棧中,但是對象本身仍然存儲在堆區。

對于一個對象的成員方法,這些方法中包含本地變量,仍需要存儲在棧區,即使它們所屬的對象在堆區。?
對于一個對象的成員變量,不管它是原始類型還是包裝類型,都會被存儲到堆區。

Static類型的變量以及類本身相關信息都會隨著類本身存儲在堆區。

堆中的對象可以被多線程共享。如果一個線程獲得一個對象的應用,它便可訪問這個對象的成員變量。如果兩個線程同時調用了同一個對象的同一個方法,那么這兩個線程便可同時訪問這個對象的成員變量,但是對于本地變量,每個線程都會拷貝一份到自己的線程棧中。

下圖展示了上面描述的過程:?
這里寫圖片描述

硬件內存架構

不管是什么內存模型,最終還是運行在計算機硬件上的,所以我們有必要了解計算機硬件內存架構,下圖就簡單描述了當代計算機硬件內存架構:?
這里寫圖片描述

現代計算機一般都有2個以上CPU,而且每個CPU還有可能包含多個核心。因此,如果我們的應用是多線程的話,這些線程可能會在各個CPU核心中并行運行。

在CPU內部有一組CPU寄存器,也就是CPU的儲存器。CPU操作寄存器的速度要比操作計算機主存快的多。在主存和CPU寄存器之間還存在一個CPU緩存,CPU操作CPU緩存的速度快于主存但慢于CPU寄存器。某些CPU可能有多個緩存層(一級緩存和二級緩存)。計算機的主存也稱作RAM,所有的CPU都能夠訪問主存,而且主存比上面提到的緩存和寄存器大很多。

當一個CPU需要訪問主存時,會先讀取一部分主存數據到CPU緩存,進而在讀取CPU緩存到寄存器。當CPU需要寫數據到主存時,同樣會先flush寄存器到CPU緩存,然后再在某些節點把緩存數據flush到主存。

Java內存模型和硬件架構之間的橋接

正如上面講到的,Java內存模型和硬件內存架構并不一致。硬件內存架構中并沒有區分棧和堆,從硬件上看,不管是棧還是堆,大部分數據都會存到主存中,當然一部分棧和堆的數據也有可能會存到CPU寄存器中,如下圖所示,Java內存模型和計算機硬件內存架構是一個交叉關系:?
這里寫圖片描述?
當對象和變量存儲到計算機的各個內存區域時,必然會面臨一些問題,其中最主要的兩個問題是:

1. 共享對象對各個線程的可見性
2. 共享對象的競爭現象
  • 1
  • 2
  • 3

共享對象的可見性

當多個線程同時操作同一個共享對象時,如果沒有合理的使用volatile和synchronization關鍵字,一個線程對共享對象的更新有可能導致其它線程不可見。

想象一下我們的共享對象存儲在主存,一個CPU中的線程讀取主存數據到CPU緩存,然后對共享對象做了更改,但CPU緩存中的更改后的對象還沒有flush到主存,此時線程對共享對象的更改對其它CPU中的線程是不可見的。最終就是每個線程最終都會拷貝共享對象,而且拷貝的對象位于不同的CPU緩存中。

下圖展示了上面描述的過程。左邊CPU中運行的線程從主存中拷貝共享對象obj到它的CPU緩存,把對象obj的count變量改為2。但這個變更對運行在右邊CPU中的線程不可見,因為這個更改還沒有flush到主存中:?
這里寫圖片描述?
要解決共享對象可見性這個問題,我們可以使用java volatile關鍵字。 Java’s volatile keyword. volatile 關鍵字可以保證變量會直接從主存讀取,而對變量的更新也會直接寫到主存。volatile原理是基于CPU內存屏障指令實現的,后面會講到。

競爭現象

如果多個線程共享一個對象,如果它們同時修改這個共享對象,這就產生了競爭現象。

如下圖所示,線程A和線程B共享一個對象obj。假設線程A從主存讀取Obj.count變量到自己的CPU緩存,同時,線程B也讀取了Obj.count變量到它的CPU緩存,并且這兩個線程都對Obj.count做了加1操作。此時,Obj.count加1操作被執行了兩次,不過都在不同的CPU緩存中。

如果這兩個加1操作是串行執行的,那么Obj.count變量便會在原始值上加2,最終主存中的Obj.count的值會是3。然而下圖中兩個加1操作是并行的,不管是線程A還是線程B先flush計算結果到主存,最終主存中的Obj.count只會增加1次變成2,盡管一共有兩次加1操作。?
這里寫圖片描述

要解決上面的問題我們可以使用java synchronized代碼塊。synchronized代碼塊可以保證同一個時刻只能有一個線程進入代碼競爭區,synchronized代碼塊也能保證代碼塊中所有變量都將會從主存中讀,當線程退出代碼塊時,對所有變量的更新將會flush到主存,不管這些變量是不是volatile類型的。

volatile和 synchronized區別

詳細請見?volatile和synchronized的區別

支撐Java內存模型的基礎原理

指令重排序

在執行程序時,為了提高性能,編譯器和處理器會對指令做重排序。但是,JMM確保在不同的編譯器和不同的處理器平臺之上,通過插入特定類型的Memory Barrier來禁止特定類型的編譯器重排序和處理器重排序,為上層提供一致的內存可見性保證。

  1. 編譯器優化重排序:編譯器在不改變單線程程序語義的前提下,可以重新安排語句的執行順序。
  2. 指令級并行的重排序:如果不存l在數據依賴性,處理器可以改變語句對應機器指令的執行順序。
  3. 內存系統的重排序:處理器使用緩存和讀寫緩沖區,這使得加載和存儲操作看上去可能是在亂序執行。

數據依賴性

如果兩個操作訪問同一個變量,其中一個為寫操作,此時這兩個操作之間存在數據依賴性。?
編譯器和處理器不會改變存在數據依賴性關系的兩個操作的執行順序,即不會重排序。

as-if-serial

不管怎么重排序,單線程下的執行結果不能被改變,編譯器、runtime和處理器都必須遵守as-if-serial語義。

內存屏障(Memory Barrier )

上面講到了,通過內存屏障可以禁止特定類型處理器的重排序,從而讓程序按我們預想的流程去執行。內存屏障,又稱內存柵欄,是一個CPU指令,基本上它是一條這樣的指令:

  1. 保證特定操作的執行順序。
  2. 影響某些數據(或則是某條指令的執行結果)的內存可見性。

編譯器和CPU能夠重排序指令,保證最終相同的結果,嘗試優化性能。插入一條Memory Barrier會告訴編譯器和CPU:不管什么指令都不能和這條Memory Barrier指令重排序。

Memory Barrier所做的另外一件事是強制刷出各種CPU cache,如一個Write-Barrier(寫入屏障)將刷出所有在Barrier之前寫入 cache 的數據,因此,任何CPU上的線程都能讀取到這些數據的最新版本。

這和java有什么關系?上面java內存模型中講到的volatile是基于Memory Barrier實現的。

如果一個變量是volatile修飾的,JMM會在寫入這個字段之后插進一個Write-Barrier指令,并在讀這個字段之前插入一個Read-Barrier指令。這意味著,如果寫入一個volatile變量,就可以保證:

  1. 一個線程寫入變量a后,任何線程訪問該變量都會拿到最新值。
  2. 在寫入變量a之前的寫入操作,其更新的數據對于其他線程也是可見的。因為Memory Barrier會刷出cache中的所有先前的寫入。

happens-before

從jdk5開始,java使用新的JSR-133內存模型,基于happens-before的概念來闡述操作之間的內存可見性。

在JMM中,如果一個操作的執行結果需要對另一個操作可見,那么這兩個操作之間必須要存在happens-before關系,這個的兩個操作既可以在同一個線程,也可以在不同的兩個線程中。

與程序員密切相關的happens-before規則如下:

  1. 程序順序規則:一個線程中的每個操作,happens-before于該線程中任意的后續操作。
  2. 監視器鎖規則:對一個鎖的解鎖操作,happens-before于隨后對這個鎖的加鎖操作。
  3. volatile域規則:對一個volatile域的寫操作,happens-before于任意線程后續對這個volatile域的讀。
  4. 傳遞性規則:如果 A happens-before B,且 B happens-before C,那么A happens-before C。

注意:兩個操作之間具有happens-before關系,并不意味前一個操作必須要在后一個操作之前執行!僅僅要求前一個操作的執行結果,對于后一個操作是可見的,且前一個操作按順序排在后一個操作之前。

轉載于:https://www.cnblogs.com/shizhiyi/p/7857141.html

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/390803.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/390803.shtml
英文地址,請注明出處:http://en.pswp.cn/news/390803.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

React Native指南

React本機 (React Native) React Native is a cross-platform framework for building mobile applications that can run outside of the browser?—?most commonly iOS and Android applicationsReact Native是一個跨平臺框架,用于構建可在瀏覽器外部運行的移動…

leetcode 1074. 元素和為目標值的子矩陣數量(map+前綴和)

給出矩陣 matrix 和目標值 target&#xff0c;返回元素總和等于目標值的非空子矩陣的數量。 子矩陣 x1, y1, x2, y2 是滿足 x1 < x < x2 且 y1 < y < y2 的所有單元 matrix[x][y] 的集合。 如果 (x1, y1, x2, y2) 和 (x1’, y1’, x2’, y2’) 兩個子矩陣中部分坐…

失物招領php_新奧爾良圣徒隊是否增加了失物招領?

失物招領phpOver the past couple of years, the New Orleans Saints’ offense has been criticized for its lack of wide receiver options. Luckily for Saints’ fans like me, this area has been addressed by the signing of Emmanuel Sanders back in March — or has…

教你分分鐘使用Retrofit+Rxjava實現網絡請求

擼代碼之前&#xff0c;先簡單了解一下為什么Retrofit這么受大家青睞吧。 Retrofit是Square公司出品的基于OkHttp封裝的一套RESTful&#xff08;目前流行的一套api設計的風格&#xff09;網絡請求框架。它內部使用了大量的設計模式&#xff0c;以達到高度解耦的目的&#xff1b…

線程與進程區別

一.定義&#xff1a; 進程&#xff08;process&#xff09;是一塊包含了某些資源的內存區域。操作系統利用進程把它的工作劃分為一些功能單元。 進程中所包含的一個或多個執行單元稱為線程&#xff08;thread&#xff09;。進程還擁有一個私有的虛擬地址空間&#xff0c;該空間…

基本SQL命令-您應該知道的數據庫查詢和語句列表

SQL stands for Structured Query Language. SQL commands are the instructions used to communicate with a database to perform tasks, functions, and queries with data.SQL代表結構化查詢語言。 SQL命令是用于與數據庫通信以執行任務&#xff0c;功能和數據查詢的指令。…

leetcode 5756. 兩個數組最小的異或值之和(狀態壓縮dp)

題目 給你兩個整數數組 nums1 和 nums2 &#xff0c;它們長度都為 n 。 兩個數組的 異或值之和 為 (nums1[0] XOR nums2[0]) (nums1[1] XOR nums2[1]) … (nums1[n - 1] XOR nums2[n - 1]) &#xff08;下標從 0 開始&#xff09;。 比方說&#xff0c;[1,2,3] 和 [3,2,1…

客戶細分模型_Avarto金融解決方案的客戶細分和監督學習模型

客戶細分模型Lets assume that you are a CEO of a company which have some X amount of customers in a city with 1000 *X population. Analyzing the trends/features of your customer and segmenting the population of the city to land new potential customers would …

用 Go 編寫一個簡單的 WebSocket 推送服務

用 Go 編寫一個簡單的 WebSocket 推送服務 本文中代碼可以在 github.com/alfred-zhon… 獲取。 背景 最近拿到需求要在網頁上展示報警信息。以往報警信息都是通過短信&#xff0c;微信和 App 推送給用戶的&#xff0c;現在要讓登錄用戶在網頁端也能實時接收到報警推送。 依稀記…

leetcode 231. 2 的冪

給你一個整數 n&#xff0c;請你判斷該整數是否是 2 的冪次方。如果是&#xff0c;返回 true &#xff1b;否則&#xff0c;返回 false 。 如果存在一個整數 x 使得 n 2x &#xff0c;則認為 n 是 2 的冪次方。 示例 1&#xff1a; 輸入&#xff1a;n 1 輸出&#xff1a;tr…

Java概述、環境變量、注釋、關鍵字、標識符、常量

Java語言的特點 有很多小特點&#xff0c;重點有兩個開源&#xff0c;跨平臺 Java語言是跨平臺的 Java語言的平臺 JavaSE JavaME--Android JavaEE DK,JRE,JVM的作用及關系(掌握) (1)作用 JVM&#xff1a;保證Java語言跨平臺 &#xff0…

寫游戲軟件要學什么_為什么要寫關于您所知道的(或所學到的)的內容

寫游戲軟件要學什么Im either comfortably retired or unemployed, I havent decided which. What I do know is that I am not yet ready for decades of hard-won knowledge to lie fallow. Still driven to learn new technologies and to develop new projects, I see the …

leetcode 342. 4的冪

給定一個整數&#xff0c;寫一個函數來判斷它是否是 4 的冪次方。如果是&#xff0c;返回 true &#xff1b;否則&#xff0c;返回 false 。 整數 n 是 4 的冪次方需滿足&#xff1a;存在整數 x 使得 n 4x 示例 1&#xff1a; 輸入&#xff1a;n 16 輸出&#xff1a;true …

梯度反傳_反事實政策梯度解釋

梯度反傳Among many of its challenges, multi-agent reinforcement learning has one obstacle that is overlooked: “credit assignment.” To explain this concept, let’s first take a look at an example…在許多挑戰中&#xff0c;多主體強化學習有一個被忽略的障礙&a…

三款功能強大代碼比較工具Beyond compare、DiffMerge、WinMerge

我們經常會遇到需要比較同一文件的不同版本&#xff0c;特別是代碼文件。如果人工去對比查看&#xff0c;勢必費時實力還會出現紕漏和錯誤&#xff0c;因此我們需要借助一些代碼比較的工具來自動完成這些工作。這里介紹3款比較流行且功能強大的工具。 1. Beyond compare這是一款…

shell腳本_Shell腳本

shell腳本In the command line, a shell script is an executable file that contains a set of instructions that the shell will execute. Its main purpose is to reduce a set of instructions (or commands) in just one file. Also it can handle some logic because it…

大數據與Hadoop

大數據的定義 大數據是指無法在一定時間內用常規軟件工具對其內容進行抓取、管理和處理的數據集合。 大數據的概念–4VXV 1,數據量大&#xff08;Volume&#xff09;2,類型繁多&#xff08;Variety &#xff09;3,速度快時效高&#xff08;Velocity&#xff09;4,價值密度低…

Arm匯編指令學習

ARM指令格式 ARM指令格式解析 opcode: 指令助記符,例如,MOV ,ADD,SUB等等 cond&#xff1a;指令條件碼表.下面附一張圖 {S}:是否影響CPSR的值. {.W .N}:指令寬度說明符,無論是ARM代碼還是Thumb&#xff08;armv6t2或更高版本&#xff09;代碼都可以在其中使用.W寬度說明符&…

facebook.com_如何降低電子商務的Facebook CPM

facebook.comWith the 2020 election looming, Facebook advertisers and e-commerce stores are going to continually see their ad costs go up as the date gets closer (if they haven’t already).隨著2020年選舉的臨近&#xff0c;隨著日期越來越近&#xff0c;Facebook…

Python中的If,Elif和Else語句

如果Elif Else聲明 (If Elif Else Statements) The if/elif/else structure is a common way to control the flow of a program, allowing you to execute specific blocks of code depending on the value of some data.if / elif / else結構是控制程序流程的常用方法&#x…