日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程语言 > java >内容正文

java

Java内存模型—JMM详解

發(fā)布時(shí)間:2023/12/10 java 46 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Java内存模型—JMM详解 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

目錄

什么是JMM?

JMM內(nèi)存模型

內(nèi)存交互操作

JMM三大特性

原子性

可見性

有序性

指令重排問(wèn)題

處理器重排序與內(nèi)存屏障

數(shù)據(jù)依賴性

as-if-serial語(yǔ)義?

volatile

CAS

ABA問(wèn)題

什么是ABA問(wèn)題

如何解決ABA問(wèn)題

各種鎖的理解

公平鎖和非公平鎖

可重入鎖

自旋鎖?


什么是JMM?

JMM即為JAVA 內(nèi)存模型(java memory model)。不存在的東西,是概念,是約定。因?yàn)樵诓煌挠布a(chǎn)商和不同的操作系統(tǒng)下,內(nèi)存的訪問(wèn)邏輯有一定的差異,結(jié)果就是當(dāng)你的代碼在某個(gè)系統(tǒng)環(huán)境下運(yùn)行良好,并且線程安全,但是換了個(gè)系統(tǒng)就出現(xiàn)各種問(wèn)題。Java內(nèi)存模型,就是為了屏蔽系統(tǒng)和硬件的差異,讓一套代碼在不同平臺(tái)下能到達(dá)相同的訪問(wèn)結(jié)果。即達(dá)到Java程序能夠“一次編寫,到處運(yùn)行”。

內(nèi)存模型描述了程序中各個(gè)變量(實(shí)例域、靜態(tài)域和數(shù)組元素)之間的關(guān)系,以及在實(shí)際計(jì)算機(jī)系統(tǒng)中將變量存儲(chǔ)到內(nèi)存和從內(nèi)存中取出變量這樣的底層細(xì)節(jié)

Java Memory?Model(Java內(nèi)存模型), 圍繞著在并發(fā)過(guò)程中如何處理可見性、原子性、有序性這三個(gè)特性而建立的模型。

JMM從java 5開始的JSR-133發(fā)布后,已經(jīng)成熟和完善起來(lái)。

JSR-133規(guī)范

即JavaTM內(nèi)存模型與線程規(guī)范,由JSR-133專家組開發(fā)。本規(guī)范是JSR-176(定義了JavaTM平臺(tái) Tiger(5.0)發(fā)布版的主要特性)的一部分。本規(guī)范的標(biāo)準(zhǔn)內(nèi)容將合并到JavaTM語(yǔ)言規(guī)范、JavaTM虛擬機(jī)規(guī)范以及java.lang包的類說(shuō)明中。
JSR-133中文版下載
  該規(guī)范在Java語(yǔ)言規(guī)范里面指出了JMM是一個(gè)比較開拓性的嘗試,這種嘗試視圖定義一個(gè)一致的、跨平臺(tái)的內(nèi)存模型,但是它有一些比較細(xì)微而且很重要的缺點(diǎn)。它提供大范圍的流行硬件體系結(jié)構(gòu)上的高性能JVM實(shí)現(xiàn),現(xiàn)在的處理器在它們的內(nèi)存模型上有著很大的不同,JMM應(yīng)該能夠適合于實(shí)際的盡可能多的體系結(jié)構(gòu)而不以性能為代價(jià),這也是Java跨平臺(tái)型設(shè)計(jì)的基礎(chǔ)。

其實(shí)Java語(yǔ)言里面比較容易混淆的關(guān)鍵字主要是synchronized和volatile,也因?yàn)檫@樣在開發(fā)過(guò)程中往往開發(fā)者會(huì)忽略掉這些規(guī)則,這也使得編寫同步代碼比較困難。
JSR133本身的目的是為了修復(fù)原本JMM的一些缺陷而提出的。

?

JMM內(nèi)存模型

JMM規(guī)定了所有的變量都存儲(chǔ)在主內(nèi)存(Main Memory)中。每個(gè)線程還有自己的工作內(nèi)存(Working Memory),線程的工作內(nèi)存中保存了該線程使用到的變量的主內(nèi)存的副本拷貝,線程對(duì)變量的所有操作(讀取、賦值等)都必須在工作內(nèi)存中進(jìn)行,而不能直接讀寫主內(nèi)存中的變量(volatile變量仍然有工作內(nèi)存的拷貝,但是由于它特殊的操作順序性規(guī)定,所以看起來(lái)如同直接在主內(nèi)存中讀寫訪問(wèn)一般)。不同的線程之間也無(wú)法直接訪問(wèn)對(duì)方工作內(nèi)存中的變量,線程之間值的傳遞都需要通過(guò)主內(nèi)存來(lái)完成。

??

從更底層的來(lái)說(shuō),主內(nèi)存對(duì)應(yīng)的是硬件的物理內(nèi)存,工作內(nèi)存對(duì)應(yīng)的是寄存器和高速緩存。

?JVM在設(shè)計(jì)時(shí)候考慮到,如果JAVA線程每次讀取和寫入變量都直接操作主內(nèi)存,對(duì)性能影響比較大,所以每條線程擁有各自的工作內(nèi)存,工作內(nèi)存中的變量是主內(nèi)存中的一份拷貝,線程對(duì)變量的讀取和寫入,直接在工作內(nèi)存中操作,而不能直接去操作主內(nèi)存中的變量。但是這樣就會(huì)出現(xiàn)一個(gè)問(wèn)題,當(dāng)一個(gè)線程修改了自己工作內(nèi)存中變量,對(duì)其他線程是不可見的,會(huì)導(dǎo)致線程不安全的問(wèn)題。因?yàn)镴MM制定了一套標(biāo)準(zhǔn)來(lái)保證開發(fā)者在編寫多線程程序的時(shí)候,能夠控制什么時(shí)候內(nèi)存會(huì)被同步給其他線程。

內(nèi)存交互操作

內(nèi)存交互操作有8種,虛擬機(jī)實(shí)現(xiàn)必須保證每一個(gè)操作都是原子的,不可在分的(對(duì)于double和long類型的變量來(lái)說(shuō),load、store、read和write操作在某些平臺(tái)上允許例外)

  • lock? ? ?(鎖定):作用于主內(nèi)存的變量,把一個(gè)變量標(biāo)識(shí)為線程獨(dú)占狀態(tài)
  • unlock (解鎖):作用于主內(nèi)存的變量,它把一個(gè)處于鎖定狀態(tài)的變量釋放出來(lái),釋放后的變量才可以被其他線程鎖定
  • read? ? (讀取):作用于主內(nèi)存變量,它把一個(gè)變量的值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存中,以便隨后的load動(dòng)作使用
  • load? ? ?(載入):作用于工作內(nèi)存的變量,它把read操作從主存中變量放入工作內(nèi)存中
  • use? ? ? (使用):作用于工作內(nèi)存中的變量,它把工作內(nèi)存中的變量傳輸給執(zhí)行引擎,每當(dāng)虛擬機(jī)遇到一個(gè)需要使用到變量的值,就會(huì)使用到這個(gè)指令
  • assign? (賦值):作用于工作內(nèi)存中的變量,它把一個(gè)從執(zhí)行引擎中接受到的值放入工作內(nèi)存的變量副本中
  • store? ? (存儲(chǔ)):作用于主內(nèi)存中的變量,它把一個(gè)從工作內(nèi)存中一個(gè)變量的值傳送到主內(nèi)存中,以便后續(xù)的write使用
  • write  (寫入):作用于主內(nèi)存中的變量,它把store操作從工作內(nèi)存中得到的變量的值放入主內(nèi)存的變量中

JMM對(duì)這八種指令的使用,制定了如下規(guī)則:

  • 不允許read和load、store和write操作之一單獨(dú)出現(xiàn)。即使用了read必須load,使用了store必須write
  • 不允許線程丟棄他最近的assign操作,即工作變量的數(shù)據(jù)改變了之后,必須告知主存
  • 不允許一個(gè)線程將沒有assign的數(shù)據(jù)從工作內(nèi)存同步回主內(nèi)存
  • 一個(gè)新的變量必須在主內(nèi)存中誕生,不允許工作內(nèi)存直接使用一個(gè)未被初始化的變量。就是懟變量實(shí)施use、store操作之前,必須經(jīng)過(guò)assign和load操作
  • 一個(gè)變量同一時(shí)間只有一個(gè)線程能對(duì)其進(jìn)行l(wèi)ock。多次lock后,必須執(zhí)行相同次數(shù)的unlock才能解鎖
  • 如果對(duì)一個(gè)變量進(jìn)行l(wèi)ock操作,會(huì)清空所有工作內(nèi)存中此變量的值,在執(zhí)行引擎使用這個(gè)變量前,必須重新load或assign操作初始化變量的值
  • 如果一個(gè)變量沒有被lock,就不能對(duì)其進(jìn)行unlock操作。也不能unlock一個(gè)被其他線程鎖住的變量
  • 對(duì)一個(gè)變量進(jìn)行unlock操作之前,必須把此變量同步回主內(nèi)存

畫圖舉例:

分析:

1、首先,主內(nèi)存中,initFlag=false。

2、線程1經(jīng)過(guò) read、load,工作內(nèi)存處,initFlag = false,use 之后, 執(zhí)行引擎處 !initFlag = true,此時(shí),卡在while處。

3、線程2同理,經(jīng)過(guò) read、load 之后,此時(shí)工作內(nèi)存處,initFlag=false,經(jīng)過(guò) use (initFlag=true)、assign后,initFlag=true。

4、因?yàn)榫€程2處的cpu修改了 initFlag 的值,會(huì) 馬上回寫 到主內(nèi)存中(經(jīng)過(guò) store、write兩步)。

5、線程1處的cpu 通過(guò) 總線嗅探機(jī)制 嗅探到變化,會(huì)將工作內(nèi)存中的數(shù)據(jù) 失效(initFlag=false失效)

6、線程1會(huì) 重新 去主內(nèi)存 read 最新的數(shù)據(jù)(此時(shí),主內(nèi)存中的數(shù)據(jù) initFlag=true)。

7、那么,線程1在讀取最新的數(shù)據(jù)時(shí),執(zhí)行引擎處,!initFlag = false,結(jié)束循環(huán),輸出 “success”。

? ?JMM對(duì)這八種操作規(guī)則和對(duì)volatile的一些特殊規(guī)則就能確定哪里操作是線程安全,哪些操作是線程不安全的了。但是這些規(guī)則實(shí)在復(fù)雜,很難在實(shí)踐中直接分析。所以一般我們也不會(huì)通過(guò)上述規(guī)則進(jìn)行分析。更多的時(shí)候,使用java的happen-before規(guī)則來(lái)進(jìn)行分析。

JMM三大特性

Java內(nèi)存模型是圍繞著并發(fā)編程中原子性、可見性、有序性這三個(gè)特征來(lái)建立的,那我們依次看一下這三個(gè)特征:

原子性

什么是原子性:一個(gè)操作不能被打斷,要么全部執(zhí)行完畢,要么不執(zhí)行。在這點(diǎn)上有點(diǎn)類似于事務(wù)操作,要么全部執(zhí)行成功,要么回退到執(zhí)行該操作之前的狀態(tài)。

為什么會(huì)有原子性問(wèn)題:因?yàn)镃PU 有時(shí)間片的概念,會(huì)根據(jù)不同的調(diào)度算法進(jìn)行線程調(diào)度。當(dāng)一個(gè)線程獲得時(shí)間片之后開始執(zhí)行,在時(shí)間片耗盡之后,就會(huì)失去 CPU 使用權(quán)。所以在多線程場(chǎng)景下,由于時(shí)間片在線程間輪換,就會(huì)發(fā)生原子性問(wèn)題。

舉個(gè)例子:你覺得num++是原子性操作嗎?看起來(lái)它就一行代碼,然而反編譯之后可以看到num++在內(nèi)存中操作也是分為了三步操作,那么多線程同時(shí)進(jìn)來(lái)就可能在某個(gè)步驟被線程的隨機(jī)調(diào)度打斷而產(chǎn)生的一系列的問(wèn)題。

a = true; ?//原子性
a = 5; ? ? //原子性
a = b; ? ? //非原子性,分兩步完成,第一步加載b的值,第二步將b賦值給a
a = b + 2; //非原子性,分三步完成
a ++; ? ? ?//非原子性,分三步完成:1、讀取a的值,2、計(jì)算a的值+1,3、賦值

?如何保證原子性:1、synchronized一定能保證原子性,因?yàn)楸黄湫揎椀哪扯未a,只能由一個(gè)線程執(zhí)行,所以一定可以保證原子操作。2、juc(java.util.concurrent包)中的lock包和atomic包,他們也可以解決原子性問(wèn)題.

可見性

什么是可見性:一個(gè)線程對(duì)共享變量做了修改之后,其他的線程立即能夠看到(感知到)該變量的這種修改(變化)。

為什么會(huì)有可見性問(wèn)題:根據(jù)JMM內(nèi)存模型,可以看到主內(nèi)存和線程工作內(nèi)存之間存在時(shí)間差(延遲)問(wèn)題。

由于線程對(duì)共享變量的操作都是線程拷貝到各自的工作內(nèi)存進(jìn)行操作后才寫回到主內(nèi)存中的,這就可能存在一個(gè)線程A修改了共享變量 i 的值,還未寫回主內(nèi)存時(shí),另外一個(gè)線程B又對(duì)主內(nèi)存中同一個(gè)共享變量 i 進(jìn)行操作,但此時(shí)A線程工作內(nèi)存中共享變量 i 對(duì)線程B來(lái)說(shuō)并不可見,這種工作內(nèi)存與主內(nèi)存同步延遲現(xiàn)象就造成了可見性問(wèn)題,另外指令重排以及編譯器優(yōu)化也可能導(dǎo)致可見性問(wèn)題,通過(guò)前面的分析,我們知道無(wú)論是編譯器優(yōu)化還是處理器優(yōu)化的重排現(xiàn)象,在多線程環(huán)境下,確實(shí)會(huì)導(dǎo)致程序輪序執(zhí)行的問(wèn)題,從而也就導(dǎo)致可見性問(wèn)題。

如何保證可見性:volatile的特殊規(guī)則保證了volatile變量值修改后的新值立刻同步到主內(nèi)存,每次使用volatile變量前立即從主內(nèi)存中刷新,因此volatile保證了多線程之間的操作變量的可見性,而普通變量則不能保證這一點(diǎn)。

除了volatile關(guān)鍵字能實(shí)現(xiàn)可見性之外,還有synchronized,Lock,final也是可以的。

使用Lock接口的最常用的實(shí)現(xiàn)ReentrantLock(重入鎖)來(lái)實(shí)現(xiàn)可見性:當(dāng)我們?cè)诜椒ǖ拈_始位置執(zhí)行l(wèi)ock.lock()方法,這和synchronized開始位置(Monitor Enter)有相同的語(yǔ)義,即使用共享變量時(shí)會(huì)從主內(nèi)存中刷新變量值到工作內(nèi)存中(即從主內(nèi)存中讀取最新值到線程私有的工作內(nèi)存中),在方法的最后finally塊里執(zhí)行l(wèi)ock.unlock()方法,和synchronized結(jié)束位置(Monitor Exit)有相同的語(yǔ)義,即會(huì)將工作內(nèi)存中的變量值同步到主內(nèi)存中去(即將線程私有的工作內(nèi)存中的值寫入到主內(nèi)存進(jìn)行同步)。

final關(guān)鍵字的可見性是指:被final修飾的變量,在構(gòu)造函數(shù)數(shù)一旦初始化完成,并且在構(gòu)造函數(shù)中并沒有把“this”的引用傳遞出去(“this”引用逃逸是很危險(xiǎn)的,其他的線程很可能通過(guò)該引用訪問(wèn)到只“初始化一半”的對(duì)象),那么其他線程就可以看到final變量的值。

有序性

什么是有序性:代碼按順序執(zhí)行

為什么會(huì)有有序性問(wèn)題:Java語(yǔ)言規(guī)定JVM線程內(nèi)部維持順序話語(yǔ)義,只要程序結(jié)果不受影響,那么執(zhí)行的指令是可以優(yōu)化的,可以和編寫的代碼順序不一致,這就是指令重排。指令重排可能發(fā)生在多個(gè)階段,例如Java源代碼編譯階段、內(nèi)存系統(tǒng)重排序等。但是指令重排有一個(gè)原則: as-if-seiral:不管怎么重排序,單線程的程序執(zhí)行結(jié)果不能夠被改變,編譯器、處理器等都得遵循這個(gè)規(guī)范和準(zhǔn)則。

如何保證有序性:Java提供了兩個(gè)關(guān)鍵字volatile和synchronized來(lái)保證多線程之間操作的有序性,volatile關(guān)鍵字本身通過(guò)加入內(nèi)存屏障來(lái)禁止指令的重排序,而synchronized關(guān)鍵字通過(guò)一個(gè)變量在同一時(shí)間只允許有一個(gè)線程對(duì)其進(jìn)行加鎖的規(guī)則來(lái)實(shí)現(xiàn)。

指令重排問(wèn)題

在執(zhí)行程序時(shí)為了提高性能,編譯器和處理器常常會(huì)對(duì)指令做重排序。重排序分三種類型:

1、編譯器優(yōu)化的重排序。編譯器在不改變單線程程序語(yǔ)義的前提下,可以重新安排語(yǔ)句的執(zhí)行順序。
2、指令并行的重排序。現(xiàn)代處理器采用了指令級(jí)并行技術(shù)(Instruction-Level Parallelism, ILP)來(lái)將多條指令重疊執(zhí)行。如果不存在數(shù)據(jù)依賴性,處理器可以改變語(yǔ)句對(duì)應(yīng)機(jī)器指令的執(zhí)行順序。
3、內(nèi)存系統(tǒng)的重排序。由于處理器使用緩存和讀/寫緩沖區(qū),這使得加載和存儲(chǔ)操作看上去可能是在亂序執(zhí)行。

從java源代碼到最終實(shí)際執(zhí)行的指令序列,會(huì)分別經(jīng)歷下面三種重排序:
上述的1屬于編譯器重排序,2和3屬于處理器重排序。這些重排序都可能會(huì)導(dǎo)致多線程程序出現(xiàn)內(nèi)存可見性問(wèn)題。對(duì)于編譯器,JMM的編譯器重排序規(guī)則會(huì)禁止特定類型的編譯器重排序(不是所有的編譯器重排序都要禁止)。

對(duì)于處理器重排序,JMM的處理器重排序規(guī)則會(huì)要求java編譯器在生成指令序列時(shí),插入特定類型的內(nèi)存屏障(memory barriers,intel稱之為memory fence)指令,通過(guò)內(nèi)存屏障指令來(lái)禁止特定類型的處理器重排序(不是所有的處理器重排序都要禁止)。

例如:volatile的有序性是通過(guò)禁止指令重排來(lái)實(shí)現(xiàn)的。為了性能,在JMM中,在不影響正確語(yǔ)義的情況下,允許編譯器和處理器對(duì)指令序列進(jìn)行重排序。而禁止指令重排底層是通過(guò)設(shè)置內(nèi)存屏障來(lái)實(shí)現(xiàn)。

處理器重排序與內(nèi)存屏障

現(xiàn)代的處理器(物理處理器即CPU)使用寫緩沖區(qū)來(lái)臨時(shí)保存向內(nèi)存寫入的數(shù)據(jù)。寫緩沖區(qū)可以保證指令流水線持續(xù)運(yùn)行,它可以避免由于處理器停頓下來(lái)等待向內(nèi)存寫入數(shù)據(jù)而產(chǎn)生的延遲。同時(shí),通過(guò)以批處理的方式刷新寫緩沖區(qū),以及合并寫緩沖區(qū)中對(duì)同一內(nèi)存地址的多次寫,可以減少對(duì)內(nèi)存總線的占用。雖然寫緩沖區(qū)有這么多好處,但每個(gè)處理器上的寫緩沖區(qū),僅僅對(duì)它所在的處理器可見。這個(gè)特性會(huì)對(duì)內(nèi)存操作的執(zhí)行順序產(chǎn)生重要的影響:處理器排序后對(duì)內(nèi)存的讀/寫操作的執(zhí)行順序,不一定與內(nèi)存實(shí)際發(fā)生的讀/寫操作順序一致!

常見處理器允許的重排序類型的列表:

?上表單元格中的“N”表示處理器不允許兩個(gè)操作重排序,“Y”表示允許重排序。

從上表我們可以看出:

  • 常見的處理器都允許Store-Load重排序;
  • 常見的處理器都不允許對(duì)存在數(shù)據(jù)依賴的操作做重排序。sparc-TSO和x86擁有相對(duì)較強(qiáng)的處理器內(nèi)存模型,它們僅允許對(duì)寫-讀操作做重排序(因?yàn)樗鼈兌际褂昧藢懢彌_區(qū))。

※注1:sparc-TSO是指以TSO(Total Store Order)內(nèi)存模型運(yùn)行時(shí),sparc處理器的特性。
※注2:上表中的x86包括x64及AMD64。
※注3:由于ARM處理器的內(nèi)存模型與PowerPC處理器的內(nèi)存模型非常類似,本文將忽略它。
※注4:數(shù)據(jù)依賴性后文會(huì)專門說(shuō)明。
?

為了保證內(nèi)存可見性,java編譯器在生成指令序列的適當(dāng)位置會(huì)插入內(nèi)存屏障指令來(lái)禁止特定類型的處理器重排序。JMM內(nèi)存屏障分為四類:

?StoreLoad Barriers是一個(gè)“全能型”的屏障,它同時(shí)具有其他三個(gè)屏障的效果。現(xiàn)代的多處理器大都支持該屏障(其他類型的屏障不一定被所有處理器支持)。執(zhí)行該屏障開銷會(huì)很昂貴,因?yàn)楫?dāng)前處理器通常要把寫緩沖區(qū)中的數(shù)據(jù)全部刷新到內(nèi)存中(buffer fully flush)。

數(shù)據(jù)依賴性

如果兩個(gè)操作訪問(wèn)同一個(gè)變量,且這兩個(gè)操作中有一個(gè)為寫操作,此時(shí)這兩個(gè)操作之間就存在數(shù)據(jù)依賴性。數(shù)據(jù)依賴分下列三種類型:

上面三種情況,只要重排序兩個(gè)操作的執(zhí)行順序,程序的執(zhí)行結(jié)果將會(huì)被改變。

前面提到過(guò),編譯器和處理器可能會(huì)對(duì)操作做重排序。但是,編譯器和處理器在重排序時(shí),會(huì)遵守?cái)?shù)據(jù)依賴性,編譯器和處理器不會(huì)改變存在數(shù)據(jù)依賴關(guān)系的兩個(gè)操作的執(zhí)行順序。
注意,這里所說(shuō)的數(shù)據(jù)依賴性僅針對(duì)單個(gè)處理器中執(zhí)行的指令序列和單個(gè)線程中執(zhí)行的操作,不同處理器之間和不同線程之間的數(shù)據(jù)依賴性不被編譯器和處理器考慮。
?

as-if-serial語(yǔ)義?

as-if-serial語(yǔ)義的意思指:不管怎么重排序(編譯器和處理器為了提高并行度),(單線程)程序的執(zhí)行結(jié)果不能被改變。編譯器,runtime 和處理器都必須遵守as-if-serial語(yǔ)義。

【例】

double pi = 3.14; //A double r = 1.0; //B double area = pi * r * r; //C

如上圖所示,A和C之間存在數(shù)據(jù)依賴關(guān)系,同時(shí)B和C之間也存在數(shù)據(jù)依賴關(guān)系。因此在最終執(zhí)行的指令序列中,C不能被重排序到A和B的前面(C排到A和B的前面,程序的結(jié)果將會(huì)被改變)。但A和B之間沒有數(shù)據(jù)依賴關(guān)系,編譯器和處理器可以重排序A和B之間的執(zhí)行順序。下圖是該程序的兩種執(zhí)行順序:

as-if-serial語(yǔ)義把單線程程序保護(hù)了起來(lái),遵守as-if-serial語(yǔ)義的編譯器,runtime 和處理器共同為編寫單線程程序的程序員創(chuàng)建了一個(gè)幻覺:單線程程序是按程序的順序來(lái)執(zhí)行的。as-if-serial語(yǔ)義使單線程程序員無(wú)需擔(dān)心重排序會(huì)干擾他們,也無(wú)需擔(dān)心內(nèi)存可見性問(wèn)題。
?

volatile

下面這段話摘自《深入理解Java虛擬機(jī)》:

“觀察加入volatile關(guān)鍵字和沒有加入volatile關(guān)鍵字時(shí)所生成的匯編代碼發(fā)現(xiàn),加入volatile關(guān)鍵字時(shí),會(huì)多出一個(gè)lock前綴指令”

lock前綴指令實(shí)際上相當(dāng)于一個(gè)內(nèi)存屏障(也成內(nèi)存柵欄),內(nèi)存屏障會(huì)提供3個(gè)功能:

1)它確保指令重排序時(shí)不會(huì)把其后面的指令排到內(nèi)存屏障之前的位置,也不會(huì)把前面的指令排到內(nèi)存屏障的后面;即在執(zhí)行到內(nèi)存屏障這句指令時(shí),在它前面的操作已經(jīng)全部完成;

2)它會(huì)強(qiáng)制將對(duì)緩存的修改操作立即寫入主存;

3)如果是寫操作,它會(huì)導(dǎo)致其他CPU中對(duì)應(yīng)的緩存行無(wú)效。

1、保證有序性(禁止指令重排)

Load指令(讀屏障):將內(nèi)存的數(shù)據(jù)拷貝到處理器緩存

Store指令(寫屏障):讓當(dāng)前線程寫入緩存的數(shù)據(jù)可以被其他線程看見

volatile的內(nèi)存屏障策略:

volatile寫之前插入StoreStore屏障;(規(guī)則a,防止重排)

volatile寫之后插入StoreLoad屏障;(規(guī)則c,保障可見性)

volatile讀之后插入LoadStore屏障;(規(guī)則b,防止重排)

volatile讀之后插入LoadLoad屏障;(規(guī)則b,防止重排)

LoadLoad Barriers
排隊(duì),當(dāng)?shù)谝粋€(gè)讀屏障指令讀取數(shù)據(jù)完畢之后,后一個(gè)讀屏障指令才能夠進(jìn)行加載讀取
(禁止讀和讀的重排序)
StoreStore Barriers
當(dāng)A寫屏障指令寫完之后且保證A的的寫入可以被其他處理器看見,再進(jìn)行B的寫入操作
(禁止寫與寫的重排序)
LoadStore Barriers
前一個(gè)讀屏障指令讀取完畢后,后一個(gè)寫屏障指令才會(huì)進(jìn)行寫入
(禁止讀和寫的重排序)
StoreLoad Barriers
全能屏障,同時(shí)具有前三個(gè)的類型的效果,但開銷較大。
先保證A的寫入會(huì)被其他處理器可見,才進(jìn)行B的讀取指令
(禁止寫和讀的重排序)

volatile:在每一個(gè)寫的volatile前后插入寫屏障,讀的volatile前后插入讀屏障。
寫,在每一次寫入之前屏障拿到其他的線程修改的數(shù)據(jù)(因?yàn)榭梢娦院椭嘏判?。寫入后的屏障可以被其他線程拿到最新的值。
讀,在每一個(gè)讀之前屏障獲取某個(gè)變量的值的時(shí)候,這個(gè)值可以被其他線程也獲取到。讀取后的屏障可以在其他線程修改之前獲取到主內(nèi)存變量的當(dāng)前值
?


?

2、保證可見性

volatile很好的保證了變量的可見性,變量經(jīng)過(guò)volatile修飾后,對(duì)此變量進(jìn)行寫操作時(shí),匯編指令中會(huì)有一個(gè)LOCK前綴指令,這個(gè)不需要過(guò)多了解,但是加了這個(gè)指令后,會(huì)引發(fā)兩件事情:

  • 將當(dāng)前處理器緩存行的數(shù)據(jù)寫回到系統(tǒng)內(nèi)存
  • 這個(gè)寫回內(nèi)存的操作會(huì)使得在其他處理器緩存了該內(nèi)存地址無(wú)效

volatile修飾的共享變量在執(zhí)行寫操作后,會(huì)立即刷回到主存,以供其它線程讀取到最新的記錄。

3、不保證原子性

volatile只有寫操作是原子性的,也就是數(shù)據(jù)操作完成后會(huì)立刻刷新到主內(nèi)存中。但是被volatile修飾的變量在讀的時(shí)候可能會(huì)被多個(gè)線程讀。也就是說(shuō)int i = 1;i++;
A線程讀 i = 1同時(shí)B線程也讀了i = 1,然后自增完成刷新入主內(nèi)存。i的值是2。

所以如果該變量是volatile修飾的,那可以完全保證此時(shí)取到的是最新信息。但在入棧和自增計(jì)算執(zhí)行過(guò)程中,該變量有可能正在被其他線程修改,最后計(jì)算出來(lái)的結(jié)果照樣存在問(wèn)題,因此volatile并不能保證非原子操作的原子性,僅在單次讀或者單次寫這樣的原子操作中,volatile能夠?qū)崿F(xiàn)線程安全。

CAS

在多線程編程時(shí),如果想保證一段代碼具有原子性,通過(guò)會(huì)使用鎖來(lái)解決,而CAS是通過(guò)硬件指令來(lái)達(dá)到比較并交換的過(guò)程;簡(jiǎn)單來(lái)說(shuō),CAS是操作系統(tǒng)層上的原子性操作。

CAS在java上的實(shí)現(xiàn)方式主要是JUC下的atomic原子類包


?

我們知道,Java是無(wú)法直接操作內(nèi)存的,而C++可以,C++沒有虛擬機(jī),因此在Java里面有native可以調(diào)用C++;

CAS操作原理

CAS包括三個(gè)值:
?V:內(nèi)存地址;
?A:期望值;
?B:新值;
?如果這個(gè)內(nèi)存地址V的值和期望值A(chǔ)相等,則將其賦值為B;?

例如:public final?int?getAndIncrement() 原子上增加一個(gè)當(dāng)前值。

?這里的原子類方法用了do while,無(wú)限循環(huán),其實(shí)就是一個(gè)標(biāo)準(zhǔn)的自旋鎖。

ABA問(wèn)題

CAS只是比較和交換,失敗就返回false 。但是原子類里面的方法會(huì)用自旋鎖,無(wú)限循環(huán),存在三個(gè)問(wèn)題:

1、循環(huán)會(huì)耗時(shí)

2、一次性只能保證一個(gè)共享變量的原子性

3、ABA問(wèn)題

什么是ABA問(wèn)題

舉個(gè)例子

如何解決ABA問(wèn)題

ABA解決思路還是使用樂觀鎖,版本號(hào),時(shí)間戳的思想。對(duì)于樂觀鎖不記得了,可以回顧這篇文章

Mysql—鎖:全面理解_JagTom的博客-CSDN博客

在atomic包里面有個(gè)類 AtomicStampedReference<V>就是使用了版本號(hào)的實(shí)現(xiàn)方式

這里有個(gè)注意事項(xiàng)?

這句代碼是會(huì)有問(wèn)題的:?

??

compareAndSet源碼,底層是用==進(jìn)行判斷?,也就是我們?cè)诜盒褪褂冒b類的時(shí)候要注意,Integer類型的范圍是-128~127,超出范圍會(huì)在堆里面新建一個(gè)對(duì)象并不會(huì)復(fù)用對(duì)象。

?阿里巴巴規(guī)范手冊(cè):

?AtomicStampedReference<V>源碼

public class AtomicStampedReference<V> {// 定義引用類型,包裝值和版本號(hào);private static class Pair<T> {final T reference;final int stamp;private Pair(T reference, int stamp) {this.reference = reference;this.stamp = stamp;}static <T> Pair<T> of(T reference, int stamp) {return new Pair<T>(reference, stamp);}}private volatile Pair<V> pair;// 比較并交換public boolean compareAndSet(V expectedReference,V newReference,int expectedStamp,int newStamp) {Pair<V> current = pair;return// 先做一次校驗(yàn),如果在這里都已經(jīng)不一致,則直接返回false,這里沒有加鎖,那么它可能會(huì)存在并發(fā);// 可能會(huì)有兩個(gè)線程同時(shí)進(jìn)來(lái),判斷并且都成立,則兩個(gè)線程都會(huì)進(jìn)入到:casPair方法;// Pair<V> current = pair; 多個(gè)線程進(jìn)入到compareAndSet方法時(shí),都已經(jīng)保留了當(dāng)前的pair值,那如果pair被其他線程修改,則另外一個(gè)線程去做cas的時(shí)候一定會(huì)返回false,所以這塊是通過(guò)這種方式來(lái)防止并發(fā)的;expectedReference == current.reference &&expectedStamp == current.stamp &&((newReference == current.reference &&newStamp == current.stamp) ||casPair(current, Pair.of(newReference, newStamp)));}private static final sun.misc.Unsafe UNSAFE = sun.misc.Unsafe.getUnsafe();private static final long pairOffset =objectFieldOffset(UNSAFE, "pair", AtomicStampedReference.class);private boolean casPair(Pair<V> cmp, Pair<V> val) {return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);}static long objectFieldOffset(sun.misc.Unsafe UNSAFE,String field, Class<?> klazz) {try {return UNSAFE.objectFieldOffset(klazz.getDeclaredField(field));} catch (NoSuchFieldException e) {// Convert Exception to corresponding ErrorNoSuchFieldError error = new NoSuchFieldError(field);error.initCause(e);throw error;}} }

各種鎖的理解

lock、synchronized默認(rèn)都是可重入鎖,非公平鎖

公平鎖和非公平鎖

公平鎖:非常公平,不能夠插隊(duì),必須先來(lái)后到!

非公平鎖:非常不公平,可以插隊(duì)(默認(rèn)都是非公平)

public ReentrantLock { sync = new Nonfairsync(); //默認(rèn)非公平}public ReentrantLock(boolean fair) { sync = fair ? new Fairsync() : new Nonfairsynco; //如果為true則設(shè)置為公平鎖 }

可重入鎖

? ? ? ? 解釋一:可重入就是說(shuō)某個(gè)線程已經(jīng)獲得某個(gè)鎖,可以再次獲取鎖而不會(huì)出現(xiàn)死鎖。

? ? ? ? 這是可重入鎖的概念描述。

? ? ? ? 解釋二:可重入鎖又稱遞歸鎖,是指同一個(gè)線程在外層方法獲取鎖的時(shí)候,再進(jìn)入該線程的內(nèi)層方法會(huì)自動(dòng)獲取鎖(前提是鎖對(duì)象得是同一個(gè)對(duì)象),不會(huì)因?yàn)橹耙呀?jīng)獲取過(guò)鎖還沒有釋放而阻塞。

? ? ? ? 這是可重入鎖的一種表現(xiàn)方式,不代表說(shuō)某段代碼中的鎖沒有發(fā)生嵌套,這個(gè)鎖就不是可重入鎖。

可重入鎖是某個(gè)線程已經(jīng)獲得某個(gè)鎖,可以再次獲取鎖而不會(huì)出現(xiàn)死鎖。再次獲取鎖的時(shí)候會(huì)判斷當(dāng)前線程是否是已經(jīng)加鎖的線程,如果是對(duì)鎖的次數(shù)+1,釋放鎖的時(shí)候加了幾次鎖,就需要釋放幾次鎖。

? ? ? ? 代碼中的鎖的遞歸只是鎖的一種表現(xiàn)及證明形式,除了這種形式外,還有另一種表現(xiàn)形式。同一個(gè)線程在沒有釋放鎖的情況下多次調(diào)用一個(gè)加鎖方法,如果成功,則也說(shuō)明是可重入鎖。

自旋鎖?

Java-concurrency/Java內(nèi)存模型以及happens-before.md at master · three-body-zhangbeihai/Java-concurrency · GitHub

總結(jié)

以上是生活随笔為你收集整理的Java内存模型—JMM详解的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。