最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

java 多線程與并發(fā)之volatile詳解分析

 更新時間:2021年11月02日 10:16:08   作者:臨木小屋  
volatile這個關(guān)鍵字可能很多朋友都聽說過,或許也都用過。在Java 5之前,它是一個備受爭議的關(guān)鍵字,因?yàn)樵诔绦蛑惺褂盟鶗?dǎo)致出人意料的結(jié)果。在Java 5之后,volatile關(guān)鍵字才得以重獲生機(jī)

CPU、內(nèi)存、緩存的關(guān)系

要理解JMM,要先從計(jì)算機(jī)底層開始,下面是一份大佬的研究報告

在這里插入圖片描述

計(jì)算機(jī)在做一些我們平時的基本操作時,需要的響應(yīng)時間是不一樣的!如果我們計(jì)算一次a+b所需要的的時間:

  • CPU讀取內(nèi)存獲得a,100納秒
  • CPU讀取內(nèi)存獲得b,100納秒
  • CPU執(zhí)行一條指令 a+b ,0.6納秒

也就是說99%的時間花費(fèi)在CPU讀取內(nèi)存上了,那如何解決速度不均衡問題?

早期計(jì)算機(jī)中cpu和內(nèi)存的速度是差不多的,但在現(xiàn)代計(jì)算機(jī)中cpu的指令速度遠(yuǎn)超內(nèi)存的存取速度,由于計(jì)算機(jī)的存儲設(shè)備與處理器的運(yùn)算速度有幾個數(shù)量級的差距,所以現(xiàn)代計(jì)算機(jī)系統(tǒng)都不得不加入一層讀寫速度盡可能接近處理器運(yùn)算速度的高速緩存(Cache)來作為內(nèi)存與處理器之間的緩沖:將運(yùn)算需要使用到的數(shù)據(jù)復(fù)制到緩存中,讓運(yùn)算能快速進(jìn)行,當(dāng)運(yùn)算結(jié)束后再從緩存同步回內(nèi)存之中,這樣處理器就無須等待緩慢的內(nèi)存讀寫了

CPU緩存

什么是CPU緩存

在計(jì)算機(jī)系統(tǒng)中,CPU高速緩存(英語:CPU Cache,在本文中簡稱緩存)是用于減少處理器訪問內(nèi)存所需平均時間的部件。在金字塔式存儲體系中它位于自頂向下的第二層,僅次于CPU寄存器。其容量遠(yuǎn)小于內(nèi)存,但速度卻可以接近處理器的頻率。當(dāng)處理器發(fā)出內(nèi)存訪問請求時,會先查看緩存內(nèi)是否有請求數(shù)據(jù)。如果存在(命中),則不經(jīng)訪問內(nèi)存直接返回該數(shù)據(jù);如果不存在(失效),則要先把內(nèi)存中的相應(yīng)數(shù)據(jù)載入緩存,再將其返回處理器。

下圖是一個典型的存儲器層次結(jié)構(gòu),我們可以看到一共使用了三級緩存:

在這里插入圖片描述

為什么要有多級CPU Cache

在計(jì)算機(jī)系統(tǒng)中,寄存器劃是L0級緩存,接著依次是L1,L2,L3(接下來是內(nèi)存,本地磁盤,遠(yuǎn)程存儲)。越往上的緩存存儲空間越小,速度越快,成本也更高;越往下的存儲空間越大,速度更慢,成本也更低。從上至下,每一層都可以看做是更下一層的緩存,即:L0寄存器是L1一級緩存的緩存,L1是L2的緩存,依次類推;每一層的數(shù)據(jù)都是來至它的下一層,所以每一層的數(shù)據(jù)是下一層的數(shù)據(jù)的子集

在這里插入圖片描述


下圖是我電腦的三級緩存,可以看到層級越小容量越小。速度越快價格越高!!

在這里插入圖片描述

在現(xiàn)代CPU上,一般來說L0, L1,L2,L3都集成在CPU內(nèi)部,而L1還分為一級數(shù)據(jù)緩存(Data Cache,D-Cache,L1d)和一級指令緩存(Instruction Cache,I-Cache,L1i),分別用于存放數(shù)據(jù)和執(zhí)行數(shù)據(jù)的指令解碼。每個核心擁有獨(dú)立的運(yùn)算處理單元、控制器、寄存器、L1、L2緩存,然后一個CPU的多個核心共享最后一層CPU緩存L3。

為了充分利用 CPU Cache,Java提出了內(nèi)存模型這個概念

Java內(nèi)存模型(Java Memory Model,JMM)

從抽象的角度來看,JMM定義了線程和主內(nèi)存之間的抽象關(guān)系:線程之間的共享變量存儲在主內(nèi)存(Main Memory)中,每個線程都有一個私有的本地內(nèi)存(Local Memory),本地內(nèi)存中存儲了該線程以讀/寫共享變量的副本。本地內(nèi)存是JMM的一個抽象概念,并不真實(shí)存在。它涵蓋了緩存、寫緩沖區(qū)、寄存器以及其他的硬件和編譯器優(yōu)化。

在這里插入圖片描述

程之間的共享變量存儲在主內(nèi)存(Main Memory)中,每個線程都有一個私有的工作內(nèi)存(Local Memory),工作內(nèi)存中存儲了該線程以讀/寫共享變量的副本。

在這里插入圖片描述

舉個栗子:多個線程去修改主內(nèi)存中的變量a。線程不能直接修改主內(nèi)存中的數(shù)據(jù),先把數(shù)據(jù)拷貝到工作內(nèi)存,線程對私有的工作內(nèi)存修改然后再同步到主內(nèi)存。那這樣做會帶來什么問題呢?

JMM導(dǎo)致的并發(fā)安全問題

從JMM角度看,如果兩個線程同時調(diào)用 a=a+1這個函數(shù)(假設(shè)a的初始值是0),A、B線程同時從主內(nèi)存中拷貝a=0,然后修改寫回,最后主內(nèi)存為a=1,咋搞?

在這里插入圖片描述

如下是代碼栗子

public class MainTest {
    
    private  long count = 0;

    public  void incCount() {
        count += 1;
    }

    public static void main(String[] args) throws InterruptedException {

        MainTest test = new MainTest();
        Count count = new Count(test);
        Count count1 = new Count(test);
        count.start();
        count1.start();
        Thread.sleep(5);
        System.out.println("result is :" + test.count);
    }
    
    private static class Count extends Thread{
        private MainTest m;

        public Count(MainTest m){
            this.m = m;
        }

        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                m.incCount();
            }
        }
    }
} 

執(zhí)行結(jié)果

// 第一次執(zhí)行
> Task :lib-test:MainTest.main()
result is :11861

// 第二次執(zhí)行
> Task :lib-test:MainTest.main()
result is :10535

可見性

可見性是指當(dāng)多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值

由于線程對變量的所有操作都必須在工作內(nèi)存中進(jìn)行,而不能直接讀寫主內(nèi)存中的變量,那么對于共享變量a,它們首先是在自己的工作內(nèi)存,之后再同步到主內(nèi)存。可是并不會及時的刷到主存中,而是會有一定時間差。很明顯,這個時候線程 A 對變量 a 的操作對于線程 B 而言就不具備可見性了 。

要解決共享對象可見性這個問題,我們可以使用volatile關(guān)鍵字或者是加鎖

原子性

即一個操作或者多個操作 要么全部執(zhí)行并且執(zhí)行的過程不會被任何因素打斷,要么就都不執(zhí)行

我們都知道CPU資源的分配都是以線程為單位的,并且是分時調(diào)用,操作系統(tǒng)允許某個進(jìn)程執(zhí)行一小段時間,例如 50 毫秒,過了 50 毫秒操作系統(tǒng)就會重新選擇一個進(jìn)程來執(zhí)行(我們稱為“任務(wù)切換”),這個 50 毫秒稱為“時間片”。而任務(wù)的切換大多數(shù)是在時間片段結(jié)束以后。

那么線程切換為什么會帶來bug呢?因?yàn)椴僮飨到y(tǒng)做任務(wù)切換,可以發(fā)生在任何一條CPU 指令執(zhí)行完!注意,是 CPU 指令,CPU 指令,CPU 指令,而不是高級語言里的一條語句。比如count++,在java里就是一句話,但高級語言里一條語句往往需要多條 CPU 指令完成。其實(shí)count++包含了三個CPU指令

有序性

即程序執(zhí)行的順序按照代碼的先后順序執(zhí)行。

在Java內(nèi)存模型中,為了效率是允許編譯器和處理器對指令進(jìn)行重排序,當(dāng)然重排序不會影響單線程的運(yùn)行結(jié)果,但是對多線程會有影響。Java提供volatile來保證一定的有序性。最著名的例子就是單例模式里面的DCL(雙重檢查鎖)。另外,可以通過synchronized和Lock來保證有序性,synchronized和Lock保證每個時刻是有一個線程執(zhí)行同步代碼,相當(dāng)于是讓線程順序執(zhí)行同步代碼,自然就保證了有序性。

在單線程的情況下,CPU執(zhí)行語句并不是按照順序來的,為了更高的執(zhí)行效率可能會重新排序,單線程下是可以提高執(zhí)行效率且保證正確。但在多線程下反而變成了安全問題,Java提供volatile來保證一定的有序性。此處不做深入!

volatile

volatile特性

  • 可見性:對一個volatile變量的讀,總是能看到(任意線程)對這個volatile變量最后的寫入
  • 原子性:對任意單個volatile變量的讀/寫具有原子性,但類似于volatile++這種復(fù)合操作不具有原子性

【面試題】為什么volatile不能保證a++的線程安全問題
:線程執(zhí)行a++要經(jīng)歷讀取主內(nèi)存-加載-使用-賦值-寫內(nèi)存-寫回主內(nèi)存幾個階段,而且a++不是原子操作,至少可以分為三步執(zhí)行。線程A、B同時從主內(nèi)存讀取a的值,A線程執(zhí)行到加載階段切換上下文交出CPU使用權(quán),B線程完成整個操作并刷新了主內(nèi)存中a的值。此時A線程繼續(xù)賦值等其他操作,已經(jīng)造成了安全問題??梢娦允潜WC線程每次讀取時必須讀取主內(nèi)存的值,對后續(xù)的操作沒有限制,不會因?yàn)橹鲀?nèi)存中的值改變而中斷了操作。如果是原子性則可以,synchronized可以保證原子性。

在這里插入圖片描述

volatile 的實(shí)現(xiàn)原理

有volatile修飾的共享變量進(jìn)行寫操作的時候會使用CPU提供的Lock前綴指令

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

單例模式的雙重鎖為什么要加volatile

public class TestInstance{
	private volatile static TestInstance instance;
	public static TestInstance getInstance(){        //1
		if(instance == null){                        //2
			synchronized(TestInstance.class){        //3
				if(instance == null){                //4
					instance = new TestInstance();   //5
				}
			}
		}
		return instance;                             //6
	}
}

需要volatile關(guān)鍵字的原因是,在并發(fā)情況下,如果沒有volatile關(guān)鍵字,在第5行會出現(xiàn)問題。
instance = new TestInstance()可以分解為3行偽代碼

a. memory = allocate() //分配內(nèi)存
 
b. ctorInstanc(memory) //初始化對象
 
c. instance = memory //設(shè)置instance指向剛分配的地址

上面的代碼在編譯運(yùn)行時,可能會出現(xiàn)重排序從a-b-c排序?yàn)閍-c-b。在多線程的情況下會出現(xiàn)以下問題。當(dāng)線程A在執(zhí)行第5行代碼時,B線程進(jìn)來執(zhí)行到第2行代碼。假設(shè)此時A執(zhí)行的過程中發(fā)生了指令重排序,即先執(zhí)行了a和c,沒有執(zhí)行b。那么由于A線程執(zhí)行了c導(dǎo)致instance指向了一段地址,所以B線程判斷instance不為null,會直接跳到第6行并返回一個未初始化的對象

總結(jié)

因?yàn)镃PU與內(nèi)存的速度差距越來越大,為了彌補(bǔ)速度差距引入了CPU緩存,又因?yàn)榫彺鎸?dǎo)致線程安全問題,從前到后縷出一條線來就很容易理解了。如果只是單線程完全不擔(dān)心什么指令重排,想要更高的執(zhí)行效率必然付出安全風(fēng)險。知其然,知其所以然!

到此這篇關(guān)于java 多線程與并發(fā)之volatile詳解分析的文章就介紹到這了,更多相關(guān)Java volatile內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評論

道孚县| 汉川市| 丰原市| 海宁市| 嵩明县| 桂东县| 吉林市| 英山县| 普兰店市| 香格里拉县| 全南县| 商都县| 井研县| 资中县| 茌平县| 甘洛县| 马尔康县| 乐亭县| 浑源县| 光泽县| 密山市| 大同县| 武川县| 互助| 大城县| 中江县| 齐齐哈尔市| 凤翔县| 武功县| 宜阳县| 阳泉市| 贵阳市| 泊头市| 新津县| 上犹县| 固原市| 江山市| 云南省| 尼玛县| 新晃| 建德市|