如何理解Java中volatile關(guān)鍵字

這篇文章主要介紹“如何理解Java中volatile關(guān)鍵字”,在日常操作中,相信很多人在如何理解Java中volatile關(guān)鍵字問(wèn)題上存在疑惑,小編查閱了各式資料,整理出簡(jiǎn)單好用的操作方法,希望對(duì)大家解答”如何理解Java中volatile關(guān)鍵字”的疑惑有所幫助!接下來(lái),請(qǐng)跟著小編一起來(lái)學(xué)習(xí)吧!

在云和等地區(qū),都構(gòu)建了全面的區(qū)域性戰(zhàn)略布局,加強(qiáng)發(fā)展的系統(tǒng)性、市場(chǎng)前瞻性、產(chǎn)品創(chuàng)新能力,以專注、極致的服務(wù)理念,為客戶提供做網(wǎng)站、成都網(wǎng)站建設(shè) 網(wǎng)站設(shè)計(jì)制作按需制作,公司網(wǎng)站建設(shè),企業(yè)網(wǎng)站建設(shè),高端網(wǎng)站設(shè)計(jì),營(yíng)銷型網(wǎng)站建設(shè),外貿(mào)網(wǎng)站制作,云和網(wǎng)站建設(shè)費(fèi)用合理。

CPU緩存

緩存模型

計(jì)算機(jī)中的所有運(yùn)算操作都是由CPU完成的,CPU指令執(zhí)行過(guò)程需要涉及數(shù)據(jù)讀取和寫入操作,但是CPU只能訪問(wèn)處于內(nèi)存中的數(shù)據(jù),而內(nèi)存的速度和CPU的速度是遠(yuǎn)遠(yuǎn)不對(duì)等的,因此就出現(xiàn)了緩存模型,也就是在CPU和內(nèi)存之間加入了緩存層。一般現(xiàn)代的CPU緩存層分為三級(jí),分別叫L1緩存、L2緩存和L3緩存,簡(jiǎn)略圖如下:

如何理解Java中volatile關(guān)鍵字

  • L1緩存:三級(jí)緩存中訪問(wèn)速度最快,但是容量最小,另外L1緩存還被劃分成了數(shù)據(jù)緩存(L1d,data首字母)和指令緩存(L1i,instruction首字母)

  • L2緩存:速度比L1慢,但是容量比L1大,在現(xiàn)代的多核CPU中,L2一般被單個(gè)核獨(dú)占

  • L3緩存:三級(jí)緩存中速度最慢,但是容量最大,現(xiàn)代CPU中也有L3是多核共享的設(shè)計(jì),比如zen3架構(gòu)的設(shè)計(jì)

如何理解Java中volatile關(guān)鍵字

緩存的出現(xiàn),是為了解決CPU直接訪問(wèn)內(nèi)存效率低下的問(wèn)題,CPU進(jìn)行運(yùn)算的時(shí)候,將需要的數(shù)據(jù)從主存復(fù)制一份到緩存中,因?yàn)榫彺娴脑L問(wèn)速度快于內(nèi)存,在計(jì)算的時(shí)候只需要讀取緩存并將結(jié)果更新到緩存,運(yùn)算結(jié)束再將結(jié)果刷新到主存,這樣就大大提高了計(jì)算效率,整體交互圖簡(jiǎn)略如下:

如何理解Java中volatile關(guān)鍵字

緩存一致性問(wèn)題

雖然緩存的出現(xiàn),大大提高了吞吐能力,但是,也引入了一個(gè)新的問(wèn)題,就是緩存不一致。比如,最簡(jiǎn)單的一個(gè)i++操作,需要將內(nèi)存數(shù)據(jù)復(fù)制一份到緩存中,CPU讀取緩存值并進(jìn)行更新,先寫入緩存,運(yùn)算結(jié)束后再將緩存中新的刷新到內(nèi)存,具體過(guò)程如下:

  • 讀取內(nèi)存中的i到緩存中

  • CPU讀取緩存i中的值

  • 對(duì)i進(jìn)行加1操作

  • 將結(jié)果寫回緩存

  • 再將數(shù)據(jù)刷新到主存

這樣的i++操作在單線程不會(huì)出現(xiàn)問(wèn)題,但在多線程中,因?yàn)槊總€(gè)線程都有自己的工作內(nèi)存(也叫本地內(nèi)存,是線程自己的緩存),變量i在多個(gè)線程的本地內(nèi)存中都存在一個(gè)副本,如果有兩個(gè)線程執(zhí)行i++操作:

  • 假設(shè)兩個(gè)線程為A、B,同時(shí)假設(shè)i初始值為0

  • 線程A從內(nèi)存中讀取i的值放入緩存中,此時(shí)i的值為0,線程B也同理,放入緩存中的值也是0

  • 兩個(gè)線程同時(shí)進(jìn)行自增操作,此時(shí)A、B線程的緩存中,i的值都是1

  • 兩個(gè)線程將i寫入主內(nèi)存,相當(dāng)于i被兩次賦值為1

  • 最終結(jié)果是i的值為1

這個(gè)就是典型的緩存不一致問(wèn)題,主流的解決辦法有:

  • 總線加鎖

  • 緩存一致性協(xié)議

總線加鎖

這是一種悲觀的實(shí)現(xiàn)方式,具體來(lái)說(shuō),就是通過(guò)處理器發(fā)出lock指令,鎖住總線,總線收到指令后,會(huì)阻塞其他處理器的請(qǐng)求,直到占用鎖的處理器完成操作。特點(diǎn)是只有一個(gè)搶到總線鎖的處理器運(yùn)行,但是這種方式效率低下,一旦某個(gè)處理器獲取到鎖其他處理器只能阻塞等待,會(huì)影響多核處理器的性能。

緩存一致性協(xié)議

圖示如下:

如何理解Java中volatile關(guān)鍵字

緩存一致性協(xié)議中最出名的就是MESI協(xié)議,MESI保證了每一個(gè)緩存中使用的共享變量的副本都是一致的。大致思想是,CPU操作緩存中的數(shù)據(jù)時(shí),如果發(fā)現(xiàn)該變量是一個(gè)共享變量,操作如下:

  • 讀?。翰蛔銎渌幚?,只是將緩存中數(shù)據(jù)讀取到寄存器中

  • 寫入:發(fā)出信號(hào)通知其他CPU將該變量的緩存行設(shè)置為無(wú)效狀態(tài)(Invalid),其他CPU進(jìn)行該變量的讀取時(shí)需要到主存中再次獲取

具體來(lái)說(shuō),MESI中規(guī)定了緩存行使用4種狀態(tài)標(biāo)記:

  • MModified,被修改

  • EExclusive,獨(dú)享的

  • SShared,共享的

  • IInvalid,無(wú)效的

有關(guān)MESI詳細(xì)的實(shí)現(xiàn)超出了本文的范圍,想要詳細(xì)了解可以參考此處或此處。

JMM

看完了CPU緩存再來(lái)看一下JMM,也就是Java內(nèi)存模型,指定了JVM如何與計(jì)算機(jī)的主存進(jìn)行工作,同時(shí)也決定了一個(gè)線程對(duì)共享變量的寫入何時(shí)對(duì)其他線程可見(jiàn),JMM定義了線程和主內(nèi)存之間的抽象關(guān)系,具體如下:

  • 共享變量存儲(chǔ)于主內(nèi)存中,每個(gè)線程都可以訪問(wèn)

  • 每個(gè)線程都有私有的工作內(nèi)存或者叫本地內(nèi)存

  • 工作內(nèi)存只存儲(chǔ)該線程對(duì)共享變量的副本

  • 線程不能直接操作主內(nèi)存,只有先操作了工作內(nèi)存之后才能寫入主內(nèi)存

  • 工作內(nèi)存和JMM內(nèi)存模型一樣也是一個(gè)抽象概念,其實(shí)并不存在,涵蓋了緩存、寄存器、編譯期優(yōu)化以及硬件等

簡(jiǎn)略圖如下:

如何理解Java中volatile關(guān)鍵字

MESI類似,如果一個(gè)線程修改了共享變量,刷新到主內(nèi)存后,其他線程讀取工作內(nèi)存的時(shí)候發(fā)現(xiàn)緩存失效,會(huì)從主內(nèi)存再次讀取到工作內(nèi)存中。

而下圖表示了JVM與計(jì)算機(jī)硬件分配的關(guān)系:

如何理解Java中volatile關(guān)鍵字

并發(fā)編程的三個(gè)特性

文章都看了大半了還沒(méi)到volatile?別急別急,先來(lái)看看并發(fā)編程中的三個(gè)重要特性,這對(duì)正確理解volatile有很大的幫助。

原子性

原子性就是在一次或多次操作中:

  • 要么所有的操作全部都得到了執(zhí)行,且不會(huì)受到任何因素的干擾而中斷

  • 要么所有的操作都不執(zhí)行

一個(gè)典型的例子就是兩個(gè)人轉(zhuǎn)賬,比如A向B轉(zhuǎn)賬1000元,那么這包含兩個(gè)基本的操作:

  • A的賬戶扣除1000元

  • B的賬戶增加1000元

這兩個(gè)操作,要么都成功,要么都失敗,也就是不能出現(xiàn)A賬戶扣除1000但是B賬戶金額不變的情況,也不能出現(xiàn)A賬戶金額不變B賬戶增加1000的情況。

需要注意的是兩個(gè)原子性操作結(jié)合在一起未必是原子性的,比如i++。本質(zhì)上來(lái)說(shuō),i++涉及到了三個(gè)操作:

  • get i

  • i+1

  • set i

這三個(gè)操作都是原子性的,但是組合在一起(i++)就不是原子性的。

可見(jiàn)性

另一個(gè)重要的特性是可見(jiàn)性,可見(jiàn)性是指,一個(gè)線程對(duì)共享變量進(jìn)行了修改,那么另外的線程可以立即看到修改后的最新值。

一個(gè)簡(jiǎn)單的例子如下:

public class Main {
    private int x = 0;
    private static final int MAX = 100000;
    public static void main(String[] args) throws InterruptedException {
        Main m = new Main();
        Thread thread0 = new Thread(()->{
            while(m.x < MAX) {
                ++m.x;
            }
        });

        Thread thread1 = new Thread(()->{
            while(m.x < MAX){
            }
            System.out.println("finish");
        });

        thread1.start();
        TimeUnit.MILLISECONDS.sleep(1);
        thread0.start();
    }
}

線程thread1會(huì)一直運(yùn)行,因?yàn)?code>thread1把x讀入工作內(nèi)存后,會(huì)一直判斷工作內(nèi)存中的值,由于thread0改變的是thread0工作內(nèi)存的值,并沒(méi)有對(duì)thread1可見(jiàn),因此永遠(yuǎn)也不會(huì)輸出finish,使用jstack也可以看到結(jié)果:

如何理解Java中volatile關(guān)鍵字

有序性

有序性是指代碼在執(zhí)行過(guò)程中的先后順序,由于JVM的優(yōu)化,導(dǎo)致了代碼的編寫順序未必是代碼的運(yùn)行順序,比如下面的四條語(yǔ)句:

int x = 10;
int y = 0;
x++;
y = 20;

有可能y=20x++前執(zhí)行,這就是指令重排序。一般來(lái)說(shuō),處理器為了提高程序的效率,可能會(huì)對(duì)輸入的代碼指令做一定的優(yōu)化,不會(huì)嚴(yán)格按照編寫順序去執(zhí)行代碼,但可以保證最終運(yùn)算結(jié)果是編碼時(shí)的期望結(jié)果,當(dāng)然,重排序也有一定的規(guī)則,需要嚴(yán)格遵守指令之間的數(shù)據(jù)依賴關(guān)系,并不是可以任意重排序,比如:

int x = 10;
int y = 0;
x++;
y = x+1;

y=x+1就不能先優(yōu)于x++執(zhí)行。

在單線程下重排序不會(huì)導(dǎo)致預(yù)期值的改變,但在多線程下,如果有序性得不到保證,那么將可能出現(xiàn)很大的問(wèn)題:

private boolean initialized = false;
private Context context;
public Context load(){
    if(!initialized){
        context = loadContext();
        initialized = true;
    }
    return context;
}

如果發(fā)生了重排序,initialized=true排序到了context=loadContext()的前面,假設(shè)兩個(gè)線程A、B同時(shí)訪問(wèn),且loadContext()需要一定耗時(shí),那么:

  • 線程A通過(guò)判斷后,先設(shè)置布爾變量的值為true,再進(jìn)行loadContext()操作

  • 線程B中由于布爾變量被設(shè)置為true,會(huì)直接返回一個(gè)未加載完成的context

volatile

好了終于到了volatile了,前面說(shuō)了這么多,目的就是為了能徹底理解和明白volatile。這部分分為四個(gè)小節(jié):

  • volatile的語(yǔ)義

  • 如何保證有序性以及可見(jiàn)性

  • 實(shí)現(xiàn)原理

  • 使用場(chǎng)景

  • synchronized區(qū)別

先來(lái)介紹一下volatile的語(yǔ)義。

語(yǔ)義

volatile修飾的實(shí)例變量或者類變量具有兩層語(yǔ)義:

  • 保證了不同線程之間對(duì)共享變量操作時(shí)的可見(jiàn)性

  • 禁止對(duì)指令進(jìn)行重排序操作

如何保證可見(jiàn)性以及有序性

先說(shuō)結(jié)論:

  • volatile能保證可見(jiàn)性

  • volatile能保證有序性

  • volatile不能保證原子性

下面分別進(jìn)行介紹。

可見(jiàn)性

Java中保證可見(jiàn)性有如下方式:

  • volatile:當(dāng)一個(gè)變量被volatile修飾時(shí),對(duì)共享資源的讀操作會(huì)直接在主內(nèi)存中進(jìn)行(準(zhǔn)確來(lái)說(shuō)也會(huì)讀取到工作內(nèi)存中,但是如果其他線程進(jìn)行了修改就必須從主內(nèi)存重新讀?。?,寫操作是先修改工作內(nèi)存,但是修改結(jié)束后立即刷新到主內(nèi)存中

  • synchronizedsynchronized一樣能保證可見(jiàn)性,能夠保證同一時(shí)刻只有一個(gè)線程獲取到鎖,然后執(zhí)行同步方法,并且確保鎖釋放之前,變量的修改被刷新到主內(nèi)存中

  • 使用顯式鎖LockLocklock方法能保證同一時(shí)刻只有一個(gè)線程能夠獲取到鎖然后執(zhí)行同步方法,并且確保鎖釋放之前能夠?qū)?duì)變量的修改刷新到主內(nèi)存中

具體來(lái)說(shuō),可以看一下之前的例子:

public class Main {
    private int x = 0;
    private static final int MAX = 100000;
    public static void main(String[] args) throws InterruptedException {
        Main m = new Main();
        Thread thread0 = new Thread(()->{
            while(m.x < MAX) {
                ++m.x;
            }
        });

        Thread thread1 = new Thread(()->{
            while(m.x < MAX){
            }
            System.out.println("finish");
        });

        thread1.start();
        TimeUnit.MILLISECONDS.sleep(1);
        thread0.start();
    }
}

上面說(shuō)過(guò)這段代碼會(huì)不斷運(yùn)行,一直沒(méi)有輸出,就是因?yàn)樾薷暮蟮?code>x對(duì)線程thread1不可見(jiàn),如果在x的定義中加上了volatile,就不會(huì)出現(xiàn)沒(méi)有輸出的情況了,因?yàn)榇藭r(shí)對(duì)x的修改是線程thread1可見(jiàn)的。有序性

JMM中允許編譯期和處理器對(duì)指令進(jìn)行重排序,在多線程的情況下有可能會(huì)出現(xiàn)問(wèn)題,為此,Java同樣提供了三種機(jī)制去保證有序性:

  • volatile

  • synchronized

  • 顯式鎖Lock

另外,關(guān)于有序性不得不提的就是Happens-before原則。Happends-before原則說(shuō)的就是如果兩個(gè)操作的執(zhí)行次序無(wú)法從該原則推導(dǎo)出來(lái),那么就無(wú)法保證有序性,JVM或處理器可以任意重排序。這么做的目的是為了盡可能提高程序的并行度,具體規(guī)則如下:

  • 程序次序規(guī)則:在一個(gè)線程內(nèi),代碼按照編寫時(shí)的次序執(zhí)行,編寫在后面的操作發(fā)生與編寫在前面的操作之后

  • 鎖定規(guī)則:如果一個(gè)鎖處于鎖定狀態(tài),則unlock操作要先行發(fā)生于對(duì)同一個(gè)鎖的lock操作

  • volatile變量規(guī)則:對(duì)一個(gè)變量的寫操作要早于對(duì)這個(gè)變量之后的讀操作

  • 傳遞規(guī)則:如果操作A先于操作B,操作B先于操作C,那么操作A先于操作C

  • 線程啟動(dòng)規(guī)則:Thread對(duì)象的start()方法先行發(fā)生于對(duì)該線程的任何動(dòng)作

  • 線程中斷規(guī)則:對(duì)線程執(zhí)行interrupt()方法肯定要優(yōu)于捕獲到中斷信號(hào),換句話說(shuō),如果收到了中斷信號(hào),那么在此之前必定調(diào)用了interrupt()

  • 線程終結(jié)規(guī)則:線程中所有操作都要先行發(fā)生于線程的終止檢測(cè),也就是邏輯單元的執(zhí)行肯定要發(fā)生于線程終止之前

  • 對(duì)象終結(jié)規(guī)則:一個(gè)對(duì)象初始化的完成先行發(fā)生于finalize()之前

對(duì)于volatile,會(huì)直接禁止對(duì)指令重排,但是對(duì)于volatile前后無(wú)依賴關(guān)系的指令可以隨意重排,比如:

int x = 0;
int y = 1;
//private volatile int z;
z = 20;
x++;
y--;

z=20之前,先定義x或先定義y并沒(méi)有要求,只需要在執(zhí)行z=20的時(shí)候,可以保證x=0,y=1即可,同理,x++y--具體先執(zhí)行哪一個(gè)并沒(méi)有要求,只需要保證兩者執(zhí)行在z=20之后即可。

原子性

Java中,所有對(duì)基本數(shù)據(jù)類型變量的讀取賦值操作都是原子性的,對(duì)引用類型的變量讀取和賦值也是原子性的,但是:

  • 將一個(gè)變量賦值給另一個(gè)變量的操作不是原子性的,因?yàn)樯婕暗搅艘粋€(gè)變量的讀取以及一個(gè)變量的寫入,兩個(gè)原子性操作結(jié)合在一起就不是原子性操作

  • 多個(gè)原子性操作在一起就不是原子性操作,比如i++

  • JMM只保證基本讀取和賦值的原子性操作,其他的均不保證,如果需要具備原子性,那么可以使用synchronizedLock,或者JUC包下的原子操作類

也就是說(shuō),volatile并不能保證原子性,例子如下:

public class Main {
    private volatile int x = 0;
    private static final CountDownLatch latch = new CountDownLatch(10);

    public void inc() {
        ++x;
    }

    public static void main(String[] args) throws InterruptedException {
        Main m = new Main();
        IntStream.range(0, 10).forEach(i -> {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    m.inc();
                }
                latch.countDown();
            }).start();
        });
        latch.await();
        System.out.println(m.x);
    }
}

最后輸出的x的值會(huì)少于10000,而且每次運(yùn)行的結(jié)果也并不相同,至于原因,可以從兩個(gè)線程A、B開(kāi)始分析,圖示如下:

如何理解Java中volatile關(guān)鍵字

  • 0-t1:線程A將x讀入工作內(nèi)存,此時(shí)x=0

  • t1-t2:線程A時(shí)間片完,CPU調(diào)度線程B,線程B將x讀入工作內(nèi)存,此時(shí)x=0

  • t2-t3:線程B對(duì)工作內(nèi)存中的x進(jìn)行自增操作,并更新到工作內(nèi)存中

  • t3-t4:線程B時(shí)間片完,CPU調(diào)度線程A,同理線程A對(duì)工作內(nèi)存中的x自增

  • t4-t5:線程A將工作內(nèi)存中的值寫回主內(nèi)存,此時(shí)主內(nèi)存中的值為x=1

  • t5以后:線程A時(shí)間片完,CPU調(diào)度線程B,線程B也將自己的工作內(nèi)存寫回主內(nèi)存,再次將主內(nèi)存中的x賦值為1

也就是說(shuō),多線程操作的話,會(huì)出現(xiàn)兩次自增但是實(shí)際上只進(jìn)行一次數(shù)值修改的操作。想要x的值變?yōu)?code>10000也很簡(jiǎn)單,加上synchronized即可:

new Thread(() -> {
    synchronized (m) {
        for (int j = 0; j < 1000; j++) {
            m.inc();
        }
    }
    latch.countDown();
}).start();

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

前面已經(jīng)知道,volatile可以保證有序性以及可見(jiàn)性,那么,具體是如何操作的呢?

答案就是一個(gè)lock;前綴,該前綴實(shí)際上相當(dāng)于一個(gè)內(nèi)存屏障,該內(nèi)存屏障會(huì)為指令的執(zhí)行提供如下幾個(gè)保障:

  • 確保指令重排序時(shí)不會(huì)將其后面的代碼排到內(nèi)存屏障之前

  • 確保指令重排序時(shí)不會(huì)將其前面的代碼排到內(nèi)存屏障之后

  • 確保執(zhí)行到內(nèi)存屏障修飾的指令時(shí)前面的代碼全部執(zhí)行完成

  • 強(qiáng)制將線程工作內(nèi)存中的值修改刷新到主存中

  • 如果是寫操作,會(huì)導(dǎo)致其他線程工作內(nèi)存中的緩存數(shù)據(jù)失效

使用場(chǎng)景

一個(gè)典型的使用場(chǎng)景是利用開(kāi)關(guān)進(jìn)行線程的關(guān)閉操作,例子如下:

public class ThreadTest extends Thread{
    private volatile boolean started = true;

    @Override
    public void run() {
        while (started){
            
        }
    }

    public void shutdown(){
        this.started = false;
    }
}

如果布爾變量沒(méi)有被volatile修飾,那么很可能新的布爾值刷新不到主內(nèi)存中,導(dǎo)致線程不會(huì)結(jié)束。

synchronized的區(qū)別

  • 使用上的區(qū)別:volatile只能用于修飾實(shí)例變量或者類變量,但是不能用于修飾方法、方法參數(shù)、局部變量等,另外可以修飾的變量為null。但synchronized不能用于對(duì)變量的修飾,只能修飾方法或語(yǔ)句塊,而且monitor對(duì)象不能為null

  • 對(duì)原子性的保證:volatile無(wú)法保證原子性,但是synchronized可以保證

  • 對(duì)可見(jiàn)性的保證:volatilesynchronized都能保證可見(jiàn)性,但是synchronized是借助于JVM指令monitor enter/monitor exit保證的,在monitor exit的時(shí)候所有共享資源都被刷新到主內(nèi)存中,而volatile是通過(guò)lock;機(jī)器指令實(shí)現(xiàn)的,迫使其他線程工作內(nèi)存失效,需要到主內(nèi)存加載

  • 對(duì)有序性的保證:volatile能夠禁止JVM以及處理器對(duì)其進(jìn)行重排序,而synchronized保證的有序性是通過(guò)程序串行化執(zhí)行換來(lái)的,并且在synchronized代碼塊中的代碼也會(huì)發(fā)生指令重排的情況

  • 其他區(qū)別:volatile不會(huì)使線程陷入阻塞,但synchronized會(huì)

到此,關(guān)于“如何理解Java中volatile關(guān)鍵字”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實(shí)踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識(shí),請(qǐng)繼續(xù)關(guān)注創(chuàng)新互聯(lián)網(wǎng)站,小編會(huì)繼續(xù)努力為大家?guī)?lái)更多實(shí)用的文章!

文章名稱:如何理解Java中volatile關(guān)鍵字
文章路徑:http://muchs.cn/article8/ihgsip.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供虛擬主機(jī)、網(wǎng)站收錄標(biāo)簽優(yōu)化、網(wǎng)站內(nèi)鏈網(wǎng)站設(shè)計(jì)、網(wǎng)站導(dǎo)航

廣告

聲明:本網(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í)需注明來(lái)源: 創(chuàng)新互聯(lián)

微信小程序開(kāi)發(fā)