Java內(nèi)存模型之可見(jiàn)性(填坑之路)

前幾天路過(guò)一個(gè)經(jīng)常負(fù)責(zé)面試的同事附近届搁,看到幾個(gè)人在討論volatile的可見(jiàn)性問(wèn)題氢橙,當(dāng)時(shí)第一感覺(jué)是 :“可見(jiàn)性還不簡(jiǎn)單嗎绅这?volatile修飾一個(gè)變量時(shí)纺蛆,那么在一個(gè)線程都對(duì)這個(gè)變量的更改,其他線程都立即可見(jiàn)污筷」す耄”

后面聽(tīng)到這樣一句話:“實(shí)際運(yùn)行結(jié)果能刷新你的三觀,網(wǎng)上的例子很多都是有問(wèn)題的”瓣蛀,讓我瞬間產(chǎn)生了興趣陆蟆。湊近一看,果然跟我的很多認(rèn)知都產(chǎn)生了偏差惋增。

為了解決其中的疑惑叠殷,查閱的不少文章,撥開(kāi)了一些迷霧诈皿,現(xiàn)將結(jié)果整理出來(lái)林束,與大家一同探討像棘。

基礎(chǔ)Java環(huán)境:
java version "1.8.0_172" Java(TM) SE Runtime Environment (build 1.8.0_172-b11) Java HotSpot(TM) 64-Bit Server VM (build 25.172-b11, mixed mode)

基本概念

Java內(nèi)存模型

首先先復(fù)習(xí)一下內(nèi)存模型的概念:

Java內(nèi)存模型(即Java Memory Model,簡(jiǎn)稱JMM)本身是一種抽象的概念壶冒,并不真實(shí)存在缕题,它描述的是一組規(guī)則或規(guī)范,通過(guò)這組規(guī)范 定義了程序中各個(gè)變量(包括實(shí)例字段依痊,靜態(tài)字段和構(gòu)成數(shù)組對(duì)象的元素)的訪問(wèn)方式。

JVM程序運(yùn)行的實(shí)體是線程怎披,而每個(gè)線程創(chuàng)建時(shí)JVM都會(huì)為其創(chuàng)建一個(gè)工作內(nèi)存(有些地方稱為椥剜遥空間),用于存儲(chǔ)線程私有的數(shù)據(jù)凉逛,而Java內(nèi)存模型中規(guī)定所有變量都存儲(chǔ)在主內(nèi)存性宏,主內(nèi)存是共享內(nèi)存區(qū)域,所有線程都可以訪問(wèn)状飞,但線程對(duì)變量的操作(讀取賦值等)必須在工作內(nèi)存中進(jìn)行毫胜,首先要將變量從主內(nèi)存拷貝的自己的工作內(nèi)存空間,然后對(duì)變量進(jìn)行操作诬辈,操作完成后再將變量寫(xiě)回主內(nèi)存酵使,不能直接操作主內(nèi)存中的變量,工作內(nèi)存中存儲(chǔ)著主內(nèi)存中的變量副本拷貝焙糟,前面說(shuō)過(guò)口渔,工作內(nèi)存是每個(gè)線程的私有數(shù)據(jù)區(qū)域,因此不同的線程間無(wú)法訪問(wèn)對(duì)方的工作內(nèi)存穿撮,線程間的通信(傳值)必須通過(guò)主內(nèi)存來(lái)完成缺脉,其簡(jiǎn)要訪問(wèn)過(guò)程如下圖:

image.png

volatile關(guān)鍵字

volatile是老生常談的一個(gè)關(guān)鍵字,大家在編程中其實(shí)用得都很少悦穿,面試中比較常見(jiàn)攻礼,也正是這個(gè)原因,讓大家對(duì)這一塊的理解與實(shí)際結(jié)果產(chǎn)生了偏差栗柒。

volatile是Java虛擬機(jī)提供的輕量級(jí)的同步機(jī)制礁扮。volatile關(guān)鍵字有如下兩個(gè)作用。
1)保證被volatile修飾的共享變量對(duì)所有線程 總是可見(jiàn)的瞬沦,也就是當(dāng)一個(gè)線程修改了一個(gè)被volatile修飾共享變量的值深员,新值總是可以被其他線程立即得知
2)禁止指令重排序優(yōu)化蛙埂。

可見(jiàn)性

關(guān)于內(nèi)存模型和volatile的概念本篇不做詳細(xì)贅述倦畅,不熟悉的看官建議先百度一下。JMM是圍繞 原子性绣的、有序性叠赐、可見(jiàn)性 展開(kāi)的欲账,本文主要圍繞內(nèi)存模型的可見(jiàn)性出發(fā),通過(guò)實(shí)際例子來(lái)探究其運(yùn)行原理芭概。

先思考一個(gè)問(wèn)題:volatile保證的“立即可見(jiàn)”的反義是什么赛不?

這是大家最容易想到的答案,應(yīng)該是“不可見(jiàn)”罢洲,且有實(shí)實(shí)在在的例子讓我們覺(jué)得“不可見(jiàn)”深根不移踢故。

示例1:

package com.youzan;

/**
 * Date: 2018/8/12
 * @author xuzhiyi
 */
public class Test1 {

    private static boolean flag = true;

    private static int i = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            while (flag) {
                i++;
            }
            System.out.printf("**********test1 跳出成功, i=%d **********\n", i);
        });
        thread.start();
        Thread.sleep(100);
        flag = false;
        System.out.printf("**********test1 main thread 結(jié)束, i=%d **********\n", i);
    }
}

示例2:

package com.youzan;

/**
 * Date: 2018/8/12
 * @author xuzhiyi
 */
public class Test2 {

    private static volatile boolean flag = true;

    private static int i = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            while (flag) {
                i++;
            }
            System.out.printf("**********test2 跳出成功, i=%d **********\n", i);
        });
        thread.start();
        Thread.sleep(100);
        flag = false;
        System.out.printf("**********test2 main thread 結(jié)束, i=%d **********\n", i);
    }
}

示例1和示例2的唯一區(qū)別在于,示例2的flag有volatile修飾惹苗。上述示例的運(yùn)行結(jié)果大家都“知道”殿较,示例1會(huì)一直死循環(huán),示例2會(huì)立即跳出循環(huán)桩蓉。大家可能都運(yùn)行過(guò)這兩段(或者相似的)代碼淋纲,大部分人對(duì)結(jié)果很滿意,因?yàn)榉项A(yù)期院究,沒(méi)有加volatile關(guān)鍵字的成員變量多線程之間不可見(jiàn)洽瞬。

回到剛剛那個(gè)問(wèn)題,“立即可見(jiàn)”的反義是什么业汰?

通過(guò)上述實(shí)踐我們可以“肯定”的回答:“立即可見(jiàn)”的反義是“不可見(jiàn)”;锴浴!样漆!而且是“一直不可見(jiàn)”

說(shuō)到這里对供,可能有部分人有疑問(wèn)了,“立即可見(jiàn)”的反義應(yīng)該是“不立即可見(jiàn)”氛濒,說(shuō)人話就是“可能過(guò)一段時(shí)間后可見(jiàn)产场,不一定是馬上可見(jiàn)”∥韪停可是即使我們運(yùn)行一萬(wàn)遍示例1的代碼京景,都是一直不可見(jiàn)。怎么辦骗奖?繼續(xù)往下看确徙。

實(shí)戰(zhàn)

讓沒(méi)有volatile也能跳出循環(huán)

方式一

示例3:

package com.youzan;

/**
 * Date: 2018/8/12
 * @author xuzhiyi
 */
public class Test3 {

    private static boolean flag = true;

    private static int i = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            while (flag) {
                i++;
            }
            System.out.printf("**********test3 跳出成功, i=%d **********\n", i);
        });
        thread.start();
        Thread.sleep(1);
        flag = false;
        System.out.printf("**********test3 main thread 結(jié)束, i=%d **********\n", i);
    }
}

在示例3中,我僅將示例1中的sleep時(shí)間改為1毫秒执桌,while循環(huán)即可成功跳出鄙皇,輸出結(jié)果如下:

**********test3 main thread 結(jié)束, i=60167 **********
**********test3 跳出成功, i=60167 **********

ps:主線程可能由于停頓時(shí)間太短,導(dǎo)致while循環(huán)根本沒(méi)進(jìn)去仰挣。重試幾次伴逸,當(dāng)i的值不為0即代表已經(jīng)進(jìn)入循環(huán)。

對(duì)比示例1和示例3我們可以得出一個(gè)結(jié)論:

  • 當(dāng)主線程停頓時(shí)間很極短(1~2ms)時(shí)膘壶,可以跳出循環(huán)错蝴;
  • 當(dāng)主線程停頓時(shí)間較長(zhǎng)時(shí)洲愤,無(wú)法跳出循環(huán);

結(jié)論變種1:

  • 當(dāng)子線程循環(huán)執(zhí)行時(shí)間極短(1~2ms)時(shí)顷锰,可以跳出循環(huán)柬赐;
  • 當(dāng)子線程循環(huán)執(zhí)行時(shí)間較長(zhǎng)時(shí),無(wú)法跳出循環(huán)官紫;

結(jié)論變種2:

  • 當(dāng)子線程循環(huán)次數(shù)較少時(shí)肛宋,可以跳出循環(huán);
  • 當(dāng)子線程循環(huán)次數(shù)較多時(shí)束世,無(wú)法跳出循環(huán)酝陈;

看上去是不是有點(diǎn)意思?代碼的執(zhí)行結(jié)果居然跟執(zhí)行時(shí)間良狈、循環(huán)次數(shù)有關(guān)后添?推斷到這里笨枯,有些看官可能已經(jīng)想到了JIT即使編譯優(yōu)化薪丁。沒(méi)錯(cuò),正是JIT的優(yōu)化對(duì)運(yùn)行結(jié)果產(chǎn)生了影響馅精。

關(guān)于JIT
當(dāng)虛擬機(jī)發(fā)現(xiàn)某個(gè)方法或代碼塊運(yùn)行特別頻繁時(shí)严嗜,就會(huì)把這些代碼認(rèn)定為“Hot Spot Code”(熱點(diǎn)代碼),為了提高熱點(diǎn)代碼的執(zhí)行效率洲敢,在運(yùn)行時(shí)漫玄,虛擬機(jī)將會(huì)把這些代碼編譯成與本地平臺(tái)相關(guān)的機(jī)器碼,并進(jìn)行各層次的優(yōu)化压彭,完成這項(xiàng)任務(wù)的正是 JIT 編譯器睦优。
運(yùn)行過(guò)程中會(huì)被即時(shí)編譯器編譯的“熱點(diǎn)代碼”有兩類:
1)被多次調(diào)用的方法。
2)被多次調(diào)用的循環(huán)體壮不。

如何驗(yàn)證上述結(jié)論呢汗盘?

  • -Xint :強(qiáng)制使用解釋執(zhí)行的方式啟動(dòng)java虛擬機(jī),此模式下询一,不會(huì)使用JIT優(yōu)化隐孽,示例1和示例3的代碼都會(huì)跳出循環(huán)。
  • -Xcomp:強(qiáng)制使用編譯執(zhí)行的方式啟動(dòng)java虛擬機(jī)健蕊,此模式下菱阵,代碼會(huì)被優(yōu)化并編譯成機(jī)器碼,示例1和示例3都無(wú)法填出循環(huán)缩功。

總結(jié)一下:mac下默認(rèn)為-Xmixed混合模式晴及,使用java -version可以查看,混合模式下只有熱點(diǎn)代碼達(dá)到一定閾值才會(huì)發(fā)生JIT優(yōu)化嫡锌,因此導(dǎo)致了上述看到的運(yùn)行時(shí)間長(zhǎng)短對(duì)運(yùn)行結(jié)果的影響抗俄。

方式二

不少熱心的網(wǎng)友在自己運(yùn)行示例1代碼的時(shí)候脆丁,會(huì)不由自主的加上一行print,如下:
示例4:

package com.youzan;

/**
 * Date: 2018/8/12
 * @author xuzhiyi
 */
public class Test4 {

    private static boolean flag = true;

    private static int i = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            while (flag) {
                i++;
                System.out.println("i=" + i);
            }
            System.out.printf("**********test4 跳出成功, i=%d **********\n", i);
        });
        thread.start();
        Thread.sleep(100);
        flag = false;
        System.out.printf("**********test4 main thread 結(jié)束, i=%d **********\n", i);
    }
}

上述代碼一運(yùn)行后成功跳出动雹,可能又驚倒了一批看官槽卫,為什么多一行print結(jié)果又不一樣了。而且就算在-Xcomp模式優(yōu)化后也可以跳出胰蝠。有點(diǎn)神奇吧歼培?
為了找出原因,我對(duì)print代碼進(jìn)行了幾次不同的替換:

示例5:

package com.youzan;

import java.util.HashMap;

/**
 * Date: 2018/8/12
 * @author xuzhiyi
 */
public class Test5 {

    private static boolean flag = true;

    private static int i = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            while (flag) {
                doSomeThing1();
            }
            System.out.printf("**********test4 跳出成功, i=%d **********\n", i);
        });
        thread.start();
        Thread.sleep(10);
        flag = false;
        System.out.printf("**********test4 main thread 結(jié)束, i=%d **********\n", i);
    }

    private static void doSomeThing1() {
        System.out.println("doSomeThing1");
    }

    private static void doSomeThing2() {
        synchronized (Test5.class) {
            i++;
        }
    }

    private static void doSomeThing3() {
        i++;
        Thread.yield();
    }

    private static void doSomeThing4() {
        new HashMap<>();
    }
}

上述代碼中茸塞,不論是在循環(huán)體內(nèi)執(zhí)行哪一個(gè)方法(doSomeThing1~ doSomeThing4)躲庄,都可以正常跳出循環(huán)。為什么呢钾虐?究竟是什么影響了線程對(duì)成員變量的可見(jiàn)性呢噪窘?我的結(jié)論如下:
根據(jù)java的內(nèi)存模型規(guī)范,一個(gè)線程對(duì)普通變量的修改并不需要立即寫(xiě)回到主存效扫,且另一個(gè)線程讀取也不需要每一次都從主存中去讀取倔监。至于什么時(shí)候與主內(nèi)存同步,虛擬機(jī)只需保證方法出棧時(shí)將修改的值同步到主內(nèi)存菌仁。因此這其中有比較寬松的優(yōu)化空間浩习。而上述幾個(gè)方法,都存在一定的同步空間济丘。虛擬機(jī)會(huì)在此時(shí)與主內(nèi)存同步谱秽。
ps:以上結(jié)論純屬猜測(cè),沒(méi)有很好的論據(jù)摹迷,歡迎大家探討疟赊!

volatile的傳播范圍

思考兩個(gè)問(wèn)題:

  1. 把volatile對(duì)象傳遞給另一個(gè)對(duì)象,新對(duì)象是否立即可見(jiàn)呢峡碉?
  2. 當(dāng)volatile修飾對(duì)象時(shí)近哟,如果對(duì)象的嵌套的層級(jí)較深,那該對(duì)象的內(nèi)部是否立即可見(jiàn)呢异赫?

示例6:

package com.youzan;

/**
 * Date: 2018/8/12
 * @author xuzhiyi
 */
public class Test6 {

    private static volatile ReferenceFlag referenceFlag = new ReferenceFlag();

    private static int i = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            BaseFlag baseFlag = referenceFlag.baseFlag;
            while (baseFlag.flag) {
                i++;
            }
            System.out.printf("**********test6 跳出成功, i=%d **********\n", i);
        });
        thread.start();
        Thread.sleep(100);
        referenceFlag.baseFlag.flag = false;
        System.out.printf("**********test6 main thread 結(jié)束, i=%d **********\n", i);
    }

    static class BaseFlag {
        boolean flag = true;
    }

    static class ReferenceFlag {
        volatile BaseFlag baseFlag = new BaseFlag();
    }
}

在示例6中椅挣,使用了引用嵌套的方式來(lái)驗(yàn)證volatile是否可以傳遞給一個(gè)局部變量,示例中的引用都是用來(lái)volatile關(guān)鍵字來(lái)修飾塔拳,運(yùn)行結(jié)果是無(wú)法跳出鼠证。

結(jié)論一:當(dāng)使用一個(gè)變量來(lái)接受一個(gè)volatile修飾的變量時(shí),volatile的可見(jiàn)性并不會(huì)傳遞靠抑。即新的變量不再具有volatile特性量九。

示例2:

package com.youzan;

/**
 * Date: 2018/8/12
 * @author xuzhiyi
 */
public class Test7 {

    private static int i = 0;

    private static volatile DeapReferenceInnerFlag deapReferenceInnerFlag = new DeapReferenceInnerFlag();

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            while (deapReferenceInnerFlag.referenceInnerFlag.baseFlag.flag) {
                i++;
            }
            System.out.printf("**********test7 跳出成功, i=%d **********\n", i);
        });
        thread.start();
        Thread.sleep(100);
        deapReferenceInnerFlag.referenceInnerFlag.baseFlag.flag = false;
        System.out.printf("**********test7 main thread 結(jié)束, i=%d **********\n", i);
    }

    static class BaseFlag {
        boolean flag = true;
    }

    static class ReferenceInnerFlag {
        BaseFlag baseFlag = new BaseFlag();
    }

    static class DeapReferenceInnerFlag {
        ReferenceInnerFlag referenceInnerFlag = new ReferenceInnerFlag();
    }
}

示例7是一個(gè)多層嵌套的對(duì)象,只有最外層使用volatile修飾,當(dāng)其內(nèi)部的值改變后荠列,使用鏈?zhǔn)秸{(diào)用的方式类浪,則一直可以取到最新的值。

結(jié)論二:對(duì)于多層嵌套的對(duì)象肌似,最外層使用volatile修飾费就,使用鏈?zhǔn)秸{(diào)用的方式,volatile的可見(jiàn)性可以傳播川队。
ps:結(jié)論二沒(méi)有很好的理論依據(jù)力细,僅從實(shí)踐上看是如此。

總結(jié)

本篇結(jié)合實(shí)際的幾個(gè)例子固额,講述了幾個(gè)認(rèn)識(shí)誤區(qū)眠蚂。僅通過(guò)運(yùn)行結(jié)果說(shuō)明了一些問(wèn)題,但依然不夠深入斗躏,不足之處逝慧,還望指出。想深入探究的看官啄糙,可以參考下面的幾篇文章笛臣。

參考文章

  1. 全面理解Java內(nèi)存模型(JMM)及volatile關(guān)鍵字
  2. 并行編程之多線程共享非volatile變量,會(huì)不會(huì)可能導(dǎo)致線程while死循環(huán)
  3. 深入淺出 JIT 編譯器
  4. 一個(gè)由JIT優(yōu)化引發(fā)的問(wèn)題
  5. JVM執(zhí)行篇:使用HSDIS插件分析JVM代碼執(zhí)行細(xì)節(jié)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末迈套,一起剝皮案震驚了整個(gè)濱河市捐祠,隨后出現(xiàn)的幾起案子碱鳞,更是在濱河造成了極大的恐慌桑李,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件窿给,死亡現(xiàn)場(chǎng)離奇詭異贵白,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)崩泡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門禁荒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人角撞,你說(shuō)我怎么就攤上這事呛伴。” “怎么了谒所?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵热康,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我劣领,道長(zhǎng)姐军,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮奕锌,結(jié)果婚禮上著觉,老公的妹妹穿的比我還像新娘。我一直安慰自己惊暴,他們只是感情好饼丘,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著辽话,像睡著了一般葬毫。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上屡穗,一...
    開(kāi)封第一講書(shū)人閱讀 49,144評(píng)論 1 285
  • 那天贴捡,我揣著相機(jī)與錄音,去河邊找鬼村砂。 笑死烂斋,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的础废。 我是一名探鬼主播汛骂,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼评腺!你這毒婦竟也來(lái)了帘瞭?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤蒿讥,失蹤者是張志新(化名)和其女友劉穎蝶念,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體芋绸,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡媒殉,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了摔敛。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片廷蓉。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖马昙,靈堂內(nèi)的尸體忽然破棺而出桃犬,到底是詐尸還是另有隱情,我是刑警寧澤行楞,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布攒暇,位于F島的核電站,受9級(jí)特大地震影響敢伸,放射性物質(zhì)發(fā)生泄漏扯饶。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望尾序。 院中可真熱鬧钓丰,春花似錦、人聲如沸每币。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)兰怠。三九已至梦鉴,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間揭保,已是汗流浹背肥橙。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留秸侣,地道東北人存筏。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像味榛,于是被迫代替她去往敵國(guó)和親椭坚。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容