精講Java內(nèi)存模型

版權(quán)聲明:轉(zhuǎn)載于 http://blog.csdn.net/ccj659/article/details/53019995

Java內(nèi)存模型規(guī)范了Java虛擬機(jī)與計(jì)算機(jī)內(nèi)存是如何協(xié)同工作的耘擂。Java虛擬機(jī)是一個(gè)完整的計(jì)算機(jī)的一個(gè)模型胆剧,因此這個(gè)模型自然也包含一個(gè)內(nèi)存模型——又稱(chēng)為Java內(nèi)存模型。

如果你想設(shè)計(jì)表現(xiàn)良好的并發(fā)程序,理解Java內(nèi)存模型是非常重要的秩霍。Java內(nèi)存模型規(guī)定了如何和何時(shí)可以看到由其他線(xiàn)程修改過(guò)后的共享變量的值篙悯,以及在必須時(shí)如何同步的訪(fǎng)問(wèn)共享變量。

原始的Java內(nèi)存模型存在一些不足铃绒,因此Java內(nèi)存模型在Java1.5時(shí)被重新修訂鸽照。這個(gè)版本的Java內(nèi)存模型在Java8中人在使用。

Java內(nèi)存模型內(nèi)部原理

Java內(nèi)存模型把Java虛擬機(jī)內(nèi)部劃分為線(xiàn)程棧和堆颠悬。這張圖演示了Java內(nèi)存模型的邏輯視圖矮燎。



每一個(gè)運(yùn)行在Java虛擬機(jī)里的線(xiàn)程都擁有自己的線(xiàn)程棧。這個(gè)線(xiàn)程棧包含了這個(gè)線(xiàn)程調(diào)用的方法當(dāng)前執(zhí)行點(diǎn)相關(guān)的信息赔癌。一個(gè)線(xiàn)程僅能訪(fǎng)問(wèn)自己的線(xiàn)程棧诞外。一個(gè)線(xiàn)程創(chuàng)建的本地變量對(duì)其它線(xiàn)程不可見(jiàn),僅自己可見(jiàn)灾票。即使兩個(gè)線(xiàn)程執(zhí)行同樣的代碼峡谊,這兩個(gè)線(xiàn)程任然在在自己的線(xiàn)程棧中的代碼來(lái)創(chuàng)建本地變量。因此铝条,每個(gè)線(xiàn)程擁有每個(gè)本地變量的獨(dú)有版本靖苇。
所有原始類(lèi)型的本地變量都存放在線(xiàn)程棧上席噩,因此對(duì)其它線(xiàn)程不可見(jiàn)班缰。一個(gè)線(xiàn)程可能向另一個(gè)線(xiàn)程傳遞一個(gè)原始類(lèi)型變量的拷貝,但是它不能共享這個(gè)原始類(lèi)型變量自身悼枢。
堆上包含在Java程序中創(chuàng)建的所有對(duì)象埠忘,無(wú)論是哪一個(gè)對(duì)象創(chuàng)建的。這包括原始類(lèi)型的對(duì)象版本馒索。如果一個(gè)對(duì)象被創(chuàng)建然后賦值給一個(gè)局部變量莹妒,或者用來(lái)作為另一個(gè)對(duì)象的成員變量,這個(gè)對(duì)象任然是存放在堆上绰上。
下面這張圖演示了調(diào)用棧和本地變量存放在線(xiàn)程棧上旨怠,對(duì)象存放在堆上。



一個(gè)本地變量可能是原始類(lèi)型蜈块,在這種情況下鉴腻,它總是“呆在”線(xiàn)程棧上。
一個(gè)本地變量也可能是指向一個(gè)對(duì)象的一個(gè)引用百揭。在這種情況下爽哎,引用(這個(gè)本地變量)存放在線(xiàn)程棧上,但是對(duì)象本身存放在堆上器一。

一個(gè)對(duì)象可能包含方法课锌,這些方法可能包含本地變量。這些本地變量任然存放在線(xiàn)程棧上祈秕,即使這些方法所屬的對(duì)象存放在堆上渺贤。
一個(gè)對(duì)象的成員變量可能隨著這個(gè)對(duì)象自身存放在堆上雏胃。不管這個(gè)成員變量是原始類(lèi)型還是引用類(lèi)型。
靜態(tài)成員變量跟隨著類(lèi)定義一起也存放在堆上志鞍。
存放在堆上的對(duì)象可以被所有持有對(duì)這個(gè)對(duì)象引用的線(xiàn)程訪(fǎng)問(wèn)丑掺。當(dāng)一個(gè)線(xiàn)程可以訪(fǎng)問(wèn)一個(gè)對(duì)象時(shí),它也可以訪(fǎng)問(wèn)這個(gè)對(duì)象的成員變量述雾。如果兩個(gè)線(xiàn)程同時(shí)調(diào)用同一個(gè)對(duì)象上的同一個(gè)方法街州,它們將會(huì)都訪(fǎng)問(wèn)這個(gè)對(duì)象的成員變量,但是每一個(gè)線(xiàn)程都擁有這個(gè)本地變量的私有拷貝玻孟。
下圖演示了上面提到的點(diǎn):



兩個(gè)線(xiàn)程擁有一些列的本地變量唆缴。其中一個(gè)本地變量(Local Variable 2)執(zhí)行堆上的一個(gè)共享對(duì)象(Object 3)。這兩個(gè)線(xiàn)程分別擁有同一個(gè)對(duì)象的不同引用黍翎。這些引用都是本地變量面徽,因此存放在各自線(xiàn)程的線(xiàn)程棧上。這兩個(gè)不同的引用指向堆上同一個(gè)對(duì)象匣掸。
注意趟紊,這個(gè)共享對(duì)象(Object 3)持有Object2和Object4一個(gè)引用作為其成員變量(如圖中Object3指向Object2和Object4的箭頭)。通過(guò)在Object3中這些成員變量引用碰酝,這兩個(gè)線(xiàn)程就可以訪(fǎng)問(wèn)Object2和Object4霎匈。
這張圖也展示了指向堆上兩個(gè)不同對(duì)象的一個(gè)本地變量。在這種情況下送爸,指向兩個(gè)不同對(duì)象的引用不是同一個(gè)對(duì)象铛嘱。理論上,兩個(gè)線(xiàn)程都可以訪(fǎng)問(wèn)Object1和Object5袭厂,如果兩個(gè)線(xiàn)程都擁有兩個(gè)對(duì)象的引用墨吓。但是在上圖中,每一個(gè)線(xiàn)程僅有一個(gè)引用指向兩個(gè)對(duì)象其中之一纹磺。

因此帖烘,什么類(lèi)型的Java代碼會(huì)導(dǎo)致上面的內(nèi)存圖呢?如下所示:

public class MyRunnable implements Runnable() {
    public void run() {
        methodOne();
    }
    public void methodOne() {
        int localVariable1 = 45;
        MySharedObject localVariable2 =
            MySharedObject.sharedInstance;
        //... do more with local variables.
        methodTwo();
    }
    public void methodTwo() {
        Integer localVariable1 = new Integer(99);
        //... do more with local variable.
    }
}
public class MySharedObject {
    //static variable pointing to instance of MySharedObject
    public static final MySharedObject sharedInstance =
    new MySharedObject();
    //member variables pointing to two objects on the heap
    public Integer object2 = new Integer(22);
    public Integer object4 = new Integer(44);
    public long member1 = 12345;
    public long member1 = 67890;
}

如果兩個(gè)線(xiàn)程同時(shí)執(zhí)行 run() 方法橄杨,就會(huì)出現(xiàn)上圖所示的情景秘症。 run() 方法調(diào)用methodOne() 方法, methodOne() 調(diào)用 methodTwo() 方法讥珍。

methodOne() 聲明了一個(gè)原始類(lèi)型的本地變量和一個(gè)引用類(lèi)型的本地變量历极。

每個(gè)線(xiàn)程執(zhí)行 methodOne() 都會(huì)在它們對(duì)應(yīng)的線(xiàn)程棧上創(chuàng)建 localVariable1 和 localVariable2 的私有拷貝。 localVariable1 變量彼此完全獨(dú)立衷佃,僅“生活”在每個(gè)線(xiàn)程的線(xiàn)程棧上趟卸。一個(gè)線(xiàn)程看不到另一個(gè)線(xiàn)程對(duì)它的 localVariable1 私有拷貝做出的修改。

每個(gè)線(xiàn)程執(zhí)行 methodOne() 時(shí)也將會(huì)創(chuàng)建它們各自的 localVariable2 拷貝。然而锄列,兩個(gè) localVariable2 的不同拷貝都指向堆上的同一個(gè)對(duì)象图云。代碼中通過(guò)一個(gè)靜態(tài)變量設(shè)置 localVariable2 指向一個(gè)對(duì)象引用。僅存在一個(gè)靜態(tài)變量的一份拷貝邻邮,這份拷貝存放在堆上竣况。因此, localVariable2 的兩份拷貝都指向由 MySharedObject指向的靜態(tài)變量的同一個(gè)實(shí)例筒严。 MySharedObject 實(shí)例也存放在堆上丹泉。它對(duì)應(yīng)于上圖中的Object3。

注意鸭蛙, MySharedObject 類(lèi)也包含兩個(gè)成員變量摹恨。這些成員變量隨著這個(gè)對(duì)象存放在堆上。這兩個(gè)成員變量指向另外兩個(gè) Integer 對(duì)象娶视。這些 Integer 對(duì)象對(duì)應(yīng)于上圖中的Object2和Object4.

注意晒哄, methodTwo() 創(chuàng)建一個(gè)名為 localVariable 的本地變量。這個(gè)成員變量是一個(gè)指向一個(gè) Integer 對(duì)象的對(duì)象引用肪获。這個(gè)方法設(shè)置 localVariable1 引用指向一個(gè)新的 Integer 實(shí)例寝凌。在執(zhí)行 methodTwo 方法時(shí), localVariable1 引用將會(huì)在每個(gè)線(xiàn)程中存放一份拷貝孝赫。這兩個(gè) Integer 對(duì)象實(shí)例化將會(huì)被存儲(chǔ)堆上较木,但是每次執(zhí)行這個(gè)方法時(shí),這個(gè)方法都會(huì)創(chuàng)建一個(gè)新的 Integer 對(duì)象寒锚,兩個(gè)線(xiàn)程執(zhí)行這個(gè)方法將會(huì)創(chuàng)建兩個(gè)不同的 Integer 實(shí)例劫映。 methodTwo 方法創(chuàng)建的 Integer 對(duì)象對(duì)應(yīng)于上圖中的Object1和Object5。

還有一點(diǎn)刹前, MySharedObject 類(lèi)中的兩個(gè) long 類(lèi)型的成員變量是原始類(lèi)型的。因?yàn)榇粕#@些變量是成員變量喇喉,所以它們?nèi)稳浑S著該對(duì)象存放在堆上,僅有本地變量存放在線(xiàn)程棧上校坑。

硬件內(nèi)存架構(gòu)

現(xiàn)代硬件內(nèi)存模型與Java內(nèi)存模型有一些不同拣技。理解內(nèi)存模型架構(gòu)以及Java內(nèi)存模型如何與它協(xié)同工作也是非常重要的。這部分描述了通用的硬件內(nèi)存架構(gòu)耍目,下面的部分將會(huì)描述Java內(nèi)存是如何與它“聯(lián)手”工作的膏斤。
下面是現(xiàn)代計(jì)算機(jī)硬件架構(gòu)的簡(jiǎn)單圖示:



一個(gè)現(xiàn)代計(jì)算機(jī)通常由兩個(gè)或者多個(gè)CPU。其中一些CPU還有多核邪驮。從這一點(diǎn)可以看出莫辨,在一個(gè)有兩個(gè)或者多個(gè)CPU的現(xiàn)代計(jì)算機(jī)上同時(shí)運(yùn)行多個(gè)線(xiàn)程是可能的。每個(gè)CPU在某一時(shí)刻運(yùn)行一個(gè)線(xiàn)程是沒(méi)有問(wèn)題的。這意味著沮榜,如果你的Java程序是多線(xiàn)程的盘榨,在你的Java程序中每個(gè)CPU上一個(gè)線(xiàn)程可能同時(shí)(并發(fā))執(zhí)行。
每個(gè)CPU都包含一系列的寄存器蟆融,它們是CPU內(nèi)內(nèi)存的基礎(chǔ)草巡。CPU在寄存器上執(zhí)行操作的速度遠(yuǎn)大于在主存上執(zhí)行的速度。這是因?yàn)镃PU訪(fǎng)問(wèn)寄存器的速度遠(yuǎn)大于主存型酥。
每個(gè)CPU可能還有一個(gè)CPU緩存層山憨。實(shí)際上,絕大多數(shù)的現(xiàn)代CPU都有一定大小的緩存層弥喉。CPU訪(fǎng)問(wèn)緩存層的速度快于訪(fǎng)問(wèn)主存的速度萍歉,但通常比訪(fǎng)問(wèn)內(nèi)部寄存器的速度還要慢一點(diǎn)。一些CPU還有多層緩存档桃,但這些對(duì)理解Java內(nèi)存模型如何和內(nèi)存交互不是那么重要枪孩。只要知道CPU中可以有一個(gè)緩存層就可以了。
一個(gè)計(jì)算機(jī)還包含一個(gè)主存藻肄。所有的CPU都可以訪(fǎng)問(wèn)主存蔑舞。主存通常比CPU中的緩存大得多。
通常情況下嘹屯,當(dāng)一個(gè)CPU需要讀取主存時(shí)攻询,它會(huì)將主存的部分讀到CPU緩存中。它甚至可能將緩存中的部分內(nèi)容讀到它的內(nèi)部寄存器中州弟,然后在寄存器中執(zhí)行操作钧栖。當(dāng)CPU需要將結(jié)果寫(xiě)回到主存中去時(shí),它會(huì)將內(nèi)部寄存器的值刷新到緩存中婆翔,然后在某個(gè)時(shí)間點(diǎn)將值刷新回主存拯杠。
當(dāng)CPU需要在緩存層存放一些東西的時(shí)候,存放在緩存中的內(nèi)容通常會(huì)被刷新回主存啃奴。CPU緩存可以在某一時(shí)刻將數(shù)據(jù)局部寫(xiě)到它的內(nèi)存中潭陪,和在某一時(shí)刻局部刷新它的內(nèi)存。它不會(huì)再某一時(shí)刻讀/寫(xiě)整個(gè)緩存最蕾。通常依溯,在一個(gè)被稱(chēng)作“cache lines”的更小的內(nèi)存塊中緩存被更新。一個(gè)或者多個(gè)緩存行可能被讀到緩存瘟则,一個(gè)或者多個(gè)緩存行可能再被刷新回主存黎炉。

Java內(nèi)存模型和硬件內(nèi)存架構(gòu)之間的橋接

上面已經(jīng)提到,Java內(nèi)存模型與硬件內(nèi)存架構(gòu)之間存在差異醋拧。硬件內(nèi)存架構(gòu)沒(méi)有區(qū)分線(xiàn)程棧和堆慷嗜。對(duì)于硬件淀弹,所有的線(xiàn)程棧和堆都分布在主內(nèi)中。部分線(xiàn)程棧和堆可能有時(shí)候會(huì)出現(xiàn)在CPU緩存中和CPU內(nèi)部的寄存器中洪添。如下圖所示:



當(dāng)對(duì)象和變量被存放在計(jì)算機(jī)中各種不同的內(nèi)存區(qū)域中時(shí)垦页,就可能會(huì)出現(xiàn)一些具體的問(wèn)題。主要包括如下兩個(gè)方面:
-線(xiàn)程對(duì)共享變量修改的可見(jiàn)性-當(dāng)讀干奢,寫(xiě)和檢查共享變量時(shí)出現(xiàn)race conditions
下面我們專(zhuān)門(mén)來(lái)解釋以下這兩個(gè)問(wèn)題痊焊。

共享對(duì)象可見(jiàn)性

如果兩個(gè)或者更多的線(xiàn)程在沒(méi)有正確的使用 volatile
聲明或者同步的情況下共享一個(gè)對(duì)象,一個(gè)線(xiàn)程更新這個(gè)共享對(duì)象可能對(duì)其它線(xiàn)程來(lái)說(shuō)是不接見(jiàn)的忿峻。
想象一下薄啥,共享對(duì)象被初始化在主存中。跑在CPU上的一個(gè)線(xiàn)程將這個(gè)共享對(duì)象讀到CPU緩存中逛尚。然后修改了這個(gè)對(duì)象垄惧。只要CPU緩存沒(méi)有被刷新會(huì)主存,對(duì)象修改后的版本對(duì)跑在其它CPU上的線(xiàn)程都是不可見(jiàn)的绰寞。這種方式可能導(dǎo)致每個(gè)線(xiàn)程擁有這個(gè)共享對(duì)象的私有拷貝到逊,每個(gè)拷貝停留在不同的CPU緩存中。
下圖示意了這種情形滤钱。跑在左邊CPU的線(xiàn)程拷貝這個(gè)共享對(duì)象到它的CPU緩存中觉壶,然后將count變量的值修改為2。這個(gè)修改對(duì)跑在右邊CPU上的其它線(xiàn)程是不可見(jiàn)的件缸,因?yàn)樾薷暮蟮腸ount的值還沒(méi)有被刷新回主存中去铜靶。



解決這個(gè)問(wèn)題你可以使用Java中的 volatile
關(guān)鍵字。 volatile
關(guān)鍵字可以保證直接從主存中讀取一個(gè)變量他炊,如果這個(gè)變量被修改后争剿,總是會(huì)被寫(xiě)回到主存中去。

Race Conditions

如果兩個(gè)或者更多的線(xiàn)程共享一個(gè)對(duì)象痊末,多個(gè)線(xiàn)程在這個(gè)共享對(duì)象上更新變量蚕苇,就有可能發(fā)生 race conditions
想象一下舌胶,如果線(xiàn)程A讀一個(gè)共享對(duì)象的變量count到它的CPU緩存中捆蜀。再想象一下,線(xiàn)程B也做了同樣的事情幔嫂,但是往一個(gè)不同的CPU緩存中。現(xiàn)在線(xiàn)程A將 count
加1誊薄,線(xiàn)程B也做了同樣的事情÷亩鳎現(xiàn)在 count
已經(jīng)被增在了兩個(gè),每個(gè)CPU緩存中一次呢蔫。
如果這些增加操作被順序的執(zhí)行切心,變量 count
應(yīng)該被增加兩次飒筑,然后原值+2被寫(xiě)回到主存中去。
然而绽昏,兩次增加都是在沒(méi)有適當(dāng)?shù)耐较虏l(fā)執(zhí)行的协屡。無(wú)論是線(xiàn)程A還是線(xiàn)程B將 count
修改后的版本寫(xiě)回到主存中取,修改后的值僅會(huì)被原值大1全谤,盡管增加了兩次肤晓。
下圖演示了上面描述的情況:


解決這個(gè)問(wèn)題可以使用 Java同步塊 。一個(gè)同步塊可以保證在同一時(shí)刻僅有一個(gè)線(xiàn)程可以進(jìn)入代碼的臨界區(qū)认然。同步塊還可以保證代碼塊中所有被訪(fǎng)問(wèn)的變量將會(huì)從主存中讀入补憾,當(dāng)線(xiàn)程退出同步代碼塊時(shí),所有被更新的變量都會(huì)被刷新回主存中去卷员,不管這個(gè)變量是否被聲明為volatile盈匾。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市毕骡,隨后出現(xiàn)的幾起案子削饵,更是在濱河造成了極大的恐慌,老刑警劉巖未巫,帶你破解...
    沈念sama閱讀 218,640評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件窿撬,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡橱赠,警方通過(guò)查閱死者的電腦和手機(jī)尤仍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,254評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)狭姨,“玉大人宰啦,你說(shuō)我怎么就攤上這事”模” “怎么了赡模?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,011評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀(guān)的道長(zhǎng)师抄。 經(jīng)常有香客問(wèn)我漓柑,道長(zhǎng),這世上最難降的妖魔是什么叨吮? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,755評(píng)論 1 294
  • 正文 為了忘掉前任辆布,我火速辦了婚禮,結(jié)果婚禮上茶鉴,老公的妹妹穿的比我還像新娘锋玲。我一直安慰自己,他們只是感情好涵叮,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,774評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布惭蹂。 她就那樣靜靜地躺著伞插,像睡著了一般。 火紅的嫁衣襯著肌膚如雪盾碗。 梳的紋絲不亂的頭發(fā)上媚污,一...
    開(kāi)封第一講書(shū)人閱讀 51,610評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音廷雅,去河邊找鬼耗美。 笑死,一個(gè)胖子當(dāng)著我的面吹牛榜轿,可吹牛的內(nèi)容都是我干的幽歼。 我是一名探鬼主播,決...
    沈念sama閱讀 40,352評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼谬盐,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼甸私!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起飞傀,我...
    開(kāi)封第一講書(shū)人閱讀 39,257評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤皇型,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后砸烦,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體弃鸦,經(jīng)...
    沈念sama閱讀 45,717評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,894評(píng)論 3 336
  • 正文 我和宋清朗相戀三年幢痘,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了唬格。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,021評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡颜说,死狀恐怖购岗,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情门粪,我是刑警寧澤喊积,帶...
    沈念sama閱讀 35,735評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站玄妈,受9級(jí)特大地震影響乾吻,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜拟蜻,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,354評(píng)論 3 330
  • 文/蒙蒙 一绎签、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧酝锅,春花似錦辜御、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,936評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至阁谆,卻和暖如春碳抄,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背场绿。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,054評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工剖效, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人焰盗。 一個(gè)月前我還...
    沈念sama閱讀 48,224評(píng)論 3 371
  • 正文 我出身青樓璧尸,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親熬拒。 傳聞我的和親對(duì)象是個(gè)殘疾皇子爷光,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,974評(píng)論 2 355

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