關(guān)于synchronized
的底層實(shí)現(xiàn),網(wǎng)上有很多文章了。但是很多文章要么作者根本沒看代碼,僅僅是根據(jù)網(wǎng)上其他文章總結(jié)、照搬而成,難免有些錯(cuò)誤;要么很多點(diǎn)都是一筆帶過,對(duì)于為什么這樣實(shí)現(xiàn)沒有一個(gè)說法,讓像我這樣的讀者意猶未盡。
我們提供的服務(wù)有:成都網(wǎng)站制作、成都網(wǎng)站建設(shè)、外貿(mào)營銷網(wǎng)站建設(shè)、微信公眾號(hào)開發(fā)、網(wǎng)站優(yōu)化、網(wǎng)站認(rèn)證、晉中ssl等。為1000多家企事業(yè)單位解決了網(wǎng)站和推廣的問題。提供周到的售前咨詢和貼心的售后服務(wù),是有科學(xué)管理、有技術(shù)的晉中網(wǎng)站制作公司
本系列文章將對(duì)HotSpot的synchronized
鎖實(shí)現(xiàn)進(jìn)行全面分析,內(nèi)容包括偏向鎖、輕量級(jí)鎖、重量級(jí)鎖的加鎖、解鎖、鎖升級(jí)流程的原理及源碼分析,希望給在研究synchronized
路上的同學(xué)一些幫助。
?
大概花費(fèi)了兩周的實(shí)現(xiàn)看代碼(花費(fèi)了這么久時(shí)間有些懺愧,主要是對(duì)C++、JVM底層機(jī)制、JVM調(diào)試以及匯編代碼不太熟),將synchronized
涉及到的代碼基本都看了一遍,其中還包括在JVM中添加日志驗(yàn)證自己的猜想,總的來說目前對(duì)synchronized
這塊有了一個(gè)比較全面清晰的認(rèn)識(shí),但水平有限,有些細(xì)節(jié)難免有些疏漏,還望請(qǐng)大家指正。
本篇文章將對(duì)synchronized
機(jī)制做個(gè)大致的介紹,包括用以承載鎖狀態(tài)的對(duì)象頭、鎖的幾種形式、各種形式鎖的加鎖和解鎖流程、什么時(shí)候會(huì)發(fā)生鎖升級(jí)。需要注意的是本文旨在介紹背景和概念,在講述一些流程的時(shí)候,只提到了主要case,對(duì)于實(shí)現(xiàn)細(xì)節(jié)、運(yùn)行時(shí)的不同分支都在后面的文章中詳細(xì)分析。
本人看的JVM版本是jdk8u,具體版本號(hào)以及代碼可以在這里看到。
Java中提供了兩種實(shí)現(xiàn)同步的基礎(chǔ)語義:synchronized
方法和synchronized
塊, 我們來看個(gè)demo:
public?class?SyncTest?{ ????public?void?syncBlock(){ ????????synchronized?(this){ ????????????System.out.println("hello?block"); ????????} ????} ????public?synchronized?void?syncMethod(){ ????????System.out.println("hello?method"); ????} }
當(dāng)SyncTest.java被編譯成class文件的時(shí)候,synchronized
關(guān)鍵字和synchronized
方法的字節(jié)碼略有不同,我們可以用javap -v
?命令查看class文件對(duì)應(yīng)的JVM字節(jié)碼信息,部分信息如下:
{ ??public?void?syncBlock(); ????descriptor:?()V ????flags:?ACC_PUBLIC ????Code: ??????stack=2,?locals=3,?args_size=1 ?????????0:?aload_0 ?????????1:?dup ?????????2:?astore_1 ?????????3:?monitorenter ? ??//?monitorenter指令進(jìn)入同步塊 ?????????4:?getstatic?????#2??????????????????//?Field?java/lang/System.out:Ljava/io/PrintStream; ?????????7:?ldc???????????#3??????????????????//?String?hello?block ?????????9:?invokevirtual?#4??????????????????//?Method?java/io/PrintStream.println:(Ljava/lang/String;)V ????????12:?aload_1 ????????13:?monitorexit ??//?monitorexit指令退出同步塊 ????????14:?goto??????????22 ????????17:?astore_2 ????????18:?aload_1 ????????19:?monitorexit ??//?monitorexit指令退出同步塊 ????????20:?aload_2 ????????21:?athrow ????????22:?return ??????Exception?table: ?????????from????to??target?type ?????????????4????14????17???any ????????????17????20????17???any ? ??public?synchronized?void?syncMethod(); ????descriptor:?()V ????flags:?ACC_PUBLIC,?ACC_SYNCHRONIZED??????//添加了ACC_SYNCHRONIZED標(biāo)記 ????Code: ??????stack=2,?locals=1,?args_size=1 ?????????0:?getstatic?????#2??????????????????//?Field?java/lang/System.out:Ljava/io/PrintStream; ?????????3:?ldc???????????#5??????????????????//?String?hello?method ?????????5:?invokevirtual?#4??????????????????//?Method?java/io/PrintStream.println:(Ljava/lang/String;)V ?????????8:?return ? }
從上面的中文注釋處可以看到,對(duì)于synchronized
關(guān)鍵字而言,javac
在編譯時(shí),會(huì)生成對(duì)應(yīng)的monitorenter
和monitorexit
指令分別對(duì)應(yīng)synchronized
同步塊的進(jìn)入和退出,有兩個(gè)monitorexit
指令的原因是:為了保證拋異常的情況下也能釋放鎖,所以javac
為同步代碼塊添加了一個(gè)隱式的try-finally,在finally中會(huì)調(diào)用monitorexit
命令釋放鎖。而對(duì)于synchronized
方法而言,javac
為其生成了一個(gè)ACC_SYNCHRONIZED
關(guān)鍵字,在JVM進(jìn)行方法調(diào)用時(shí),發(fā)現(xiàn)調(diào)用的方法被ACC_SYNCHRONIZED
修飾,則會(huì)先嘗試獲得鎖。
在JVM底層,對(duì)于這兩種synchronized
語義的實(shí)現(xiàn)大致相同,在后文中會(huì)選擇一種進(jìn)行詳細(xì)分析。
因?yàn)楸疚闹荚诜治?code>synchronized的實(shí)現(xiàn)原理,因此對(duì)于其使用的一些問題就不贅述了,不了解的朋友可以看看這篇文章。
傳統(tǒng)的鎖(也就是下文要說的重量級(jí)鎖)依賴于系統(tǒng)的同步函數(shù),在linux上使用mutex
互斥鎖,最底層實(shí)現(xiàn)依賴于futex
,關(guān)于futex
可以看我之前的文章,這些同步函數(shù)都涉及到用戶態(tài)和內(nèi)核態(tài)的切換、進(jìn)程的上下文切換,成本較高。對(duì)于加了synchronized
關(guān)鍵字但運(yùn)行時(shí)并沒有多線程競(jìng)爭(zhēng),或兩個(gè)線程接近于交替執(zhí)行的情況,使用傳統(tǒng)鎖機(jī)制無疑效率是會(huì)比較低的。
在JDK 1.6之前,synchronized
只有傳統(tǒng)的鎖機(jī)制,因此給開發(fā)者留下了synchronized
關(guān)鍵字相比于其他同步機(jī)制性能不好的印象。
在JDK 1.6引入了兩種新型鎖機(jī)制:偏向鎖和輕量級(jí)鎖,它們的引入是為了解決在沒有多線程競(jìng)爭(zhēng)或基本沒有競(jìng)爭(zhēng)的場(chǎng)景下因使用傳統(tǒng)鎖機(jī)制帶來的性能開銷問題。
在看這幾種鎖機(jī)制的實(shí)現(xiàn)前,我們先來了解下對(duì)象頭,它是實(shí)現(xiàn)多種鎖機(jī)制的基礎(chǔ)。
因?yàn)樵贘ava中任意對(duì)象都可以用作鎖,因此必定要有一個(gè)映射關(guān)系,存儲(chǔ)該對(duì)象以及其對(duì)應(yīng)的鎖信息(比如當(dāng)前哪個(gè)線程持有鎖,哪些線程在等待)。一種很直觀的方法是,用一個(gè)全局map,來存儲(chǔ)這個(gè)映射關(guān)系,但這樣會(huì)有一些問題:需要對(duì)map做線程安全保障,不同的synchronized
之間會(huì)相互影響,性能差;另外當(dāng)同步對(duì)象較多時(shí),該map可能會(huì)占用比較多的內(nèi)存。
所以最好的辦法是將這個(gè)映射關(guān)系存儲(chǔ)在對(duì)象頭中,因?yàn)閷?duì)象頭本身也有一些hashcode、GC相關(guān)的數(shù)據(jù),所以如果能將鎖信息與這些信息共存在對(duì)象頭中就好了。
在JVM中,對(duì)象在內(nèi)存中除了本身的數(shù)據(jù)外還會(huì)有個(gè)對(duì)象頭,對(duì)于普通對(duì)象而言,其對(duì)象頭中有兩類信息:mark word
和類型指針。另外對(duì)于數(shù)組而言還會(huì)有一份記錄數(shù)組長(zhǎng)度的數(shù)據(jù)。
類型指針是指向該對(duì)象所屬類對(duì)象的指針,mark word
用于存儲(chǔ)對(duì)象的HashCode、GC分代年齡、鎖狀態(tài)等信息。在32位系統(tǒng)上mark word
長(zhǎng)度為32bit,64位系統(tǒng)上長(zhǎng)度為64bit。為了能在有限的空間里存儲(chǔ)下更多的數(shù)據(jù),其存儲(chǔ)格式是不固定的,在32位系統(tǒng)上各狀態(tài)的格式如下:
可以看到鎖信息也是存在于對(duì)象的mark word
中的。當(dāng)對(duì)象狀態(tài)為偏向鎖(biasable)時(shí),mark word
存儲(chǔ)的是偏向的線程ID;當(dāng)狀態(tài)為輕量級(jí)鎖(lightweight locked)時(shí),mark word
存儲(chǔ)的是指向線程棧中Lock Record
的指針;當(dāng)狀態(tài)為重量級(jí)鎖(inflated)時(shí),為指向堆中的monitor對(duì)象的指針。
重量級(jí)鎖是我們常說的傳統(tǒng)意義上的鎖,其利用操作系統(tǒng)底層的同步機(jī)制去實(shí)現(xiàn)Java中的線程同步。
重量級(jí)鎖的狀態(tài)下,對(duì)象的mark word
為指向一個(gè)堆中monitor對(duì)象的指針。
一個(gè)monitor對(duì)象包括這么幾個(gè)關(guān)鍵字段:cxq(下圖中的ContentionList),EntryList ,WaitSet,owner。
其中cxq ,EntryList ,WaitSet都是由ObjectWaiter的鏈表結(jié)構(gòu),owner指向持有鎖的線程。
當(dāng)一個(gè)線程嘗試獲得鎖時(shí),如果該鎖已經(jīng)被占用,則會(huì)將該線程封裝成一個(gè)ObjectWaiter對(duì)象插入到cxq的隊(duì)列尾部,然后暫停當(dāng)前線程。當(dāng)持有鎖的線程釋放鎖前,會(huì)將cxq中的所有元素移動(dòng)到EntryList中去,并喚醒EntryList的隊(duì)首線程。
如果一個(gè)線程在同步塊中調(diào)用了Object#wait
方法,會(huì)將該線程對(duì)應(yīng)的ObjectWaiter從EntryList移除并加入到WaitSet中,然后釋放鎖。當(dāng)wait的線程被notify之后,會(huì)將對(duì)應(yīng)的ObjectWaiter從WaitSet移動(dòng)到EntryList中。
以上只是對(duì)重量級(jí)鎖流程的一個(gè)簡(jiǎn)述,其中涉及到的很多細(xì)節(jié),比如ObjectMonitor對(duì)象從哪來?釋放鎖時(shí)是將cxq中的元素移動(dòng)到EntryList的尾部還是頭部?notfiy時(shí),是將ObjectWaiter移動(dòng)到EntryList的尾部還是頭部?
關(guān)于具體的細(xì)節(jié),會(huì)在重量級(jí)鎖的文章中分析。
JVM的開發(fā)者發(fā)現(xiàn)在很多情況下,在Java程序運(yùn)行時(shí),同步塊中的代碼都是不存在競(jìng)爭(zhēng)的,不同的線程交替的執(zhí)行同步塊中的代碼。這種情況下,用重量級(jí)鎖是沒必要的。因此JVM引入了輕量級(jí)鎖的概念。
線程在執(zhí)行同步塊之前,JVM會(huì)先在當(dāng)前的線程的棧幀中創(chuàng)建一個(gè)Lock Record
,其包括一個(gè)用于存儲(chǔ)對(duì)象頭中的?mark word
(官方稱之為Displaced Mark Word
)以及一個(gè)指向?qū)ο蟮闹羔?。下圖右邊的部分就是一個(gè)Lock Record
。
1.在線程棧中創(chuàng)建一個(gè)Lock Record
,將其obj
(即上圖的Object reference)字段指向鎖對(duì)象。
2.直接通過CAS指令將Lock Record
的地址存儲(chǔ)在對(duì)象頭的mark word
中,如果對(duì)象處于無鎖狀態(tài)則修改成功,代表該線程獲得了輕量級(jí)鎖。如果失敗,進(jìn)入到步驟3。
3.如果是當(dāng)前線程已經(jīng)持有該鎖了,代表這是一次鎖重入。設(shè)置Lock Record
第一部分(Displaced Mark Word
)為null,起到了一個(gè)重入計(jì)數(shù)器的作用。然后結(jié)束。
4.走到這一步說明發(fā)生了競(jìng)爭(zhēng),需要膨脹為重量級(jí)鎖。
1.遍歷線程棧,找到所有obj
字段等于當(dāng)前鎖對(duì)象的Lock Record
。
2.如果Lock Record
的Displaced Mark Word
為null,代表這是一次重入,將obj
設(shè)置為null后continue。
3.如果Lock Record
的Displaced Mark Word
不為null,則利用CAS指令將對(duì)象頭的mark word
恢復(fù)成為Displaced Mark Word
。如果成功,則continue,否則膨脹為重量級(jí)鎖。
Java是支持多線程的語言,因此在很多二方包、基礎(chǔ)庫中為了保證代碼在多線程的情況下也能正常運(yùn)行,也就是我們常說的線程安全,都會(huì)加入如synchronized
這樣的同步語義。但是在應(yīng)用在實(shí)際運(yùn)行時(shí),很可能只有一個(gè)線程會(huì)調(diào)用相關(guān)同步方法。比如下面這個(gè)demo:
import?java.util.ArrayList; import?java.util.List; public?class?SyncDemo1?{ ????public?static?void?main(String[]?args)?{ ????????SyncDemo1?syncDemo1?=?new?SyncDemo1(); ????????for?(int?i?=?0;?i?<?100;?i++)?{ ????????????syncDemo1.addString("test:"?+?i); ????????} ????} ????private?List<String>?list?=?new?ArrayList<>(); ????public?synchronized?void?addString(String?s)?{ ????????list.add(s); ????} }
在這個(gè)demo中為了保證對(duì)list操縱時(shí)線程安全,對(duì)addString方法加了synchronized
的修飾,但實(shí)際使用時(shí)卻只有一個(gè)線程調(diào)用到該方法,對(duì)于輕量級(jí)鎖而言,每次調(diào)用addString時(shí),加鎖解鎖都有一個(gè)CAS操作;對(duì)于重量級(jí)鎖而言,加鎖也會(huì)有一個(gè)或多個(gè)CAS操作(這里的’一個(gè)‘、’多個(gè)‘?dāng)?shù)量詞只是針對(duì)該demo,并不適用于所有場(chǎng)景)。
在JDK1.6中為了提高一個(gè)對(duì)象在一段很長(zhǎng)的時(shí)間內(nèi)都只被一個(gè)線程用做鎖對(duì)象場(chǎng)景下的性能,引入了偏向鎖,在第一次獲得鎖時(shí),會(huì)有一個(gè)CAS操作,之后該線程再獲取鎖,只會(huì)執(zhí)行幾個(gè)簡(jiǎn)單的命令,而不是開銷相對(duì)較大的CAS命令。我們來看看偏向鎖是如何做的。
當(dāng)JVM啟用了偏向鎖模式(1.6以上默認(rèn)開啟),當(dāng)新創(chuàng)建一個(gè)對(duì)象的時(shí)候,如果該對(duì)象所屬的class沒有關(guān)閉偏向鎖模式(什么時(shí)候會(huì)關(guān)閉一個(gè)class的偏向模式下文會(huì)說,默認(rèn)所有class的偏向模式都是是開啟的),那新創(chuàng)建對(duì)象的mark word
將是可偏向狀態(tài),此時(shí)mark word中
的thread id(參見上文偏向狀態(tài)下的mark word
格式)為0,表示未偏向任何線程,也叫做匿名偏向(anonymously biased)。
case 1:當(dāng)該對(duì)象第一次被線程獲得鎖的時(shí)候,發(fā)現(xiàn)是匿名偏向狀態(tài),則會(huì)用CAS指令,將mark word
中的thread id由0改成當(dāng)前線程Id。如果成功,則代表獲得了偏向鎖,繼續(xù)執(zhí)行同步塊中的代碼。否則,將偏向鎖撤銷,升級(jí)為輕量級(jí)鎖。
case 2:當(dāng)被偏向的線程再次進(jìn)入同步塊時(shí),發(fā)現(xiàn)鎖對(duì)象偏向的就是當(dāng)前線程,在通過一些額外的檢查后(細(xì)節(jié)見后面的文章),會(huì)往當(dāng)前線程的棧中添加一條Displaced Mark Word
為空的Lock Record
中,然后繼續(xù)執(zhí)行同步塊的代碼,因?yàn)椴倏v的是線程私有的棧,因此不需要用到CAS指令;由此可見偏向鎖模式下,當(dāng)被偏向的線程再次嘗試獲得鎖時(shí),僅僅進(jìn)行幾個(gè)簡(jiǎn)單的操作就可以了,在這種情況下,synchronized
關(guān)鍵字帶來的性能開銷基本可以忽略。
case 3.當(dāng)其他線程進(jìn)入同步塊時(shí),發(fā)現(xiàn)已經(jīng)有偏向的線程了,則會(huì)進(jìn)入到撤銷偏向鎖的邏輯里,一般來說,會(huì)在safepoint
中去查看偏向的線程是否還存活,如果存活且還在同步塊中則將鎖升級(jí)為輕量級(jí)鎖,原偏向的線程繼續(xù)擁有鎖,當(dāng)前線程則走入到鎖升級(jí)的邏輯里;如果偏向的線程已經(jīng)不存活或者不在同步塊中,則將對(duì)象頭的mark word
改為無鎖狀態(tài)(unlocked),之后再升級(jí)為輕量級(jí)鎖。
由此可見,偏向鎖升級(jí)的時(shí)機(jī)為:當(dāng)鎖已經(jīng)發(fā)生偏向后,只要有另一個(gè)線程嘗試獲得偏向鎖,則該偏向鎖就會(huì)升級(jí)成輕量級(jí)鎖。當(dāng)然這個(gè)說法不絕對(duì),因?yàn)檫€有批量重偏向這一機(jī)制。
當(dāng)有其他線程嘗試獲得鎖時(shí),是根據(jù)遍歷偏向線程的lock record
來確定該線程是否還在執(zhí)行同步塊中的代碼。因此偏向鎖的解鎖很簡(jiǎn)單,僅僅將棧中的最近一條lock record
的obj
字段設(shè)置為null。需要注意的是,偏向鎖的解鎖步驟中并不會(huì)修改對(duì)象頭中的thread id。
下圖展示了鎖狀態(tài)的轉(zhuǎn)換流程:
另外,偏向鎖默認(rèn)不是立即就啟動(dòng)的,在程序啟動(dòng)后,通常有幾秒的延遲,可以通過命令?-XX:BiasedLockingStartupDelay=0
來關(guān)閉延遲。
從上文偏向鎖的加鎖解鎖過程中可以看出,當(dāng)只有一個(gè)線程反復(fù)進(jìn)入同步塊時(shí),偏向鎖帶來的性能開銷基本可以忽略,但是當(dāng)有其他線程嘗試獲得鎖時(shí),就需要等到safe point
時(shí)將偏向鎖撤銷為無鎖狀態(tài)或升級(jí)為輕量級(jí)/重量級(jí)鎖。safe point
這個(gè)詞我們?cè)贕C中經(jīng)常會(huì)提到,其代表了一個(gè)狀態(tài),在該狀態(tài)下所有線程都是暫停的(大概這么個(gè)意思),詳細(xì)可以看這篇文章??傊?,偏向鎖的撤銷是有一定成本的,如果說運(yùn)行時(shí)的場(chǎng)景本身存在多線程競(jìng)爭(zhēng)的,那偏向鎖的存在不僅不能提高性能,而且會(huì)導(dǎo)致性能下降。因此,JVM中增加了一種批量重偏向/撤銷的機(jī)制。
存在如下兩種情況:(見官方論文第4小節(jié)):
1.一個(gè)線程創(chuàng)建了大量對(duì)象并執(zhí)行了初始的同步操作,之后在另一個(gè)線程中將這些對(duì)象作為鎖進(jìn)行之后的操作。這種case下,會(huì)導(dǎo)致大量的偏向鎖撤銷操作。
2.存在明顯多線程競(jìng)爭(zhēng)的場(chǎng)景下使用偏向鎖是不合適的,例如生產(chǎn)者/消費(fèi)者隊(duì)列。
批量重偏向(bulk rebias)機(jī)制是為了解決第一種場(chǎng)景。批量撤銷(bulk revoke)則是為了解決第二種場(chǎng)景。
其做法是:以class為單位,為每個(gè)class維護(hù)一個(gè)偏向鎖撤銷計(jì)數(shù)器,每一次該class的對(duì)象發(fā)生偏向撤銷操作時(shí),該計(jì)數(shù)器+1,當(dāng)這個(gè)值達(dá)到重偏向閾值(默認(rèn)20)時(shí),JVM就認(rèn)為該class的偏向鎖有問題,因此會(huì)進(jìn)行批量重偏向。每個(gè)class對(duì)象會(huì)有一個(gè)對(duì)應(yīng)的epoch
字段,每個(gè)處于偏向鎖狀態(tài)對(duì)象的mark word中
也有該字段,其初始值為創(chuàng)建該對(duì)象時(shí),class中的epoch
的值。每次發(fā)生批量重偏向時(shí),就將該值+1,同時(shí)遍歷JVM中所有線程的棧,找到該class所有正處于加鎖狀態(tài)的偏向鎖,將其epoch
字段改為新值。下次獲得鎖時(shí),發(fā)現(xiàn)當(dāng)前對(duì)象的epoch
值和class的epoch
不相等,那就算當(dāng)前已經(jīng)偏向了其他線程,也不會(huì)執(zhí)行撤銷操作,而是直接通過CAS操作將其mark word
的Thread Id 改成當(dāng)前線程Id。
當(dāng)達(dá)到重偏向閾值后,假設(shè)該class計(jì)數(shù)器繼續(xù)增長(zhǎng),當(dāng)其達(dá)到批量撤銷的閾值后(默認(rèn)40),JVM就認(rèn)為該class的使用場(chǎng)景存在多線程競(jìng)爭(zhēng),會(huì)標(biāo)記該class為不可偏向,之后,對(duì)于該class的鎖,直接走輕量級(jí)鎖的邏輯。
Java中的synchronized
有偏向鎖、輕量級(jí)鎖、重量級(jí)鎖三種形式,分別對(duì)應(yīng)了鎖只被一個(gè)線程持有、不同線程交替持有鎖、多線程競(jìng)爭(zhēng)鎖三種情況。當(dāng)條件不滿足時(shí),鎖會(huì)按偏向鎖->輕量級(jí)鎖->重量級(jí)鎖 的順序升級(jí)。JVM種的鎖也是能降級(jí)的,只不過條件很苛刻,不在我們討論范圍之內(nèi)。該篇文章主要是對(duì)Java的synchronized
做個(gè)基本介紹,后文會(huì)有更詳細(xì)的分析。
?
網(wǎng)站題目:死磕Synchronized底層實(shí)現(xiàn),面試你還怕什么?
轉(zhuǎn)載注明:http://www.rwnh.cn/article0/jdciio.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供靜態(tài)網(wǎng)站、網(wǎng)站導(dǎo)航、微信公眾號(hào)、服務(wù)器托管、企業(yè)網(wǎng)站制作、標(biāo)簽優(yōu)化
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來源: 創(chuàng)新互聯(lián)