深入了解volatile和Java內(nèi)存模型
前言
在本篇文章當(dāng)中,主要給大家深入介紹Volatile關(guān)鍵字和Java內(nèi)存模型。在文章當(dāng)中首先先介紹volatile的作用和Java內(nèi)存模型,然后層層遞進(jìn)介紹實(shí)現(xiàn)這些的具體原理、JVM底層是如何實(shí)現(xiàn)volatile的和JVM實(shí)現(xiàn)的匯編代碼以及CPU內(nèi)部結(jié)構(gòu),深入剖析各種計(jì)算機(jī)系統(tǒng)底層原理。本篇文章超級(jí)干,請(qǐng)大家坐穩(wěn)扶好,發(fā)車了?。?!本文的大致框架如下圖所示:
為什么我們需要volatile?
保證數(shù)據(jù)的可見性
假如現(xiàn)在有兩個(gè)線程分別執(zhí)行不同的代碼,但是他們有同一個(gè)共享變量flag
,其中線程updater
會(huì)執(zhí)行的代碼是將flag
從false
修改成true
,而另外一個(gè)線程reader
會(huì)進(jìn)行while
循環(huán),當(dāng)flag
為true
的時(shí)候跳出循環(huán),代碼如下:
import java.util.concurrent.TimeUnit; class Resource { public boolean flag; public void update() { flag = true; } } public class Visibility { public static void main(String[] args) throws InterruptedException { Resource resource = new Resource(); Thread thread = new Thread(() -> { System.out.println(resource.flag); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } resource.update(); }, "updater"); new Thread(() -> { System.out.println(resource.flag); while (!resource.flag) { } System.out.println("循環(huán)結(jié)束"); }, "reader").start(); thread.start(); } }
運(yùn)行上面的代碼你會(huì)發(fā)現(xiàn),reader
線程始終打印不出循環(huán)結(jié)束
,也就是說它一直在進(jìn)行while
循環(huán),而進(jìn)行while
循環(huán)的原因就是resouce.flag=false
,但是線程updater
在經(jīng)過1秒之后會(huì)進(jìn)行更新啊!為什么reader
線程還讀取不到呢?
這實(shí)際上就是一種可見性的問題,updater
線程更新數(shù)據(jù)之后,reader
線程看不到,在分析這個(gè)問題之間我們首先先來了解一下Java內(nèi)存模型的邏輯布局:
在上面的代碼執(zhí)行順序大致如下:
reader
線程從主內(nèi)存當(dāng)中拿到flag
變量并且存儲(chǔ)到線程的本地內(nèi)存當(dāng)中,進(jìn)行while
循環(huán)。- 在休眠一秒之后,
Updater
線程從主內(nèi)存當(dāng)中拷貝一份flag
保存到本地內(nèi)存當(dāng)中,然后將flag
改成true
,將其寫回到主內(nèi)存當(dāng)中。 - 但是雖然
updater
線程將flag
寫回,但是reader
線程使用的還是之前從主內(nèi)存當(dāng)中加載到工作內(nèi)存的flag
,也就是說還是false
,因此reader
線程才會(huì)一直陷入死循環(huán)當(dāng)中。
現(xiàn)在我們稍微修改一下上面的代碼,先讓reader
線程休眠一秒,然后再進(jìn)行while
循環(huán),讓updater
線程直接修改。
import java.util.concurrent.TimeUnit; class Resource { public boolean flag; public void update() { flag = true; } } public class Visibility { public static void main(String[] args) throws InterruptedException { Resource resource = new Resource(); Thread thread = new Thread(() -> { System.out.println(resource.flag); resource.update(); }, "updater"); new Thread(() -> { System.out.println(resource.flag); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } while (!resource.flag) { } System.out.println("循環(huán)結(jié)束"); }, "reader").start(); thread.start(); } }
上面的代碼就不會(huì)產(chǎn)生死循環(huán)了,我們?cè)賮矸治鲆幌律厦娴拇a的執(zhí)行過程:
reader
線程先休眠一秒。updater
線程直接修改flag
為true
,然后將這個(gè)值寫回主內(nèi)存。- 在
updater
寫回之后,reader
線程從主內(nèi)存獲取flag
,這個(gè)時(shí)候的值已經(jīng)更新了,因此可以跳出while
循環(huán)了,因此上面的代碼不會(huì)出現(xiàn)死循環(huán)的情況。
像這種多個(gè)線程共享同一個(gè)變量的情況的時(shí)候,就會(huì)產(chǎn)生數(shù)據(jù)可見性的問題,如果在我們的程序當(dāng)中忽略這種問題的話,很容易讓我們的并發(fā)程序產(chǎn)生BUG。如果在我們的程序當(dāng)中需要保持多個(gè)線程對(duì)某一個(gè)數(shù)據(jù)的可見性,即如果一個(gè)線程修改了共享變量,那么這個(gè)修改的結(jié)果要對(duì)其他線程可見,也就是其他線程再次訪問這個(gè)共享變量的時(shí)候,得到的是共享變量最新的值,那么在Java當(dāng)中就需要使用關(guān)鍵字volatile
對(duì)變量進(jìn)行修飾。
現(xiàn)在我們將第一個(gè)程序的共享變量flag
加上volatile
進(jìn)行修飾:
import java.util.concurrent.TimeUnit; class Resource { public volatile boolean flag; // 這里使用 volatile 進(jìn)行修飾 public void update() { flag = true; } } public class Visibility { public static void main(String[] args) throws InterruptedException { Resource resource = new Resource(); Thread thread = new Thread(() -> { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(resource.flag); resource.update(); }, "updater"); new Thread(() -> { System.out.println(resource.flag); while (!resource.flag) { } System.out.println("循環(huán)結(jié)束"); }, "reader").start(); thread.start(); } }
上面的代碼是可以執(zhí)行完成的,reader
線程不會(huì)產(chǎn)生死循環(huán),因?yàn)?code>volatile保證了數(shù)據(jù)的可見性。即每一個(gè)線程對(duì)volatile
修飾的變量的修改,對(duì)其他的線程是可見的,只要有線程修改了值,那么其他線程就可以發(fā)現(xiàn)。
禁止指令重排序
指令重排序介紹
首先我們需要了解一下什么是指令重排序:
int a = 0; int b = 1; int c = 1; a++; b--;
比如對(duì)于上面的代碼我們正常的執(zhí)行流程是:
- 定義一個(gè)變量
a
,并且賦值為0。 - 定義一個(gè)變量
b
,并且賦值為1。 - 定義一個(gè)變量
c
,并且賦值為1。 - 變量
a
進(jìn)行自增操作。 - 變量
b
進(jìn)行自減操作。
而當(dāng)編譯器去編譯上面的程序時(shí),可能不是安裝上面的流程一步步進(jìn)行操作的,編譯器可能在編譯優(yōu)化之后進(jìn)行如下操作:
- 定義一個(gè)變量
c
,并且賦值為1。 - 定義一個(gè)變量
a
,并且賦值為1。 - 定義一個(gè)變量
b
,并且賦值為0。
從上面來看代碼的最終結(jié)果是沒有發(fā)生變化的,但是指令執(zhí)行的流程和指令的數(shù)目是發(fā)生變化的,編譯器幫助我們省略了一些操作,這可以讓CPU執(zhí)行更少的指令,加快程序的執(zhí)行速度。
上面就是一個(gè)比較簡(jiǎn)單的在編譯優(yōu)化當(dāng)中指令重排和優(yōu)化的例子。
但是如果我們?cè)谡Z句int c = 1
前面加上volatile
時(shí),上面的代碼執(zhí)行順序就會(huì)保證a
和b
的定義在語句volatile int c = 1;
之前,變量a
和變量b
的操作在語句volatile int c = 1;
之后。
int a = 0; int b = 1; volatile int c = 1; a++; b--;
但是volatile
并不限制到底是a
先定義還是b
先定義,它只保證這兩個(gè)變量的定義發(fā)生在用volatile
修飾的語句之前。
volatile
關(guān)鍵字會(huì)禁止JVM和處理器(CPU)對(duì)含有volatile
關(guān)鍵字修飾的變量的指令進(jìn)行重排序,但是對(duì)于volatile
前后沒有依賴關(guān)系的指令沒有禁止,也就是說編譯器只需要保證編譯之后的代碼的順序語義和正常的邏輯一樣,它可以盡可能的對(duì)代碼進(jìn)行編譯優(yōu)化和重排序!
Volatile禁止重排序使用——雙重檢查單例模式
在單例模式當(dāng)中,有一種單例模式的寫法就雙重檢查單例模式,其代碼如下:
public class DCL { // 這里沒有使用 volatile 進(jìn)行修飾 public static DCL INSTANCE; public static DCL getInstance() { // 如果單例還沒有生成 if (null == INSTANCE) { // 進(jìn)入同步代碼塊 synchronized (DCL.class) { // 因?yàn)槿绻麅蓚€(gè)線程同時(shí)進(jìn)入上一個(gè) if 語句 // 的話,那么第一個(gè)線程會(huì) new 一個(gè)對(duì)象 // 第二個(gè)線程也會(huì)進(jìn)入這個(gè)代碼塊,因此需要重新 // 判斷是否為 null 如果不判斷的話 第二個(gè)線程 // 也會(huì) new 一個(gè)對(duì)象,那么就破壞單例模式了 if (null == INSTANCE) { INSTANCE = new DCL(); } } } return INSTANCE; } }
上面的代碼當(dāng)中INSTANCE
是沒有使用volatile
進(jìn)行修飾的,這會(huì)導(dǎo)致上面的代碼存在問題。在分析這其中的問題之前,我們首先需要明白,在Java當(dāng)中new一個(gè)對(duì)象會(huì)經(jīng)歷以下三步:
- 步驟1:申請(qǐng)對(duì)象所需要的內(nèi)存空間。
- 步驟2:在對(duì)應(yīng)的內(nèi)存空間當(dāng)中,對(duì)對(duì)象進(jìn)行初始化。
- 步驟3:對(duì)INSTANCE進(jìn)行賦值。
但是因?yàn)樽兞縄NSTANCE沒有使用volatile
進(jìn)行修飾,就可能存在指令重排序,上面的三個(gè)步驟的執(zhí)行順序變成:
- 步驟1。
- 步驟3。
- 步驟2。
假設(shè)一個(gè)線程的執(zhí)行順序就是上面提到的那樣,如果線程在執(zhí)行完成步驟3之后在執(zhí)行完步驟2之前,另外一個(gè)線程進(jìn)入getInstance
,這個(gè)時(shí)候INSTANCE != null
,因此這個(gè)線程會(huì)直接返回這個(gè)對(duì)象進(jìn)行使用,但是此時(shí)第一個(gè)線程還在執(zhí)行步驟2,也就是說對(duì)象還沒有初始化完成,這個(gè)時(shí)候使用對(duì)象是不合法的,因此上面的代碼存在問題,而當(dāng)我們使用volatile
進(jìn)行修飾就可以禁止這種重排序,從而讓他按照正常的指令去執(zhí)行。
不保證原子性
原子性:一個(gè)操作要么不做要么全做,而且在做這個(gè)操作的時(shí)候其他線程不能夠插入破壞這個(gè)操作的完整性
public class AtomicTest { public static volatile int data; public static void add() { for (int i = 0; i < 10000; i++) { data++; } } public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(AtomicTest::add); Thread t2 = new Thread(AtomicTest::add); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(data); } }
上面的代碼就是兩個(gè)線程不斷的進(jìn)行data++
操作,一共會(huì)進(jìn)行20000次,但是我們會(huì)發(fā)現(xiàn)最終的結(jié)果不等于20000,因此這個(gè)可以驗(yàn)證volatile
不保證原子性,如果volatile
能夠保證原子性,那么出現(xiàn)的結(jié)果會(huì)等于20000。
Java內(nèi)存模型(JMM)
JMM下的內(nèi)存邏輯結(jié)構(gòu)
我們都知道Java程序可以跨平臺(tái)運(yùn)行,之所以可以跨平臺(tái),是因?yàn)镴VM幫助我們屏蔽了這些不同的平臺(tái)和操作系統(tǒng)的差異,而內(nèi)存模型也是一樣,各個(gè)平臺(tái)是不一樣的,Java為了保證程序可以跨平臺(tái)使用,Java虛擬機(jī)規(guī)范就定義了“Java內(nèi)存模型”,規(guī)定Java應(yīng)該如何并發(fā)的訪問內(nèi)存,每一個(gè)平臺(tái)實(shí)現(xiàn)的JVM都需要遵循這個(gè)規(guī)則,這樣就可以保證程序在不同的平臺(tái)執(zhí)行的結(jié)果都是一樣的。
下圖當(dāng)中的綠色部分就是由JMM進(jìn)行控制的
JMM對(duì)Java線程和線程的工作內(nèi)存還有主內(nèi)存的規(guī)定如下:
- 共享變量存儲(chǔ)在主內(nèi)存當(dāng)中,每個(gè)線程都可以進(jìn)行訪問。
- 每個(gè)線程都有自己的工作內(nèi)存,叫做線程的本地內(nèi)存。
- 線程如果想操作共享內(nèi)存必須首先將共享變量拷貝一份到自己的本地內(nèi)存。
- 線程不能直接對(duì)主內(nèi)存當(dāng)中的數(shù)據(jù)進(jìn)行修改,只能直接修改自己本地內(nèi)存當(dāng)中的數(shù)據(jù),然后通過JMM的控制,將修改后的值寫回到主內(nèi)存當(dāng)中。
這里區(qū)分一下主內(nèi)存和工作內(nèi)存(線程本地內(nèi)存):
- 主內(nèi)存:主要是Java堆當(dāng)中的對(duì)象數(shù)據(jù)。
- 工作內(nèi)存:Java虛擬機(jī)棧中存儲(chǔ)數(shù)據(jù)的某些區(qū)域、CPU的緩存(Cache)和寄存器。
因此線程、線程的工作內(nèi)存和主內(nèi)存的交互方式的邏輯結(jié)構(gòu)大致如下圖所示:
內(nèi)存交互的操作
JMM規(guī)定了線程的工作內(nèi)存應(yīng)該如何和主內(nèi)存進(jìn)行交互,即共享變量如何從內(nèi)存拷貝到工作內(nèi)存、工作內(nèi)存如何同步回主內(nèi)存,為了實(shí)現(xiàn)這些操作,JMM定義了下面8個(gè)操作,而且這8個(gè)操作都是原子的、不可再分的,如果下面的操作不是原子的話,程序的執(zhí)行就會(huì)出錯(cuò),比如說在鎖定的時(shí)候不是原子的,那么很可能出現(xiàn)兩個(gè)線程同時(shí)鎖定一個(gè)變量的情況,這顯然是不對(duì)的??!
- lock(鎖定):作用于主內(nèi)存的變量,它把一個(gè)變量標(biāo)識(shí)為一條線程獨(dú)占的狀態(tài)。
- unlock(解鎖):作用于主內(nèi)存的變量,它把一個(gè)處于鎖定狀態(tài)的變量釋放出來,釋放后的變量才可以被其他線程鎖定。
- read(讀?。鹤饔糜谥鲀?nèi)存的變量,它把一個(gè)變量的值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存中,以便隨后的load動(dòng)作使用。
- load(載入):作用于工作內(nèi)存的變量,它把read操作從主內(nèi)存中得到的變量值放入工作內(nèi)存的變量副本中。
- use(使用):作用于工作內(nèi)存的變量,它把工作內(nèi)存中一個(gè)變量的值傳遞給執(zhí)行引擎,每當(dāng)虛擬機(jī)遇到一個(gè)需要使用變量的值的字節(jié)碼指令時(shí)將會(huì)執(zhí)行這個(gè)操作。
- assign(賦值):作用于工作內(nèi)存的變量,它把一個(gè)從執(zhí)行引擎接收的值賦給工作內(nèi)存的變量,每當(dāng)虛擬機(jī)遇到一個(gè)給變量賦值的字節(jié)碼指令時(shí)執(zhí)行這個(gè)操作。
- store(存儲(chǔ)):作用于工作內(nèi)存的變量,它把工作內(nèi)存中一個(gè)變量的值傳送到主內(nèi)存中,以便隨后的write操作使用。
- write(寫入):作用于主內(nèi)存的變量,它把store操作從工作內(nèi)存中得到的變量的值放入主內(nèi)存的變量中。
如果需要將主內(nèi)存的變量拷貝到工作內(nèi)存,就需要順序執(zhí)行read
和load
操作,如果需要將工作內(nèi)存的值更新回主內(nèi)存,就需要順序執(zhí)行store
和writer
操作。
JMM定義了上述8條規(guī)則,但是在使用這8條規(guī)則的時(shí)候,還需要遵循下面的規(guī)則:
- 不允許read和load、store和write操作之一單獨(dú)出現(xiàn),即不允許一個(gè)變量從主內(nèi)存讀取了但工作內(nèi)存不接受,或者工作內(nèi)存發(fā)起回寫了但主內(nèi)存不接受的情況出現(xiàn)。
- 不允許一個(gè)線程丟棄它最近的assign操作,即變量在工作內(nèi)存中改變了之后必須把該變化同步回主內(nèi)存。不允許一個(gè)線程無原因地(沒有發(fā)生過任何assign操作)把數(shù)據(jù)從線程的工作內(nèi)存同步回主內(nèi)存 中。·
- 一個(gè)新的變量只能在主內(nèi)存中“誕生”,不允許在工作內(nèi)存中直接使用一個(gè)未被初始化(load或assign)的變量,換句話說就是對(duì)一個(gè)變量實(shí)施use、store操作之前,必須先執(zhí)行assign和load操作。
- 一個(gè)變量在同一個(gè)時(shí)刻只允許一條線程對(duì)其進(jìn)行l(wèi)ock操作,但lock操作可以被同一條線程重復(fù)執(zhí)行多次,多次執(zhí)行l(wèi)ock后,只有執(zhí)行相同次數(shù)的unlock操作,變量才會(huì)被解鎖。
- 如果對(duì)一個(gè)變量執(zhí)行l(wèi)ock操作,那將會(huì)清空工作內(nèi)存中此變量的值,在執(zhí)行引擎使用這個(gè)變量前,需要重新執(zhí)行l(wèi)oad或assign操作以初始化變量的值。
- 如果一個(gè)變量事先沒有被lock操作鎖定,那就不允許對(duì)它執(zhí)行unlock操作,也不允許去unlock一個(gè)被其他線程鎖定的變量。
- 對(duì)一個(gè)變量執(zhí)行unlock操作之前,必須先把此變量同步回主內(nèi)存中(執(zhí)行store、write操作)。
重排序
重排序介紹
我們?cè)谏衔漠?dāng)中已經(jīng)談到了,編譯器為了更好的優(yōu)化程序的性能,會(huì)對(duì)程序進(jìn)行進(jìn)行編譯優(yōu)化,在優(yōu)化的過程當(dāng)中可能會(huì)對(duì)指令進(jìn)行重排序。我們這里談到的編譯器是JIT(即時(shí)編譯器)。它JVM當(dāng)中的一個(gè)組件,它可以通過分析Java程序當(dāng)中的熱點(diǎn)代碼(經(jīng)常執(zhí)行的代碼),然后會(huì)對(duì)這段代碼進(jìn)行分析然后進(jìn)行編譯優(yōu)化,將其直接編譯成機(jī)器代碼,也就是CPU能夠直接執(zhí)行的機(jī)器碼,然后用這段代碼代替字節(jié)碼,通過這種方式來優(yōu)化程序的性能,讓程序執(zhí)行的更快。
重排序通常有以下幾種重排序方式:
- JIT編譯器對(duì)字節(jié)碼進(jìn)行優(yōu)化重排序生成機(jī)器指令。
- CPU在執(zhí)行指令的時(shí)候,CPU會(huì)在保證指令執(zhí)行時(shí)的語義不發(fā)生變化的情況下(與單線程執(zhí)行的結(jié)果相同),可以通過調(diào)整指令之間的順序,讓指令并行執(zhí)行,加快指令執(zhí)行的速度。
- 還有一種不是顯式的重排序方式,這種方式就是內(nèi)存系統(tǒng)的重排序。這是由于處理器使用緩存和讀/寫緩沖區(qū),這使得加載和存儲(chǔ)操作看上去可能是在亂序執(zhí)行。這并不是顯式的將指令進(jìn)行重排序,只是因?yàn)榫彺娴脑?,讓指令的?zhí)行看起來像亂序。
as-if-serial規(guī)則
as-if-serial語義的意思是:不管怎么重排序(編譯器和處理器為了提高并行度),(單線程)程序的執(zhí)行結(jié)果不能被改變。編譯器、處理器都必須遵守as-if-serial語義,因?yàn)槿绻B這都不遵守,在單線程下執(zhí)行的結(jié)果都不正確,那我們寫的程序執(zhí)行的結(jié)果都不是我們想要的,這顯然是不正確的。
1. int a = 1; 2. int b = 2; 3. int c = a + b;
比如上面三條語句,編譯器和處理器可以對(duì)第一條和第二條語句進(jìn)行重排序,但是必須保證第三條語句必須執(zhí)行在第一和第二條語句之后,因?yàn)榈谌龡l語句依賴于第一和第二條語句,重排序必須保證這種存在數(shù)據(jù)依賴關(guān)系的語句在重排序之后執(zhí)行的結(jié)果和順序執(zhí)行的結(jié)果是一樣的。
happer-before規(guī)則
重排序除了需要遵循as-if-serial規(guī)則,還需要遵循下面幾條規(guī)則,也就是說不管是編譯優(yōu)化還是處理器重排序必須遵循下面的原則:
- 程序順序原則 :線程當(dāng)中的每一個(gè)操作,happen-before線程當(dāng)中的后續(xù)操作。
- 鎖規(guī)則 :解鎖(unlock)操作必然發(fā)生在后續(xù)的同一個(gè)鎖的加鎖(lock)之前。
- volatile規(guī)則 :volatile變量的寫,先發(fā)生于讀。
- 線程啟動(dòng)規(guī)則 :線程的start()方法,happen-before它的每一個(gè)后續(xù)操作。
- 線程終止規(guī)則 :線程的所有操作先于線程的終結(jié),Thread.join()方法的作用是等待 當(dāng)前執(zhí)行的線程終止。假設(shè)在線程B終止之前,修改了共享變量,線程A從線程B的 join方法成功返回后,線程B對(duì)共享變量的修改將對(duì)線程A可見。
- 線程中斷規(guī)則 :對(duì)線程 interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測(cè)到中斷事件的發(fā)生,可以通Thread.interrupted()方法檢測(cè)線程是否中斷。
- 對(duì)象終結(jié)規(guī)則 :對(duì)象的構(gòu)造函數(shù)執(zhí)行,需要先于finalize()方法的執(zhí)行。
- 傳遞性 :A先于B ,B先于C 那么A必然先于C。
總而言之,重排序必須遵循下面兩條基本規(guī)則:
- 對(duì)于會(huì)改變程序執(zhí)行結(jié)果的重排序,JMM要求編譯器和處理器必須禁止這種重排序。
- 對(duì)于不會(huì)改變程序執(zhí)行結(jié)果的重排序,JMM對(duì)編譯器和處理器不做要求(JMM允許這種重排序)。
Volatile重排序規(guī)則
下表是JMM為了實(shí)現(xiàn)volatile的內(nèi)存語義制定的volatile重排序規(guī)則,列表示第一個(gè)操作,行表示第二個(gè)操作:
是否可以重排序 | 第二個(gè)操作 | 第二個(gè)操作 | 第二個(gè)操作 |
---|---|---|---|
第一個(gè)操作 | 普通讀/寫 | volatile讀 | volatile寫 |
普通讀/寫 | Yes | Yes | No |
volatile讀 | No | No | No |
volatile寫 | Yes | No | No |
說明:
- 比如在上表當(dāng)中說明,當(dāng)?shù)诙€(gè)操作是volatile寫的時(shí)候,那么這個(gè)指令不能和前面的普通讀寫和volatile讀寫進(jìn)行重排序。
- 當(dāng)?shù)谝粋€(gè)操作是volatile讀的時(shí)候,這個(gè)指令不能和后面的普通讀寫和volatile讀寫重排序。
Volatile實(shí)現(xiàn)原理
禁止重排序?qū)崿F(xiàn)原理
內(nèi)存屏障
在了解禁止重排序如何實(shí)現(xiàn)的之前,我們首先需要了解一下內(nèi)存屏障。所謂內(nèi)存屏障就是為了保證內(nèi)存的可見性而設(shè)計(jì)的,因?yàn)橹嘏判虻拇嬖诳赡軙?huì)造成內(nèi)存的不可見,因此Java編譯器(JIT編譯器)在生成指令的時(shí)候?yàn)榱私怪噶钪嘏判蚓蜁?huì)在生成的指令當(dāng)中插入一些內(nèi)存屏障指令,禁止指令重排序,從而保證內(nèi)存的可見性。
屏障類型 | 指令例子 | 解釋 |
---|---|---|
LoadLoad Barrier | Load1;LoadLoad;Load2 | 確保Load1數(shù)據(jù)的加載先于Load2和后面的Load指令 |
StoreStore Barrier | Store1;StoreStore;Store2 | 確保Store1操作的數(shù)據(jù)對(duì)其他處理器可見(將Cache刷新到內(nèi)存),即這個(gè)指令的執(zhí)行要先于Store2和后面的存儲(chǔ)指令 |
LoadStore Barrier | Load1;LoadStore;Store2 | 確保Load1數(shù)據(jù)加載先于Store2以及后面所有存儲(chǔ)指令 |
StoreLoad Barrier | Store1;StoreLoad;Load2 | 確保Store1數(shù)據(jù)對(duì)其他處理器可見,也就是將這個(gè)數(shù)據(jù)從CPU的Cache刷新到內(nèi)存當(dāng)中,這個(gè)內(nèi)存屏障會(huì)讓StoreLoad前面的所有的內(nèi)存訪問指令(不管是Store還是Load)全部完成之后,才執(zhí)行Store Load后面的Load指令 |
X86當(dāng)中內(nèi)存屏障指令
現(xiàn)在處理器一般可能不會(huì)支持上面屏障指令當(dāng)中的所有指令,但是一般都會(huì)支持Store Load屏障指令,因?yàn)檫@個(gè)指令可以達(dá)到其他三個(gè)指令的效果,因此在實(shí)際的機(jī)器指令當(dāng)中如果想達(dá)到上面的四種指令的效果,可能不需要四個(gè)指令,像在X86當(dāng)中就主要有三個(gè)內(nèi)存屏障指令:
lfence
,這是一種Load Barrier,一種讀屏障指令,這個(gè)指令可以讓高速緩存(CPU的Cache)失效,如果需要加載數(shù)據(jù),那么就需要從內(nèi)存當(dāng)中重新加載(這樣可以加載最新的數(shù)據(jù),因?yàn)槿绻渌幚砥餍薷牧司彺娈?dāng)中的數(shù)據(jù)的時(shí)候,這個(gè)緩存當(dāng)中的值已經(jīng)不對(duì)了,去內(nèi)存當(dāng)中重新加載就可以拿到最新的數(shù)據(jù)),這個(gè)指令其實(shí)可以達(dá)到上面指令當(dāng)中LoadLoad和指令的效果。同時(shí)這條指令不會(huì)讓這條指令之后讀操作被調(diào)度到lfence
指令之前執(zhí)行。sfence
,這是一種Store Barrier,一種寫屏障指令,這個(gè)指令可以將寫入高速緩存的數(shù)據(jù)刷新到內(nèi)存當(dāng)中,這樣內(nèi)存當(dāng)中的數(shù)據(jù)就是最新的了,數(shù)據(jù)就可以全局可見了,其他處理器就可以加載內(nèi)存當(dāng)中最新的數(shù)據(jù)。這條指令有StoreStore的效果。同時(shí)這條指令不會(huì)讓在其之后的寫操作調(diào)度到其之前執(zhí)行。- 關(guān)于以上兩點(diǎn)的描述是稍微有點(diǎn)不夠準(zhǔn)確的,在下文我們?cè)谟懻揝tore Buffer和Invalid Queue時(shí)我們會(huì)重新修正,這里這么寫是為了能夠幫助大家理解。
mfence
,這是一種全能型的屏障,相當(dāng)于上面lfence
和sfence
兩個(gè)指令的效果,除此之外這條指令可以達(dá)到StoreLoad指令的效果,這條指令可以保證mfence
操作之前的寫操作對(duì)mfence
之后的操作全局可見。
Volatile需要的內(nèi)存屏障
為了實(shí)現(xiàn)Volatile的內(nèi)存語義,Java編譯器(JIT編譯器)在進(jìn)行編譯的時(shí)候,會(huì)進(jìn)行如下指令的插入操作(這里你可以對(duì)照前面的volatile重排序規(guī)則,然后你就理解為什么要插入下面的內(nèi)存屏障了):
- 在每個(gè)volatile寫操作的前面插入一個(gè)StoreStore屏障。
- 在每個(gè)volatile寫操作的后面插入一個(gè)StoreLoad屏障。
- 在每個(gè)volatile讀操作的后面插入一個(gè)LoadLoad屏障。
- 在每個(gè)volatile讀操作的后面插入一個(gè)LoadStore屏障。
Volatile讀內(nèi)存屏障指令插入情況如下:
Volatile寫內(nèi)存屏障指令插入情況如下:
其實(shí)上面插入內(nèi)存屏障只是理論上所需要的,但是因?yàn)椴煌奶幚砥髦嘏判虻囊?guī)則不一樣,因此在插入內(nèi)存屏障指令的時(shí)候需要具體問題具體分析。比如X86處理器只會(huì)對(duì)讀-寫這樣的操作進(jìn)行重排序,不會(huì)對(duì)讀-讀、讀-寫和寫-寫這樣的操作進(jìn)行重排序,因此在X86處理器進(jìn)行內(nèi)存屏障指令的插入的時(shí)候可以省略這三種情況。
根據(jù)volatile重排序的規(guī)則表,我們可以發(fā)現(xiàn)在寫-讀的情況下,只禁止了volatile寫-volatile讀
的情況:
而X86僅僅只會(huì)對(duì)寫-讀的情況進(jìn)行重排序,因此我們?cè)诓迦雰?nèi)存屏障的時(shí)候只需要關(guān)心volatile寫-volatile讀
這一種情況,這種情況下我們需要使用的內(nèi)存屏障指令為StoreLoad,即volatile寫-StoreLoad-volatile讀
,因此在X86當(dāng)中我們只需要在volatile寫后面加入StoreLoad內(nèi)存屏障指令即可,在X86當(dāng)中Store Load對(duì)應(yīng)的具體的指令為mfence
。
Java虛擬機(jī)源碼實(shí)現(xiàn)Volatile語義
在Java虛擬機(jī)當(dāng)中,當(dāng)對(duì)一個(gè)被volatile修飾的變量進(jìn)行寫操作的時(shí)候,在操作進(jìn)行完成之后,在X86體系結(jié)構(gòu)下,JVM會(huì)執(zhí)行下面一段代碼,從而保證volatile的內(nèi)存語義:(下面代碼來自于:hotspot/src/os_cpu/linux_x86/vm/orderAccess_linux_x86.inline.hpp)
inline void OrderAccess::fence() { // 這里判斷是不是多處理器的機(jī)器,如果是執(zhí)行下面的代碼 if (os::is_MP()) { // 這里說明了使用 lock 指令的原因 有時(shí)候使用 mfence 代價(jià)很高 // 相比起 lock 指令來說會(huì)降低程序的性能 // always use locked addl since mfence is sometimes expensive #ifdef AMD64 // 這個(gè)表示如果是 64 位機(jī)器 __asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory"); #else // 如果不是64位機(jī)器 s // 32位和64位主要區(qū)別就是 寄存器不同 在64 位當(dāng)中是 rsp 在32位機(jī)器當(dāng)中是 esp __asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory"); #endif } }
上面代碼主要是通過內(nèi)聯(lián)匯編代碼去執(zhí)行指令lock
,如果你不熟悉C語言和內(nèi)聯(lián)匯編的形式也沒有關(guān)系,你只需要知道JVM會(huì)執(zhí)行lock
指令,lock
指令有mfence
相同的作用,它可以實(shí)現(xiàn)StoreLoad內(nèi)存屏障的作用,可以保證執(zhí)行執(zhí)行的順序,在前文當(dāng)中我們說mfence
是用于實(shí)現(xiàn)StoreLoad內(nèi)存屏障,因?yàn)?code>lock指令也可以實(shí)現(xiàn)同樣的效果,而且有時(shí)候mfence
的指令可能對(duì)程序的性能影響比較大,因此JVM使用lock
指令,這樣可以提高程序的性能。如果你對(duì)X86的lock
指令有所了解的話,你可能知道lock
還可以保證使用lock
的指令具有原子性,在X86的體系結(jié)構(gòu)下就可以使用lock
實(shí)現(xiàn)自旋鎖(CAS)。
可見性實(shí)現(xiàn)原理
可見性存在的根本原因是一個(gè)線程讀,一個(gè)線程寫,一個(gè)線程寫操作對(duì)另外一個(gè)線程的讀不可見,因此我們主要分析volatile的寫操作就行,因?yàn)槿绻际沁M(jìn)行讀操作的話,數(shù)據(jù)就不會(huì)發(fā)生變化了,也就不存在可見性的問題了。
在上文當(dāng)中我們已經(jīng)談到了Java虛擬機(jī)在執(zhí)行volatile變量的寫操作時(shí)候,會(huì)執(zhí)行lock
指令,而這個(gè)指令有mfence
的效果:
- 將執(zhí)行
lock
指令的處理器的緩存行寫回到內(nèi)存當(dāng)中,因?yàn)槲覀冞M(jìn)行了volatile數(shù)據(jù)的更新,因此我們需要將這個(gè)更新的數(shù)據(jù)寫回內(nèi)存,好讓其他處理器在訪問內(nèi)存的時(shí)候,能夠看見被修改后的值。 - 寫回內(nèi)存的操作會(huì)使在其他CPU里緩存了該內(nèi)存地址的數(shù)據(jù)無效,這些處理器如果想使用這些數(shù)據(jù)的話,就需要從內(nèi)存當(dāng)中重新加載。因?yàn)樾薷牧藇olatile變量的值,但是現(xiàn)在其他處理器中的緩存(Cache)還是舊值,因此我們需要讓其他處理器緩存了這個(gè)用volatile修飾的變量的緩存行失效,那么其他處理器想要再使用這個(gè)數(shù)據(jù)的話就需要重新去內(nèi)存當(dāng)中加載,而最新的數(shù)據(jù)已經(jīng)更新到內(nèi)存當(dāng)中了。
深入內(nèi)存屏障——Store Buffer和Invalid Queue
在前面我們提到了lock
指令,lock
指令可保證其他CPU當(dāng)中緩存了volatile變量的緩存行無效。這是因?yàn)楫?dāng)處理器修改數(shù)據(jù)之后會(huì)在總線上發(fā)送消息說改動(dòng)了這個(gè)數(shù)據(jù),而其他處理器會(huì)通過總線嗅探的方式在總線上發(fā)現(xiàn)這個(gè)改動(dòng)消息,然后將對(duì)應(yīng)的緩存行置為無效。
這其實(shí)是處理器在處理共享數(shù)據(jù)時(shí)保證緩存數(shù)據(jù)一致性(Cache coherence)的協(xié)議,比如說Intel的MESI協(xié)議,在這個(gè)協(xié)議之下緩存行有以下四種狀態(tài):
- 已修改Modified (M) 緩存行是臟的(dirty),與主存的值不同。如果別的CPU內(nèi)核要讀主存這塊數(shù)據(jù),該緩存行必須回寫到主存,狀態(tài)變?yōu)楣蚕?S).
- 獨(dú)占Exclusive (E)緩存行只在當(dāng)前緩存中,但是干凈的(clean)緩存數(shù)據(jù)和主存數(shù)據(jù)相同。當(dāng)別的緩存讀取它時(shí),狀態(tài)變?yōu)楣蚕?;?dāng)寫數(shù)據(jù)時(shí),變?yōu)橐研薷臓顟B(tài)。
- 共享Shared (S)緩存行也存在于其它緩存中且是干凈的。緩存行可以在任意時(shí)刻拋棄。
- 無效Invalid (I)緩存行是無效的。
- 因?yàn)镸ESI協(xié)議涉及的內(nèi)容還是比較多的,如果你想仔細(xì)了解MESI協(xié)議,請(qǐng)看文末,這里就不詳細(xì)說明了!
假設(shè)在某個(gè)時(shí)刻,CPU的多個(gè)核心共享一個(gè)內(nèi)存數(shù)據(jù),其中一個(gè)一個(gè)核心想要修改這個(gè)數(shù)據(jù),那么他就會(huì)通過總線給其他核心發(fā)送消息表示想要修改這個(gè)數(shù)據(jù),然后其他核心將這個(gè)數(shù)據(jù)修改為Invalid狀態(tài),再給修改數(shù)據(jù)的核心發(fā)送一個(gè)消息,表示已經(jīng)收到這個(gè)消息,然后這個(gè)修改數(shù)據(jù)的核心就會(huì)將這個(gè)數(shù)據(jù)的狀態(tài)設(shè)置為Modified。
在上面的例子當(dāng)中當(dāng)一個(gè)核心給其他CPU發(fā)送消息時(shí)需要等待其他CPU給他返回確認(rèn)消息,這顯然會(huì)降低CPU的性能,為了能夠提高CPU處理數(shù)據(jù)的性能,硬件工程師做了一層優(yōu)化,在CPU當(dāng)中加了一個(gè)部分,叫做“Store Buffer”,當(dāng)CPU寫數(shù)據(jù)之后,需要等待其他處理器返回確認(rèn)消息,因此處理器先不將數(shù)據(jù)寫入緩存(Cache)當(dāng)中,而時(shí)寫入到Store Buffer當(dāng)中,然后繼續(xù)執(zhí)行指令不進(jìn)行等待,當(dāng)其他處理器返回確認(rèn)消息之后,再將Store Buffer當(dāng)中的消息寫入緩存,以后如果CPU需要數(shù)據(jù)就會(huì)先從Store Buffer當(dāng)中去查找,如果找不到才回去緩存當(dāng)中找,這個(gè)過程也叫做Store Forwarding。
處理器在接受到其他處理器發(fā)來的修改數(shù)據(jù)的消息的時(shí)候,需要將被修改的數(shù)據(jù)對(duì)應(yīng)的緩存行進(jìn)行失效處理,然后再返回確認(rèn)消息,為了提高處理器的性能,CPU會(huì)在接到消息之后立即返回,然后將這個(gè)Invalid的消息放入到Invalid Queue當(dāng)中,這就可以降低處理器響應(yīng)Invalid消息的時(shí)間。其實(shí)這樣做還有一個(gè)好處,因?yàn)樘幚砥鞯腟tore Buffer是有限的,如果發(fā)出Invalid消息的處理器遲遲接受不到響應(yīng)信息的話,那么Store Buffer就可以寫滿,這個(gè)時(shí)候處理器還會(huì)卡住,然后等待其他處理器的響應(yīng)消息,因此處理器在接受到Invalid的消息的時(shí)候立馬返回也可以提升發(fā)出Invalid消息的處理器的性能,會(huì)減少處理器卡住的時(shí)間,從而提升處理器的性能。
Store Buffer、Valid Queue、CPU、CPU緩存以及內(nèi)存的邏輯結(jié)構(gòu)大致如下:
還記得前面的兩條指令lfence
和sfence
嗎,現(xiàn)在我們重新回顧一下這兩條指令:
lfence
,在前面的內(nèi)容當(dāng)中,這個(gè)屏障能夠讓高速緩存失效,事實(shí)上是,它掃描Invalid Queue中的消息,然后讓對(duì)應(yīng)數(shù)據(jù)的緩存行失效,這樣的話就可以更新到內(nèi)存當(dāng)中最新的數(shù)據(jù)了。這里的失效并不是L1緩存失效,而是L2和L3中的緩存行失效,讀取數(shù)據(jù)也不一定從內(nèi)存當(dāng)中讀取,因?yàn)長(zhǎng)1Cache當(dāng)中可能有最新的數(shù)據(jù),如果有的話就可以從L1Cache當(dāng)中讀取。sfence
,在前面的內(nèi)容當(dāng)中,我們談到這個(gè)屏障時(shí),說它可以將寫入高速緩存的數(shù)據(jù)刷新到內(nèi)存當(dāng)中,這樣內(nèi)存當(dāng)中的數(shù)據(jù)就是最新的了,數(shù)據(jù)就可以全局可見了。事實(shí)上這個(gè)內(nèi)存屏障是將StoreBuffer當(dāng)中的數(shù)據(jù)刷行到L1Cache當(dāng)中,這樣其他的處理器就可以看到變化了,因?yàn)槎鄠€(gè)處理器是共享同一個(gè)L1Cache的,比如下圖當(dāng)中的CPU結(jié)構(gòu)。當(dāng)然它也是可以被刷新到內(nèi)存當(dāng)中的。
(下面圖片來源于網(wǎng)絡(luò))
MESI協(xié)議
在前面的文章當(dāng)中我們已經(jīng)提到了在MESI協(xié)議當(dāng)中緩存行的四種狀態(tài):
- 已修改Modified (M) 緩存行是臟的(dirty),與主存的值不同。如果別的CPU內(nèi)核要讀主存這塊數(shù)據(jù),該緩存行必須回寫到主存,狀態(tài)變?yōu)楣蚕?S).
- 獨(dú)占Exclusive (E)緩存行只在當(dāng)前緩存中,但是干凈的(clean)緩存數(shù)據(jù)和主存數(shù)據(jù)相同。當(dāng)別的緩存讀取它時(shí),狀態(tài)變?yōu)楣蚕恚划?dāng)寫數(shù)據(jù)時(shí),變?yōu)橐研薷臓顟B(tài)。
- 共享Shared (S)緩存行也存在于其它緩存中且是干凈的。緩存行可以在任意時(shí)刻拋棄。
- 無效Invalid (I)緩存行是無效的。
下圖表示不同處理器緩存同一個(gè)數(shù)據(jù)的緩存行的狀態(tài)是否相容:
- 比如說“I”那一行,處理器A的緩存行H包含數(shù)據(jù)
data
,而且這個(gè)緩存行的狀態(tài)是Invalid,那么其他處理器包含數(shù)據(jù)data
的緩存行的狀態(tài)可以是“M、E、S、I”當(dāng)中的任意一個(gè)。 - 再比如說包含數(shù)據(jù)
data
的緩存行是“Shared”的狀態(tài),說明這個(gè)數(shù)據(jù)是各個(gè)處理器共享的,因此其他的緩存行不可能是“Exclusive”狀態(tài),因?yàn)椴豢赡芗裙蚕硪勃?dú)占。當(dāng)然肯定也不是“Modified”,如果是“Modified”狀態(tài),那么其他緩存行只能是“Invalid”的狀態(tài),而不會(huì)是“Shared”狀態(tài)
在介紹MESI協(xié)議之前,我們先介紹一些基本操作:
處理器對(duì)緩存的請(qǐng)求:
- PrRd: 處理器請(qǐng)求讀一個(gè)緩存塊。
- PrWr: 處理器請(qǐng)求寫一個(gè)緩存塊。
總線對(duì)緩存的請(qǐng)求:
- BusRd: 總線上有一個(gè)消息:其他處理器請(qǐng)求讀一個(gè)緩存塊。
- BusRdX: 總線上有一個(gè)消息:其他處理器請(qǐng)求寫一個(gè)自己不擁有的緩存塊。
- BusUpgr: 總線上有一個(gè)消息:其他處理器請(qǐng)求寫一個(gè)自己擁有的緩存塊。
- Flush:總線上有一個(gè)消息:請(qǐng)求回寫整個(gè)緩存到主存。
- FlushOpt: 總線上有一個(gè)消息:整個(gè)緩存塊被發(fā)到總線,然后通過總線送給另外一個(gè)處理器(緩存到緩存的復(fù)制)。
下圖是MESI這四種狀態(tài)在不同的操作之下的轉(zhuǎn)換圖(紅色表示總線事務(wù),黑色表示處理器事務(wù)):(圖片來自維基百科)
- 假如現(xiàn)在是“M”狀態(tài),現(xiàn)在如果有其他處理器想要讀數(shù)據(jù)(BusRd)或者處理器想要將這個(gè)數(shù)據(jù)寫回內(nèi)存(flush),那么這個(gè)“M”狀態(tài)就轉(zhuǎn)變成“S”狀態(tài)了。
- 假如現(xiàn)在是“E”狀態(tài),如果有總線請(qǐng)求讀(BusRd),那么這個(gè)狀態(tài)就需要從獨(dú)占(E)變成共享(S)。
不同的初始狀態(tài)在不同的處理器操作下的狀態(tài)變化:
初始狀態(tài) | 操作 | 響應(yīng) |
---|---|---|
Invalid(I) | PrRd | 給總線發(fā)BusRd信號(hào) 其他處理器看到BusRd,檢查自己是否有有效的數(shù)據(jù)副本,通知發(fā)出請(qǐng)求的緩存 狀態(tài)轉(zhuǎn)換為(S)Shared, 如果其他緩存有有效的副本 狀態(tài)轉(zhuǎn)換為(E)Exclusive, 如果其他緩存都沒有有效的副本 如果其他緩存有有效的副本, 其中一個(gè)緩存發(fā)出數(shù)據(jù);否則從主存獲得數(shù)據(jù) |
Exclusive(E) | PrRd | 無總線事務(wù)生成 狀態(tài)保持不變 讀操作為緩存命中 |
Shared(S) | PrRd | 無總線事務(wù)生成 狀態(tài)保持不變 讀操作為緩存命中 |
Modified(M) | PrRd | 無總線事務(wù)生成 狀態(tài)保持不變 讀操作為緩存命中 |
Invalid(I) | PrWr | 給總線發(fā)BusRdX信號(hào) 狀態(tài)轉(zhuǎn)換為(M)Modified如果其他緩存有有效的副本, 其中一個(gè)緩存發(fā)出數(shù)據(jù);否則從主存獲得數(shù)據(jù) 如果其他緩存有有效的副本, 見到BusRdX信號(hào)后無效其副本 向緩存塊中寫入修改后的值 |
Exclusive(E) | PrWr | 無總線事務(wù)生成 狀態(tài)轉(zhuǎn)換為(M)Modified向緩存塊中寫入修改后的值 |
Shared(S) | PrWr | 發(fā)出總線事務(wù)BusUpgr信號(hào) 狀態(tài)轉(zhuǎn)換為(M)Modified其他緩存看到BusUpgr總線信號(hào),標(biāo)記其副本為(I)Invalid. |
Modified(M) | PrWr | 無總線事務(wù)生成 狀態(tài)保持不變 寫操作為緩存命中 |
不同的初始狀態(tài)在不同的總線消息下的狀態(tài)變化:
初始狀態(tài) | 操作 | 響應(yīng) |
---|---|---|
Invalid(I) | BusRd | 狀態(tài)保持不變,信號(hào)忽略 |
Exclusive(E) | BusRd | 狀態(tài)變?yōu)楣蚕?br />發(fā)出總線FlushOpt信號(hào)并發(fā)出塊的內(nèi)容 |
Shared(S) | BusRd | 狀態(tài)變?yōu)楣蚕?br />可能發(fā)出總線FlushOpt信號(hào)并發(fā)出塊的內(nèi)容(設(shè)計(jì)時(shí)決定那個(gè)共享的緩存發(fā)出數(shù)據(jù)) |
Modified(M) | BusRd | 狀態(tài)變?yōu)楣蚕?br />發(fā)出總線FlushOpt信號(hào)并發(fā)出塊的內(nèi)容,接收者為最初發(fā)出BusRd的緩存與主存控制器(回寫主存) |
Exclusive(E) | BusRdX | 狀態(tài)變?yōu)闊o效 發(fā)出總線FlushOpt信號(hào)并發(fā)出塊的內(nèi)容 |
Shared(S) | BusRdX | 狀態(tài)變?yōu)闊o效 可能發(fā)出總線FlushOpt信號(hào)并發(fā)出塊的內(nèi)容(設(shè)計(jì)時(shí)決定那個(gè)共享的緩存發(fā)出數(shù)據(jù)) |
Modified(M) | BusRdX | 狀態(tài)變?yōu)闊o效 發(fā)出總線FlushOpt信號(hào)并發(fā)出塊的內(nèi)容,接收者為最初發(fā)出BusRd的緩存與主存控制器(回寫主存) |
Invalid(I) | BusRdX/BusUpgr | 狀態(tài)保持不變,信號(hào)忽略 |
總結(jié)
在本篇文章當(dāng)中主要是介紹了volatile和JMM的具體作用和規(guī)則,然后仔細(xì)介紹了實(shí)現(xiàn)這些的底層原理,尤其是內(nèi)存屏障以及它在X86當(dāng)中的具體實(shí)現(xiàn),這一部分的內(nèi)容比較抽象,可能難以理解本篇文章涉及的內(nèi)容比較多,可能需要大家慢慢的仔細(xì)思考才能理解。
以上就是深入了解volatile和Java內(nèi)存模型的詳細(xì)內(nèi)容,更多關(guān)于Java內(nèi)存模型 volatile的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
登錄EasyConnect后無法通過jdbc訪問服務(wù)器數(shù)據(jù)庫問題的解決方法
描述一下近期使用EasyConnect遇到的問題,下面這篇文章主要給大家介紹了關(guān)于登錄EasyConnect后無法通過jdbc訪問服務(wù)器數(shù)據(jù)庫問題的解決方法,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-02-02java實(shí)現(xiàn)通訊錄管理系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了java實(shí)現(xiàn)通訊錄管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-07-07java打印從1到100的值(break,return斷句)
java 先寫一個(gè)程序,打印從1到100的值。之后修改程序,通過使用break關(guān)鍵詞,使得程序在打印到98時(shí)退出。然后嘗試使用return來達(dá)到相同的目的2017-02-02基于 SpringBoot 實(shí)現(xiàn) MySQL 讀寫分離的問題
這篇文章主要介紹了基于 SpringBoot 實(shí)現(xiàn) MySQL 讀寫分離的問題,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-02-02