這篇文章主要介紹“Java內(nèi)存區(qū)域與內(nèi)存模型詳解”,在日常操作中,相信很多人在Java內(nèi)存區(qū)域與內(nèi)存模型詳解問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”Java內(nèi)存區(qū)域與內(nèi)存模型詳解”的疑惑有所幫助!接下來,請跟著小編一起來學(xué)習(xí)吧!
創(chuàng)新互聯(lián)專注于企業(yè)營銷型網(wǎng)站、網(wǎng)站重做改版、凌海網(wǎng)站定制設(shè)計(jì)、自適應(yīng)品牌網(wǎng)站建設(shè)、H5頁面制作、成都做商城網(wǎng)站、集團(tuán)公司官網(wǎng)建設(shè)、外貿(mào)網(wǎng)站建設(shè)、高端網(wǎng)站制作、響應(yīng)式網(wǎng)頁設(shè)計(jì)等建站業(yè)務(wù),價(jià)格優(yōu)惠性價(jià)比高,為凌海等各大城市提供網(wǎng)站開發(fā)制作服務(wù)。首先介紹兩個(gè)名詞:1)可見性:一個(gè)線程對共享變量值的修改,能夠及時(shí)地被其他線程看到。2)共享變量:如果一個(gè)變量在多個(gè)線程的工作內(nèi)存中都存在副本,那么這個(gè)變量就是這幾個(gè)線程的共享變量
Java線程之間的通信對程序員完全透明,在并發(fā)編程中,需要處理兩個(gè)關(guān)鍵問題:線程之間如何通信及線程之間如何同步。
通信:通信是指線程之間以何種機(jī)制來交換信息。在命令式編程中,線程之間的通信機(jī)制有兩種:共享內(nèi)存和消息傳遞。在共享內(nèi)存的并發(fā)模型里,線程之間共享程序的公共狀態(tài),通過寫-讀內(nèi)存中的公共狀態(tài)來進(jìn)行隱式通信。在消息傳遞的并發(fā)模型里,線程之間沒有公共狀態(tài),線程之間必須通過發(fā)送消息來進(jìn)行顯示通信。
同步:同步是指程序中用于控制不同線程間操作發(fā)生相對順序的機(jī)制。在共享內(nèi)存并發(fā)模型里,同步是顯示進(jìn)行的,程序員必須顯示指定某個(gè)方法或某段代碼需要在線程之間互斥執(zhí)行。在消息傳遞的并發(fā)模型里,由于消息的發(fā)送必須在消息的接收之前,因此同步是隱式進(jìn)行的。
Java并發(fā)采用的是共享內(nèi)存模型。
Java虛擬機(jī)在運(yùn)行程序時(shí)會(huì)把其自動(dòng)管理的內(nèi)存劃分為以上幾個(gè)區(qū)域,每個(gè)區(qū)域都有的用途以及創(chuàng)建銷毀的時(shí)機(jī),其中藍(lán)色部分代表的是所有線程共享的數(shù)據(jù)區(qū)域,而綠色部分代表的是每個(gè)線程的私有數(shù)據(jù)區(qū)域。
方法區(qū)(Method Area):
方法區(qū)屬于線程共享的內(nèi)存區(qū)域,又稱Non-Heap(非堆),主要用于存儲(chǔ)已被虛擬機(jī)加載的類信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù),根據(jù)Java 虛擬機(jī)規(guī)范的規(guī)定,當(dāng)方法區(qū)無法滿足內(nèi)存分配需求時(shí),將拋出OutOfMemoryError 異常。值得注意的是在方法區(qū)中存在一個(gè)叫運(yùn)行時(shí)常量池(Runtime Constant Pool)的區(qū)域,它主要用于存放編譯器生成的各種字面量和符號(hào)引用,這些內(nèi)容將在類加載后存放到運(yùn)行時(shí)常量池中,以便后續(xù)使用。
JVM堆(Java Heap):
Java 堆也是屬于線程共享的內(nèi)存區(qū)域,它在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建,是Java 虛擬機(jī)所管理的內(nèi)存中大的一塊,主要用于存放對象實(shí)例,幾乎所有的對象實(shí)例都在這里分配內(nèi)存,注意Java 堆是垃圾收集器管理的主要區(qū)域,因此很多時(shí)候也被稱做GC 堆,如果在堆中沒有內(nèi)存完成實(shí)例分配,并且堆也無法再擴(kuò)展時(shí),將會(huì)拋出OutOfMemoryError 異常。
程序計(jì)數(shù)器(Program Counter Register):
屬于線程私有的數(shù)據(jù)區(qū)域,是一小塊內(nèi)存空間,主要代表當(dāng)前線程所執(zhí)行的字節(jié)碼行號(hào)指示器。字節(jié)碼解釋器工作時(shí),通過改變這個(gè)計(jì)數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令,分支、循環(huán)、跳轉(zhuǎn)、異常處理、線程恢復(fù)等基礎(chǔ)功能都需要依賴這個(gè)計(jì)數(shù)器來完成。
虛擬機(jī)棧(Java Virtual Machine Stacks):
屬于線程私有的數(shù)據(jù)區(qū)域,與線程同時(shí)創(chuàng)建,總數(shù)與線程關(guān)聯(lián),代表Java方法執(zhí)行的內(nèi)存模型。棧中只保存基礎(chǔ)數(shù)據(jù)類型和自定義對象的引用(不是對象),對象都存放在堆區(qū)中。每個(gè)方法執(zhí)行時(shí)都會(huì)創(chuàng)建一個(gè)棧楨來存儲(chǔ)方法的的變量表、操作數(shù)棧、動(dòng)態(tài)鏈接方法、返回值、返回地址等信息。每個(gè)方法從調(diào)用直結(jié)束就對于一個(gè)棧楨在虛擬機(jī)棧中的入棧和出棧過程,如下(圖有誤,應(yīng)該為棧楨):
本地方法棧(Native Method Stacks):
本地方法棧屬于線程私有的數(shù)據(jù)區(qū)域,這部分主要與虛擬機(jī)用到的 Native 方法相關(guān),一般情況下,我們無需關(guān)心此區(qū)域。
Java內(nèi)存模型(即Java Memory Model,簡稱JMM)本身是一種抽象的概念,并不真實(shí)存在。Java線程之間的通信由JMM控制,JMM決定一個(gè)線程對共享變量的寫入何時(shí)對另一個(gè)線程可見。從抽象的角度來看,JMM定義了線程和主內(nèi)存之間的抽象關(guān)系。
由于JVM運(yùn)行程序的實(shí)體是線程,而每個(gè)線程創(chuàng)建時(shí)JVM都會(huì)為其創(chuàng)建一個(gè)工作內(nèi)存(有些地方稱為??臻g),用于存儲(chǔ)線程私有的數(shù)據(jù),而Java內(nèi)存模型中規(guī)定所有變量都存儲(chǔ)在主內(nèi)存,主內(nèi)存是共享內(nèi)存區(qū)域,所有線程都可以訪問,但線程對變量的操作(讀取賦值等)必須在工作內(nèi)存中進(jìn)行。
首先要將變量從主內(nèi)存拷貝的自己的工作內(nèi)存空間,然后對變量進(jìn)行操作,操作完成后再將變量寫回主內(nèi)存,不能直接操作主內(nèi)存中的變量,工作內(nèi)存中存儲(chǔ)著主內(nèi)存中的變量副本拷貝,前面說過,工作內(nèi)存是每個(gè)線程的私有數(shù)據(jù)區(qū)域,因此不同的線程間無法訪問對方的工作內(nèi)存,線程間的通信(傳值)必須通過主內(nèi)存來完成,其簡要訪問過程如下圖
圖3
需要注意的是,JMM與Java內(nèi)存區(qū)域的劃分是不同的概念層次,更恰當(dāng)說JMM描述的是一組規(guī)則,通過這組規(guī)則控制程序中各個(gè)變量在共享數(shù)據(jù)區(qū)域和私有數(shù)據(jù)區(qū)域的訪問方式,JMM是圍繞原子性,有序性、可見性展開的(稍后會(huì)分析)。
JMM與Java內(nèi)存區(qū)域唯一相似點(diǎn),都存在共享數(shù)據(jù)區(qū)域和私有數(shù)據(jù)區(qū)域,在JMM中主內(nèi)存屬于共享數(shù)據(jù)區(qū)域,從某個(gè)程度上講應(yīng)該包括了堆和方法區(qū),而工作內(nèi)存數(shù)據(jù)線程私有數(shù)據(jù)區(qū)域,從某個(gè)程度上講則應(yīng)該包括程序計(jì)數(shù)器、虛擬機(jī)棧以及本地方法棧。或許在某些地方,我們可能會(huì)看見主內(nèi)存被描述為堆內(nèi)存,工作內(nèi)存被稱為線程棧,實(shí)際上他們表達(dá)的都是同一個(gè)含義。關(guān)于JMM中的主內(nèi)存和工作內(nèi)存說明如下
主內(nèi)存
主要存儲(chǔ)的是Java實(shí)例對象以及線程之間的共享變量,所有線程創(chuàng)建的實(shí)例對象都存放在主內(nèi)存中,不管該實(shí)例對象是成員變量還是方法中的本地變量(也稱局部變量),當(dāng)然也包括了共享的類信息、常量、靜態(tài)變量。由于是共享數(shù)據(jù)區(qū)域,多條線程對同一個(gè)變量進(jìn)行訪問可能會(huì)發(fā)現(xiàn)線程安全問題。
工作內(nèi)存
有的書籍中也稱為本地內(nèi)存,主要存儲(chǔ)當(dāng)前方法的所有本地變量信息(工作內(nèi)存中存儲(chǔ)著主內(nèi)存中的變量副本拷貝),每個(gè)線程只能訪問自己的工作內(nèi)存,即線程中的本地變量對其它線程是不可見的,就算是兩個(gè)線程執(zhí)行的是同一段代碼,它們也會(huì)各自在自己的工作內(nèi)存中創(chuàng)建屬于當(dāng)前線程的本地變量,當(dāng)然也包括了字節(jié)碼行號(hào)指示器、相關(guān)Native方法的信息。
注意由于工作內(nèi)存是每個(gè)線程的私有數(shù)據(jù),線程間無法相互訪問工作內(nèi)存,因此存儲(chǔ)在工作內(nèi)存的數(shù)據(jù)不存在線程安全問題。注意,工作內(nèi)存是JMM的一個(gè)抽象概念,并不真實(shí)存在。
弄清楚主內(nèi)存和工作內(nèi)存后,接了解一下主內(nèi)存與工作內(nèi)存的數(shù)據(jù)存儲(chǔ)類型以及操作方式,根據(jù)虛擬機(jī)規(guī)范,對于一個(gè)實(shí)例對象中的成員方法而言,如果方法中包含本地變量是基本數(shù)據(jù)類型(boolean,byte,short,char,int,long,float,double),將直接存儲(chǔ)在工作內(nèi)存的幀棧結(jié)構(gòu)中,但倘若本地變量是引用類型,那么該變量的引用會(huì)存儲(chǔ)在功能內(nèi)存的幀棧中,而對象實(shí)例將存儲(chǔ)在主內(nèi)存(共享數(shù)據(jù)區(qū)域,堆)中。
但對于實(shí)例對象的成員變量,不管它是基本數(shù)據(jù)類型或者包裝類型(Integer、Double等)還是引用類型,都會(huì)被存儲(chǔ)到堆區(qū)。至于static變量以及類本身相關(guān)信息將會(huì)存儲(chǔ)在主內(nèi)存中。需要注意的是,在主內(nèi)存中的實(shí)例對象可以被多線程共享,倘若兩個(gè)線程同時(shí)調(diào)用了同一個(gè)對象的同一個(gè)方法,那么兩條線程會(huì)將要操作的數(shù)據(jù)拷貝一份到自己的工作內(nèi)存中,執(zhí)行完成操作后才刷新到主內(nèi)存,簡單示意圖如下所示:
圖4
從圖3來看,如果線程A與線程B之間要通信的話,必須經(jīng)歷下面兩個(gè)步驟:
1)線程A把本地內(nèi)存A中更新過的共享變量刷新到主內(nèi)存中去
2)線程B到主內(nèi)存中去讀取線程A之前已更新過的共享變量
從以上兩個(gè)步驟來看,共享內(nèi)存模型完成了“隱式通信”的過程。
JMM也主要是通過控制主內(nèi)存與每個(gè)線程的工作內(nèi)存之間的交互,來為Java程序員提供內(nèi)存可見性的保證。
重排序是指編譯器和處理器為了優(yōu)化程序性能而對指令序列進(jìn)行重新排序的一種手段。as-if-serial語義的意思是:不管怎么重排序(編譯器和處理器為了提高并行度),(單線程)程序的執(zhí)行結(jié)果不能被改變。編譯器、runtime和處理器都必須遵守as-if-serial語義。為了遵守as-if-serial語義,編譯器和處理器不會(huì)對存在數(shù)據(jù)依賴關(guān)系的操作做重排序,因?yàn)檫@種重排序會(huì)改變執(zhí)行結(jié)果。
但是,如果操作之間不存在數(shù)據(jù)依賴關(guān)系,這些操作就可能被編譯器和處理器重排序。
happens-before是JMM最核心的概念。對應(yīng)Java程序來說,理解happens-before是理解JMM的關(guān)鍵。
設(shè)計(jì)JMM時(shí),需要考慮兩個(gè)關(guān)鍵因素:
程序員對內(nèi)存模型的使用。程序員希望內(nèi)存模型易于理解、易于編程。程序員希望基于一個(gè)強(qiáng)內(nèi)存模型來編寫代碼。
編譯器和處理器對內(nèi)存模型的實(shí)現(xiàn)。編譯器和處理器希望內(nèi)存模型對它們的束縛越少越好,這樣它們就可以做盡可能多的優(yōu)化來提高性能。編譯器和處理器希望實(shí)現(xiàn)弱內(nèi)存模型。
但以上兩點(diǎn)相互矛盾,所以JSR-133專家組在設(shè)計(jì)JMM時(shí)的核心膜表就是找到一個(gè)好的平衡點(diǎn):一方面,為程序員提高足夠強(qiáng)的內(nèi)存可見性保證;另一方面,對編譯器和處理器的限制盡可能地放松。
另外還要一個(gè)特別有意思的事情就是關(guān)于重排序問題,更簡單的說,重排序可以分為兩類:1)會(huì)改變程序執(zhí)行結(jié)果的重排序。 2) 不會(huì)改變程序執(zhí)行結(jié)果的重排序。
JMM對這兩種不同性質(zhì)的重排序,采取了不同的策略,如下:
對于會(huì)改變程序執(zhí)行結(jié)果的重排序,JMM要求編譯器和處理器必須禁止這種重排序。
對于不會(huì)改變程序執(zhí)行結(jié)果的重排序,JMM對編譯器和處理器不做要求(JMM允許這種 重排序)
JMM的設(shè)計(jì)圖為:
JMM設(shè)計(jì)示意圖 從圖可以看出:
JMM向程序員提供的happens-before規(guī)則能滿足程序員的需求。JMM的happens-before規(guī)則不但簡單易懂,而且也向程序員提供了足夠強(qiáng)的內(nèi)存可見性保證(有些內(nèi)存可見性保證其實(shí)并不一定真實(shí)存在,比如上面的A happens-before B)。
JMM對編譯器和處理器的束縛已經(jīng)盡可能少。從上面的分析可以看出,JMM其實(shí)是在遵循一個(gè)基本原則:只要不改變程序的執(zhí)行結(jié)果(指的是單線程程序和正確同步的多線程程序),編譯器和處理器怎么優(yōu)化都行。例如,如果編譯器經(jīng)過細(xì)致的分析后,認(rèn)定一個(gè)鎖只會(huì)被單個(gè)線程訪問,那么這個(gè)鎖可以被消除。再如,如果編譯器經(jīng)過細(xì)致的分析后,認(rèn)定一個(gè)volatile變量只會(huì)被單個(gè)線程訪問,那么編譯器可以把這個(gè)volatile變量當(dāng)作一個(gè)普通變量來對待。這些優(yōu)化既不會(huì)改變程序的執(zhí)行結(jié)果,又能提高程序的執(zhí)行效率。
happens-before的概念最初由Leslie Lamport在其一篇影響深遠(yuǎn)的論文(《Time,Clocks and the Ordering of Events in a Distributed System》)中提出。JSR-133使用happens-before的概念來指定兩個(gè)操作之間的執(zhí)行順序。由于這兩個(gè)操作可以在一個(gè)線程之內(nèi),也可以是在不同線程之間。因此,JMM可以通過happens-before關(guān)系向程序員提供跨線程的內(nèi)存可見性保證(如果A線程的寫操作a與B線程的讀操作b之間存在happens-before關(guān)系,盡管a操作和b操作在不同的線程中執(zhí)行,但JMM向程序員保證a操作將對b操作可見)。具體的定義為:
1)如果一個(gè)操作happens-before另一個(gè)操作,那么第一個(gè)操作的執(zhí)行結(jié)果將對第二個(gè)操作可見,而且第一個(gè)操作的執(zhí)行順序排在第二個(gè)操作之前。
2)兩個(gè)操作之間存在happens-before關(guān)系,并不意味著Java平臺(tái)的具體實(shí)現(xiàn)必須要按照happens-before關(guān)系指定的順序來執(zhí)行。如果重排序之后的執(zhí)行結(jié)果,與按happens-before關(guān)系來執(zhí)行的結(jié)果一致,那么這種重排序并不非法(也就是說,JMM允許這種重排序)。
上面的1)是JMM對程序員的承諾。從程序員的角度來說,可以這樣理解happens-before關(guān)系:如果A happens-before B,那么Java內(nèi)存模型將向程序員保證——A操作的結(jié)果將對B可見,且A的執(zhí)行順序排在B之前。注意,這只是Java內(nèi)存模型向程序員做出的保證!
上面的2)是JMM對編譯器和處理器重排序的約束原則。正如前面所言,JMM其實(shí)是在遵循一個(gè)基本原則:只要不改變程序的執(zhí)行結(jié)果(指的是單線程程序和正確同步的多線程程序),編譯器和處理器怎么優(yōu)化都行。JMM這么做的原因是:程序員對于這兩個(gè)操作是否真的被重排序并不關(guān)心,程序員關(guān)心的是程序執(zhí)行時(shí)的語義不能被改變(即執(zhí)行結(jié)果不能被改變)。因此,happens-before關(guān)系本質(zhì)上和as-if-serial語義是一回事。
as-if-serial語義保證單線程內(nèi)程序的執(zhí)行結(jié)果不被改變,happens-before關(guān)系保證正確同步的多線程程序的執(zhí)行結(jié)果不被改變。
as-if-serial語義給編寫單線程程序的程序員創(chuàng)造了一個(gè)幻境:單線程程序是按程序的順序來執(zhí)行的。happens-before關(guān)系給編寫正確同步的多線程程序的程序員創(chuàng)造了一個(gè)幻境:正確同步的多線程程序是按happens-before指定的順序來執(zhí)行的。
as-if-serial語義和happens-before這么做的目的,都是為了在不改變程序執(zhí)行結(jié)果的前提下,盡可能地提高程序執(zhí)行的并行度。
程序順序規(guī)則:一個(gè)線程中的每個(gè)操作,happens-before于該線程中的任意后續(xù)操作。
監(jiān)視器鎖規(guī)則:對一個(gè)鎖的解鎖,happens-before于隨后對這個(gè)鎖的加鎖。
volatile變量規(guī)則:對一個(gè)volatile域的寫,happens-before于任意后續(xù)對這個(gè)volatile域的讀。
傳遞性:如果A happens-before B,且B happens-before C,那么A happens-before C。
start()規(guī)則:如果線程A執(zhí)行操作ThreadB.start()(啟動(dòng)線程B),那么A線程的ThreadB.start()操作happens-before于線程B中的任意操作。
join()規(guī)則:如果線程A執(zhí)行操作ThreadB.join()并成功返回,那么線程B中的任意操作happens-before于線程A從ThreadB.join()操作成功返回。
一個(gè)happens-before規(guī)則對應(yīng)于一個(gè)或多個(gè)編譯器和處理器重排序規(guī)則。對于Java程序員來說,happens-before規(guī)則簡單易懂,它避免Java程序員為了理解JMM提供的內(nèi)存可見性保證而去學(xué)習(xí)復(fù)雜的重排序規(guī)則以及這些規(guī)則的具體實(shí)現(xiàn)方法
當(dāng)聲明共享變量為volatile后,對這個(gè)變量的讀/寫會(huì)很特別。一個(gè)volatile變量的單個(gè)讀/寫操作,與一個(gè)普通變量的讀/寫操作都是使用同一個(gè)鎖來同步,它們之間的執(zhí)行效果相同。
鎖的happens-before規(guī)則保證釋放鎖和獲取鎖的兩個(gè)線程之間的內(nèi)存可見性,這意味著對一個(gè)volatile變量的讀,總是能看到(任意線程)對這個(gè)volatile變量最后的寫入。
鎖的語義決定了臨界區(qū)代碼的執(zhí)行具有原子性。這意味著,即使是64位的long型和double型變量,只要是volatile變量,對該變量的讀/寫就具有原子性。如果是多個(gè)volatile操作或類似于volatile++這種復(fù)合操作,這些操作整體上不具有原子性。
簡而言之,一旦一個(gè)共享變量(類的成員變量、類的靜態(tài)成員變量)被volatile修飾之后,那么就具備了兩層語義:
1)保證了不同線程對這個(gè)變量進(jìn)行操作時(shí)的可見性,即一個(gè)線程修改了某個(gè)變量的值,這新值對其他線程來說是立即可見的。
2)禁止進(jìn)行指令重排序。
可見性。對一個(gè)volatiole變量的讀,總是能看到(任意線程)對這個(gè)volatile變量最后的寫入。
有序性。volatile關(guān)鍵字能禁止指令重排序,所以volatile能在一定程度上保證有序性。
volatile關(guān)鍵字禁止指令重排序有兩層意思:
1)當(dāng)程序執(zhí)行到volatile變量的讀操作或者寫操作時(shí),在其前面的操作的更改肯定全部已經(jīng)進(jìn)行,且結(jié)果已經(jīng)對后面的操作可見;在其后面的操作肯定還沒有進(jìn)行;
2)在進(jìn)行指令優(yōu)化時(shí),不能將在對volatile變量訪問的語句放在其后面執(zhí)行,也不能把volatile變量后面的語句放到其前面執(zhí)行。
可能上面說的比較繞,舉個(gè)簡單的例子:
//x、y為非volatile變量 //flag為volatile變量 x = 2; //語句1 y = 0; //語句2 flag = true; //語句3 x = 4; //語句4 y = -1; //語句5
由于flag變量為volatile變量,那么在進(jìn)行指令重排序的過程的時(shí)候,不會(huì)將語句3放到語句1、語句2前面,也不會(huì)講語句3放到語句4、語句5后面。但是要注意語句1和語句2的順序、語句4和語句5的順序是不作任何保證的。
原子性。對任意單個(gè)volatile變量的讀、寫具有原子性,但類似于volatile++這種復(fù)合操作不具有原子性。
volatile寫的內(nèi)存語義:當(dāng)寫一個(gè)volatile變量時(shí),JMM會(huì)把該線程對應(yīng)的本地內(nèi)存中的共享變量值刷新到主內(nèi)存。
volatile讀的內(nèi)存語義:當(dāng)讀一個(gè)volatile變量時(shí),JMM會(huì)把該線程對應(yīng)的本地內(nèi)存置位無效。線程接下來將從主內(nèi)存中讀取共享變量。(強(qiáng)制從主內(nèi)存讀取共享變量,把本地內(nèi)存與主內(nèi)存的共享變量的值變成一致)。
volatile寫和讀的內(nèi)存語義總結(jié)總結(jié):
線程A寫一個(gè)volatile變量,實(shí)質(zhì)上是線程A向接下來將要讀這個(gè)volatile變量的某個(gè)線程發(fā)出了(其對變量所做修改的)消息。
線程B讀一個(gè)volatile變量,實(shí)質(zhì)上是線程B接收了之前某個(gè)線程發(fā)出的消息。
線程A寫一個(gè)volatile變量,隨后線程B讀這個(gè)volatile變量,這個(gè)過程實(shí)質(zhì)上是線程A通過主內(nèi)存向線程B發(fā)送消息。(隱式通信)
前面提到過編譯器重排序和處理器重排序。為了實(shí)現(xiàn)volatile內(nèi)存語義,JMM分別限制了這兩種類型的重排序類型。
當(dāng)?shù)诙€(gè)操作是volatile寫時(shí),不管第一個(gè)操作是什么,都不能重排序。這個(gè)規(guī)則確保volatile寫之前的操作不會(huì)被編譯器重排序到volatile寫之后。
當(dāng)?shù)谝粋€(gè)操作是volatile讀時(shí),不管第二個(gè)操作是什么,都不能重排序。這個(gè)規(guī)則確保volatile讀之后的操作不會(huì)被編譯器重排序到volatile讀之前。
當(dāng)?shù)谝粋€(gè)操作是volatile寫時(shí),第二個(gè)操作是volatile讀時(shí),不能重排序。
為了實(shí)現(xiàn)volatile的內(nèi)存語義,編譯器在生成字節(jié)碼時(shí),會(huì)在指令序列中插入內(nèi)存屏障來禁止特定類型的處理器重排序。對于編譯器來說,發(fā)現(xiàn)一個(gè)最優(yōu)布置來最小化插入屏障的總數(shù)幾乎不可能。為此,JMM采取保守策略。下面是基于保守策略的JMM內(nèi)存屏障插入策略:
在每個(gè)volatile寫操作的前面插入一個(gè)StoreStore屏障。
在每個(gè)volatile寫操作的后面插入一個(gè)StoreLoad屏障。
在每個(gè)volatile讀操作的后面插入一個(gè)LoadLoad屏障。
在每個(gè)volatile讀操作的后面插入一個(gè)LoadStore屏障。
上述內(nèi)存屏障插入策略非常保守,但它可以保證在任意處理器平臺(tái),任意的程序中都能得到正確的volatile內(nèi)存語義。
下面是保守策略下,volatile寫插入內(nèi)存屏障后生成的指令序列示意圖:
上圖中的StoreStore屏障可以保證在volatile寫之前,其前面的所有普通寫操作已經(jīng)對任意處理器可見了。這是因?yàn)镾toreStore屏障將保障上面所有的普通寫在volatile寫之前刷新到主內(nèi)存。
這里比較有意思的是volatile寫后面的StoreLoad屏障。這個(gè)屏障的作用是避免volatile寫與后面可能有的volatile讀/寫操作重排序。因?yàn)榫幾g器常常無法準(zhǔn)確判斷在一個(gè)volatile寫的后面,是否需要插入一個(gè)StoreLoad屏障(比如,一個(gè)volatile寫之后方法立即return)。為了保證能正確實(shí)現(xiàn)volatile的內(nèi)存語義,JMM在這里采取了保守策略:在每個(gè)volatile寫的后面或在每個(gè)volatile讀的前面插入一個(gè)StoreLoad屏障。從整體執(zhí)行效率的角度考慮,JMM選擇了在每個(gè)volatile寫的后面插入一個(gè)StoreLoad屏障。因?yàn)関olatile寫-讀內(nèi)存語義的常見使用模式是:一個(gè)寫線程寫volatile變量,多個(gè)讀線程讀同一個(gè)volatile變量。當(dāng)讀線程的數(shù)量大大超過寫線程時(shí),選擇在volatile寫之后插入StoreLoad屏障將帶來可觀的執(zhí)行效率的提升。從這里我們可以看到JMM在實(shí)現(xiàn)上的一個(gè)特點(diǎn):首先確保正確性,然后再去追求執(zhí)行效率。下面是在保守策略下,volatile讀插入內(nèi)存屏障后生成的指令序列示意圖:
上圖中的LoadLoad屏障用來禁止處理器把上面的volatile讀與下面的普通讀重排序。LoadStore屏障用來禁止處理器把上面的volatile讀與下面的普通寫重排序。
上述volatile寫和volatile讀的內(nèi)存屏障插入策略非常保守。在實(shí)際執(zhí)行時(shí),只要不改變volatile寫-讀的內(nèi)存語義,編譯器可以根據(jù)具體情況省略不必要的屏障。下面我們通過具體的示例代碼來說明:
class VolatileBarrierExample { int a; volatile int v1 = 1; volatile int v2 = 2; void readAndWrite() { int i = v1; //第一個(gè)volatile讀 int j = v2; // 第二個(gè)volatile讀 a = i + j; //普通寫 v1 = i + 1; // 第一個(gè)volatile寫 v2 = j * 2; //第二個(gè) volatile寫 } … //其他方法 }
針對readAndWrite()方法,編譯器在生成字節(jié)碼時(shí)可以做如下的優(yōu)化:
注意,最后的StoreLoad屏障不能省略。因?yàn)榈诙€(gè)volatile寫之后,方法立即return。此時(shí)編譯器可能無法準(zhǔn)確斷定后面是否會(huì)有volatile讀或?qū)?,為了安全起見,編譯器常常會(huì)在這里插入一個(gè)StoreLoad屏障。
上面的優(yōu)化是針對任意處理器平臺(tái),由于不同的處理器有不同“松緊度”的處理器內(nèi)存模型,內(nèi)存屏障的插入還可以根據(jù)具體的處理器內(nèi)存模型繼續(xù)優(yōu)化。以x86處理器為例,上圖中除最后的StoreLoad屏障外,其它的屏障都會(huì)被省略。
為了提供一種比鎖更輕量級(jí)的線程之間通信的機(jī)制,JSR-133專家組決定增強(qiáng)volatile的內(nèi)存語義:嚴(yán)格限制編譯器和處理器對volatile變量與普通變量的重排序,確保volatile的寫-讀和鎖的釋放-獲取具有相同的內(nèi)存語義。
由于volatile僅僅保證對單個(gè)volatile變量的讀/寫具有原子性,而鎖的互斥執(zhí)行的特性可以確保對整個(gè)臨界區(qū)代碼的執(zhí)行具有原子性。在功能上,鎖比volatile更強(qiáng)大;在可伸縮性和執(zhí)行性能上,volatile更有優(yōu)勢。
當(dāng)一個(gè)變量被定義為volatile之后,就可以保證此變量對所有線程的可見性,即當(dāng)一個(gè)線程修改了此變量的值的時(shí)候,變量新的值對于其他線程來說是可以立即得知的。可以理解成:對volatile變量所有的寫操作都能立刻被其他線程得知。但是這并不代表基于volatile變量的運(yùn)算在并發(fā)下是安全的,因?yàn)関olatile只能保證內(nèi)存可見性,卻沒有保證對變量操作的原子性。比如下面的代碼:
/ * * 發(fā)起20個(gè)線程,每個(gè)線程對race變量進(jìn)行10000次自增操作,如果代碼能夠正確并發(fā), * 則最終race的結(jié)果應(yīng)為200000,但實(shí)際的運(yùn)行結(jié)果卻小于200000。 * * @author Colin Wang */ public class Test { public static volatile int race = 0; public static void increase() { race++; } private static final int THREADS_COUNT = 20; public static void main(String[] args) { Thread[] threads = new Thread[THREADS_COUNT]; for (int i = 0; i < THREADS_COUNT; i++) { threads[i] = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 10000; i++) { increase(); } } }); threads[i].start(); } while (Thread.activeCount() > 1) Thread.yield(); System.out.println(race); } }
按道理來說結(jié)果是10000,但是運(yùn)行下很可能是個(gè)小于10000的值。有人可能會(huì)說volatile不是保證了可見性啊,一個(gè)線程對race的修改,另外一個(gè)線程應(yīng)該立刻看到??!可是這里的操作race++是個(gè)復(fù)合操作啊,包括讀取race的值,對其自增,然后再寫回主存。
假設(shè)線程A,讀取了race的值為10,這時(shí)候被阻塞了,因?yàn)闆]有對變量進(jìn)行修改,觸發(fā)不了volatile規(guī)則。
線程B此時(shí)也讀讀race的值,主存里race的值依舊為10,做自增,然后立刻就被寫回主存了,為11。
此時(shí)又輪到線程A執(zhí)行,由于工作內(nèi)存里保存的是10,所以繼續(xù)做自增,再寫回主存,11又被寫了一遍。所以雖然兩個(gè)線程執(zhí)行了兩次increase(),結(jié)果卻只加了一次。
有人說,volatile不是會(huì)使緩存行無效的嗎?但是這里線程A讀取到線程B也進(jìn)行操作之前,并沒有修改inc值,所以線程B讀取的時(shí)候,還是讀的10。
又有人說,線程B將11寫回主存,不會(huì)把線程A的緩存行設(shè)為無效嗎?但是線程A的讀取操作已經(jīng)做過了啊,只有在做讀取操作時(shí),發(fā)現(xiàn)自己緩存行無效,才會(huì)去讀主存的值,所以這里線程A只能繼續(xù)做自增了。
綜上所述,在這種復(fù)合操作的情景下,原子性的功能是維持不了了。但是volatile在上面那種設(shè)置flag值的例子里,由于對flag的讀/寫操作都是單步的,所以還是能保證原子性的。
要想保證原子性,只能借助于synchronized,Lock以及并發(fā)包下的atomic的原子操作類了,即對基本數(shù)據(jù)類型的 自增(加1操作),自減(減1操作)、以及加法操作(加一個(gè)數(shù)),減法操作(減一個(gè)數(shù))進(jìn)行了封裝,保證這些操作是原子性操作。
Java 理論與實(shí)踐: 正確使用 Volatile 變量 總結(jié)了volatile關(guān)鍵的使用場景,
只能在有限的一些情形下使用 volatile 變量替代鎖。要使 volatile 變量提供理想的線程安全,必須同時(shí)滿足下面兩個(gè)條件:
對變量的寫操作不依賴于當(dāng)前值。
該變量沒有包含在具有其他變量的不變式中。
實(shí)際上,這些條件表明,可以被寫入 volatile 變量的這些有效值獨(dú)立于任何程序的狀態(tài),包括變量的當(dāng)前狀態(tài)。
第一個(gè)條件的限制使 volatile 變量不能用作線程安全計(jì)數(shù)器。雖然增量操作(x++
)看上去類似一個(gè)單獨(dú)操作,實(shí)際上它是一個(gè)由讀?。薷模瓕懭氩僮餍蛄薪M成的組合操作,必須以原子方式執(zhí)行,而 volatile 不能提供必須的原子特性。實(shí)現(xiàn)正確的操作需要使x
的值在操作期間保持不變,而 volatile 變量無法實(shí)現(xiàn)這點(diǎn)。(然而,如果將值調(diào)整為只從單個(gè)線程寫入,那么可以忽略第一個(gè)條件。)
volatile一個(gè)使用場景是狀態(tài)位;還有只有一個(gè)線程寫,其余線程讀的場景
鎖可以讓臨界區(qū)互斥執(zhí)行。鎖的釋放-獲取的內(nèi)存語義與volatile變量寫-讀的內(nèi)存語義很像。
當(dāng)線程釋放鎖時(shí),JMM會(huì)把該線程對應(yīng)的本地內(nèi)存中的共享變量刷新到主內(nèi)存中。
當(dāng)線程獲取鎖時(shí),JMM會(huì)把該線程對應(yīng)的本地內(nèi)存置位無效,從而使得被監(jiān)視器保護(hù)的臨界區(qū)代碼必須從主內(nèi)存中讀取共享變量。
不難發(fā)現(xiàn):鎖釋放與volatile寫有相同的內(nèi)存語音;鎖獲取與volatile讀有相同的內(nèi)存語義。
下面對鎖釋放和鎖獲取的內(nèi)存語義做個(gè)總結(jié)。
線程A釋放一個(gè)鎖,實(shí)質(zhì)上是線程A向接下來將要獲取這個(gè)鎖的某個(gè)線程發(fā)出了(線程A對共享變量所做修改的)消息。
線程B獲取一個(gè)鎖,實(shí)質(zhì)上是線程B接收了之前某個(gè)線程發(fā)出的(在釋放這個(gè)鎖之前對共享變量所做修改)的消息。
線程A釋放鎖,隨后線程B獲取這個(gè)鎖,這個(gè)過程實(shí)質(zhì)上是線程A通過主內(nèi)存向線程B發(fā)送消息。
與前面介紹的鎖和volatile想比,對final域的讀和寫更像是普通的變量訪問。
對于final域,編譯器和處理器要遵循兩個(gè)重排序規(guī)則:
1.在構(gòu)造函數(shù)內(nèi)對一個(gè)final域的寫入,與隨后把這個(gè)被構(gòu)造對象的引用賦值給一個(gè)引用變量,這兩個(gè)操作之間不能重排序。
2.初次讀一個(gè)包含final域的對象的應(yīng)用,與隨后初次讀這個(gè)final域,這兩個(gè)操作之間不能重排序
下面通過一個(gè)示例來分別說明這兩個(gè)規(guī)則:
public class FinalTest { int i;//普通變量 final int j; static FinalExample obj; public FinalExample(){ i = 1; j = 2; } public static void writer(){ obj = new FinalExample(); } public static void reader(){ FinalExample object = obj;//讀對象引用 int a = object.i; int b = object.j; } }
這里假設(shè)一個(gè)線程A執(zhí)行writer()方法,隨后另一個(gè)線程B執(zhí)行reader()方法。下面我們通過這兩個(gè)線程的交互來說明這兩個(gè)規(guī)則。
寫final域的重排序規(guī)則禁止把final域的寫重排序到構(gòu)造函數(shù)之外。這個(gè)規(guī)則的實(shí)現(xiàn)包含下面兩個(gè)方面。
1)JMM禁止編譯器把final域的寫重排序到構(gòu)造函數(shù)之外。
2)編譯器會(huì)在final域的寫之后,構(gòu)造函數(shù)return之前,插入一個(gè)StoreStore屏障。這個(gè)屏障禁止處理器把final域的寫重排序到構(gòu)造函數(shù)之外。
現(xiàn)在讓我們分析writer方法,writer方法只包含一行代碼obj = new FinalTest();這行代碼包含兩個(gè)步驟:
1)構(gòu)造一個(gè)FinalTest類型的對象
2)把這個(gè)對象的引用賦值給obj
假設(shè)線程B的讀對象引用與讀對象的成員域之間沒有重排序,下圖是一種可能的執(zhí)行時(shí)序
在上圖中,寫普通域的操作被編譯器重排序到了構(gòu)造函數(shù)之外,讀線程B錯(cuò)誤的讀取到了普通變量i初始化之前的值。而寫final域的操作被寫final域重排序的規(guī)則限定在了構(gòu)造函數(shù)之內(nèi),讀線程B正確的讀取到了final變量初始化之后的值。
寫final域的重排序規(guī)則可以確保:在對象引用為任意線程可見之前,對象的final域已經(jīng)被初始化了,而普通變量不具有這個(gè)保證。以上圖為例,讀線程B看到對象obj的時(shí)候,很可能obj對象還沒有構(gòu)造完成(對普通域i的寫操作被重排序到構(gòu)造函數(shù)外,此時(shí)初始值1還沒有寫入普通域i)
讀final域的重排序規(guī)則是:在一個(gè)線程中,初次讀對象的引用與初次讀這個(gè)對象包含的final域,JMM禁止重排序這兩個(gè)操作(該規(guī)則僅僅針對處理器)。編譯器會(huì)在讀final域的操作前面加一個(gè)LoadLoad屏障。
初次讀對象引用與初次讀該對象包含的final域,這兩個(gè)操作之間存在間接依賴關(guān)系。由于編譯器遵守間接依賴關(guān)系,因此編譯器不會(huì)重排序這兩個(gè)操作。大多數(shù)處理器也會(huì)遵守間接依賴,也不會(huì)重排序這兩個(gè)操作。但有少數(shù)處理器允許對存在間接依賴關(guān)系的操作做重排序(比如alpha處理器),這個(gè)規(guī)則就是專門用來針對這種處理器的。
上面的例子中,reader方法包含三個(gè)操作
1)初次讀引用變量obj
2)初次讀引用變量指向?qū)ο蟮钠胀ㄓ?/p>
3)初次讀引用變量指向?qū)ο蟮膄inal域
現(xiàn)在假設(shè)寫線程A沒有發(fā)生任何重排序,同時(shí)程序在不遵守間接依賴的處理器上執(zhí)行,下圖是一種可能的執(zhí)行時(shí)序:
在上圖中,讀對象的普通域操作被處理器重排序到讀對象引用之前。在讀普通域時(shí),該域還沒有被寫線程寫入,這是一個(gè)錯(cuò)誤的讀取操作,而讀final域的重排序規(guī)則會(huì)把讀對象final域的操作“限定”在讀對象引用之后,此時(shí)該final域已經(jīng)被A線程初始化過了,這是一個(gè)正確的讀取操作。
讀final域的重排序規(guī)則可以確保:在讀一個(gè)對象的final域之前,一定會(huì)先讀包含這個(gè)final域的對象的引用。在這個(gè)示例程序中,如果該引用不為null,那么引用對象的final域一定已經(jīng)被A線程初始化過了。
final域?yàn)橐妙愋停厦嫖覀兛吹降膄inal域是基礎(chǔ)的數(shù)據(jù)類型,如果final域是引用類型呢?
public class FinalReferenceTest { final int[] arrs;//final引用 static FinalReferenceTest obj; public FinalReferenceTest(){ arrs = new int[1];//1 arrs[0] = 1;//2 } public static void write0(){//A線程 obj = new FinalReferenceTest();//3 } public static void write1(){//線程B obj.arrs[0] = 2;//4 } public static void reader(){//C線程 if(obj!=null){//5 int temp =obj.arrs[0];//6 } } }
JMM可以確保讀線程C至少能看到寫線程A在構(gòu)造函數(shù)中對final引用對象的成員域的寫入。即C至少能看到數(shù)組下標(biāo)0的值為1。而寫線程B對數(shù)組元素的寫入,讀線程C可能看得到,也可能看不到。JMM不保證線程B的寫入對讀線程C可見,因?yàn)閷懢€程B和讀線程C之間存在數(shù)據(jù)競爭,此時(shí)的執(zhí)行結(jié)果不可預(yù)知。
如果想要確保讀線程C看到寫線程B對數(shù)組元素的寫入,寫線程B和讀線程C之間需要使用同步原語(lock或volatile)來確保內(nèi)存可見性。
前面我們提到過,寫final域的重排序規(guī)則可以確保:在引用變量為任意線程可見之前,該引用變量指向的對象的final域已經(jīng)在構(gòu)造函數(shù)中被正確初始化過了。其實(shí),要得到這個(gè)效果,還需要一個(gè)保證:在構(gòu)造函數(shù)內(nèi)部,不能讓這個(gè)被構(gòu)造對象的引用為其他線程所見,也就是對象引用不能在構(gòu)造函數(shù)中“逸出”。
public class FinalReferenceEscapeExample {final int i;static FinalReferenceEscapeExample obj;public FinalReferenceEscapeExample () { i = 1; // 1寫final域 obj = this; // 2 this引用在此"逸出" } public static void writer() {new FinalReferenceEscapeExample (); }public static void reader() {if (obj != null) { // 3 int temp = obj.i; // 4 } } }
假設(shè)一個(gè)線程A執(zhí)行writer()方法,另一個(gè)線程B執(zhí)行reader()方法。這里的操作2使得對象還未完成構(gòu)造前就為線程B可見。即使這里的操作2是構(gòu)造函數(shù)的最后一步,且在程序中操作2排在操作1后面,執(zhí)行read()方法的線程仍然可能無法看到final域被初始化后的值,因?yàn)檫@里的操作1和操作2之間可能被重排序。
JSR-133為什么要增強(qiáng)final的語義:
通過為final域增加寫和讀重排序規(guī)則,可以為Java程序員提供初始化安全保證:只要對象是正確構(gòu)造的(被構(gòu)造對象的引用在構(gòu)造函數(shù)中沒有“逸出”),那么不需要使用同步(指lock和volatile的使用)就可以保證任意線程都能看到這個(gè)final域在構(gòu)造函數(shù)中被初始化之后的值。
JMM是圍繞這在并發(fā)過程中如何處理原子性、可見性和有序性這3個(gè)特性來建立的。
原子性:
Java中,對基本數(shù)據(jù)類型的讀取和賦值操作是原子性操作,所謂原子性操作就是指這些操作是不可中斷的,要做一定做完,要么就沒有執(zhí)行。比如:
i = 2;j = i;i++;i = i + 1;
上面4個(gè)操作中,i=2是讀取操作,必定是原子性操作,j=i你以為是原子性操作,其實(shí)吧,分為兩步,一是讀取i的值,然后再賦值給j,這就是2步操作了,稱不上原子操作,i++和i = i + 1其實(shí)是等效的,讀取i的值,加1,再寫回主存,那就是3步操作了。所以上面的舉例中,最后的值可能出現(xiàn)多種情況,就是因?yàn)闈M足不了原子性。
JMM只能保證對單個(gè)volatile變量的讀/寫具有原子性,但類似于volatile++這種符合操作不具有原子性,這時(shí)候就必須借助于synchronized和Lock來保證整塊代碼的原子性了。線程在釋放鎖之前,必然會(huì)把i的值刷回到主存的。
可見性:可見性指當(dāng)一個(gè)線程修改了共享變量的值,其他線程能夠立即得知這個(gè)修改。Java內(nèi)存模型是通過在變量修改后將新值同步回主內(nèi)存,在變量讀取前從主內(nèi)存刷新變量值這種依賴主內(nèi)存作為傳遞媒介的方式來實(shí)現(xiàn)可見性。
無論是普通變量還是volatile變量,它們的區(qū)別是:volatile的特殊規(guī)則保證了新值能立即同步到主內(nèi)存,以及每次使用前立即從主內(nèi)存刷新。因?yàn)?,可以說volatile保證了多線程操作時(shí)變量的可見性,而普通變量不能保證這一點(diǎn)。
除了volatile之外,java中還有2個(gè)關(guān)鍵字能實(shí)現(xiàn)可見性,即synchronized和final(final修飾的變量,線程安全級(jí)別最高)。同步塊的可見性是由“對一個(gè)變量執(zhí)行unlock操作之前,必須先把此變量同步回主內(nèi)存中(執(zhí)行store,write操作)”這條規(guī)則獲得;而final關(guān)鍵字的可見性是指:被final修飾的字段在構(gòu)造器中一旦初始化完成,并且構(gòu)造器沒有把“this”的引用傳遞出去(this引用逃逸是一件很危險(xiǎn)的事,其他線程有可能通過這個(gè)引用訪問到“初始化了一半”的對象),那么在其他線程中就能看到final字段的值。
有序性:JMM的有序性在講解volatile時(shí)詳細(xì)的討論過,java程序中天然的有序性可以總結(jié)為一句話:如果在本線程內(nèi)觀察,所有操作都是有序的;如果在一個(gè)線程中觀察另一個(gè)線程,所有操作都是無序的。前半句是指“線程內(nèi)表現(xiàn)為串行的語義”,后半句指的是“指令重排”現(xiàn)象和“工作內(nèi)存與主內(nèi)存同步延遲”現(xiàn)象。
前半句可以用JMM規(guī)定的as-if-serial語義來解決,后半句可以用JMM規(guī)定的happens-before原則來解決。Java語義提供了volatile和synchronized兩個(gè)關(guān)鍵字來保證線程之間操作的有序性,volatile關(guān)鍵字本身就包含了禁止指令重排的語義,而synchronized則是由“一個(gè)變量在同一個(gè)時(shí)刻只允許一條線程對其進(jìn)行l(wèi)ock操作”這條規(guī)則獲取的。這個(gè)規(guī)則決定了持有同一個(gè)鎖的兩個(gè)同步塊只能串行的進(jìn)入。
到此,關(guān)于“Java內(nèi)存區(qū)域與內(nèi)存模型詳解”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實(shí)踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識(shí),請繼續(xù)關(guān)注創(chuàng)新互聯(lián)-成都網(wǎng)站建設(shè)公司網(wǎng)站,小編會(huì)繼續(xù)努力為大家?guī)砀鄬?shí)用的文章!
網(wǎng)頁標(biāo)題:Java內(nèi)存區(qū)域與內(nèi)存模型詳解-創(chuàng)新互聯(lián)
路徑分享:http://jinyejixie.com/article6/ccssog.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供外貿(mào)建站、品牌網(wǎng)站建設(shè)、Google、網(wǎng)站建設(shè)、App設(shè)計(jì)、搜索引擎優(yōu)化
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來源: 創(chuàng)新互聯(lián)
猜你還喜歡下面的內(nèi)容