并發(fā)

2020/9/1

多任務(wù)(multitasking)是操作系統(tǒng)的一項能力橄碾,看起來可以在同一時間運行多個程序年局。

實際上逻谦,并發(fā)執(zhí)行的進(jìn)程數(shù)目并不受CPU數(shù)量限制颖变,操作系統(tǒng)會為每個進(jìn)程分配CPU時間片生均,給人并行處理的感覺。

多線程(multithreaded)則是更低一層的概念腥刹,單個程序看起來在同一時間完成多個任務(wù)马胧,每個任務(wù)在一個線程(thread)中執(zhí)行。線程是控制線程的簡稱衔峰。

多線程與多進(jìn)程的區(qū)別在于佩脊,每個進(jìn)程都有自己的一整套變量蛙粘,而線程則共享數(shù)據(jù)。因此威彰,線程之間存在風(fēng)險出牧,但是線程之間的通信更有效,更容易歇盼。線程“更輕量級”舔痕,創(chuàng)建撤銷的開銷都比進(jìn)程小。

并發(fā)和并行的區(qū)別

1豹缀、并發(fā)(Concurrent):指兩個或多個事件在同一時間間隔內(nèi)發(fā)生赵讯,即交替做不同事的能力,多線程是并發(fā)的一種形式耿眉。例如垃圾回收時边翼,用戶線程與垃圾收集線程同時執(zhí)行(但不一定是并行的,可能會交替執(zhí)行)鸣剪,用戶程序在繼續(xù)運行组底,而垃圾收集程序運行于另一個CPU上。

2筐骇、并行(Parallel):指兩個或者多個事件在同一時刻發(fā)生债鸡,即同時做不同事的能力。例如垃圾回收時铛纬,多條垃圾收集線程并行工作厌均,但此時用戶線程仍然處于等待狀態(tài)。

什么是線程

一個簡單的創(chuàng)建一個線程的過程:

1告唆,將任務(wù)代碼放在一個類的run方法中棺弊,這個類需要實現(xiàn)一個函數(shù)式接口Runnable。

public interface Runnable{

????void run();

}

2,從這個Runnable構(gòu)造一個Thread對象:

var t = new Thread(r);

3擒悬,啟動線程:

t.start();

綜上模她,一個轉(zhuǎn)賬線程如下:

Runnable?r?=?()?->{

????try{

????????for?(int?i?=?0;?i?<?STEPS;?i++){

????????????double?amount?=?MAX_AMOUNT?*?Math.random();

????????????bank.transfer(0,?1,?amount);

????????????Thread.sleep((int)(DELAY?*?Math.random()));

????????}

????}

????catch(InterruptedException?e){};

};

Thread?t?=?new?Thread(r);

t.start();

也可以通過建立一個Thread的子類來定義線程:

class?MyThread?extends?Thread{

????public?void?run(){

????????task?code

????}

}

然后構(gòu)造一個實例(構(gòu)造這個實例的過程其實就是分配了一個線程),并調(diào)用start方法來運行懂牧,不要調(diào)用run方法侈净,run方法只會在本線程中執(zhí)行這個任務(wù)。

現(xiàn)在不再推薦這種方法僧凤,應(yīng)當(dāng)把并行運行的任務(wù)與運行機(jī)制解耦畜侦。

線程狀態(tài)

有以下六種狀態(tài):

New(新建)

Runnable(可運行)

Blocked(阻塞)

Waiting(等待)

Timed waiting(計時等待)

Terminated(終止)


新建線程

當(dāng)用new Thread(r)創(chuàng)建一個新線程時,這個線程還沒有運行躯保,處于新建狀態(tài)旋膳。

可運行線程

一旦調(diào)用start方法,線程就處于可運行(runnable)狀態(tài)吻氧∧缬牵可運行代表進(jìn)入了排隊咏连,并不表示正在運行盯孙,可能在運行也可能沒有在運行鲁森。

操作系統(tǒng)可能提供搶占式調(diào)度系統(tǒng),它會給每一個可運行線程一個時間片來執(zhí)行任務(wù)振惰。當(dāng)時間片用完歌溉,操作系統(tǒng)就會剝奪該線程的運行權(quán),并給另一個線程一個機(jī)會來運行骑晶。

像手機(jī)這樣的小型設(shè)備痛垛,可能會使用協(xié)作式調(diào)度:一個線程只有再調(diào)用yield方法或者被阻塞或等待時才失去控制權(quán)。

阻塞和等待線程

阻塞和等待線程會退出排隊桶蛔,需要線程調(diào)度器重新激活才會進(jìn)入到排隊匙头。兩個區(qū)別在于怎樣退出排隊

阻塞線程:當(dāng)一個線程視圖獲取一個內(nèi)部的對象的鎖,但是鎖被其他線程占有仔雷,這個線程就會被阻塞蹂析。當(dāng)所有的線程都釋放了鎖,并且線程調(diào)度器允許這個線程持有這個鎖碟婆,它將變成可運行狀態(tài)电抚。

等待線程:當(dāng)線程等待另一個線程通知調(diào)度器出現(xiàn)一個條件時,這個線程就會進(jìn)入等待狀態(tài)竖共。

計時等待:有幾個方法有超時參數(shù)蝙叛,調(diào)用這些方法,線程就會進(jìn)入計時等待狀態(tài)公给。例如Tread.sleep()

終止線程

run方法正常推出或者出現(xiàn)了一個沒有被捕獲的異常而意外終止借帘。

線程屬性

線程屬性

中斷線程

線程有一個中斷狀態(tài),可以調(diào)用interrupt方法來設(shè)置它淌铐。書上講的不是很好姻蚓,這里引用別人的文章。

參考Java中interrupt的使用

守護(hù)線程

可以通過調(diào)用

t.setDaemon(boolean isDaemon)匣沼;

將一個線程轉(zhuǎn)換為守護(hù)線程(daemon thread)狰挡。這樣一個線程沒什么用。唯一用途是為其它線程提供服務(wù)释涛。

在Java中有兩類線程:用戶線程 (User Thread)加叁、守護(hù)線程 (Daemon Thread)。

所謂守護(hù) 線程唇撬,是指在程序運行的時候在后臺提供一種通用服務(wù)的線程它匕,比如垃圾回收線程就是一個很稱職的守護(hù)者,并且這種線程并不屬于程序中不可或缺的部分窖认。因此豫柬,當(dāng)所有的非守護(hù)線程結(jié)束時告希,程序也就終止了,同時會殺死進(jìn)程中的所有守護(hù)線程烧给。反過來說燕偶,只要任何非守護(hù)線程還在運行,程序就不會終止础嫡。

用戶線程和守護(hù)線程兩者幾乎沒有區(qū)別指么,唯一的不同之處就在于虛擬機(jī)的離開:如果用戶線程已經(jīng)全部退出運行了,只剩下守護(hù)線程存在了榴鼎,虛擬機(jī)也就退出了伯诬。 因為沒有了被守護(hù)者,守護(hù)線程也就沒有工作可做了巫财,也就沒有繼續(xù)運行程序的必要了盗似。

線程名

這里的線程名不是指線程實例的標(biāo)識符,可以通過

t.setName("Web crawler");

來設(shè)置名字平项,這在線程轉(zhuǎn)儲時可能很有用赫舒。

未捕獲異常的處理器

我們經(jīng)常使用try..catch進(jìn)行異常處理,但是對于Uncaught Exception是沒辦法捕獲的葵礼。對于這類異常如何處理呢号阿?

回顧一下thread的run方法,有個特別之處鸳粉,它不會拋出任何檢查型異常扔涧,但異常會導(dǎo)致線程終止運行。這非常糟糕届谈,我們必須要“感知”到異常的發(fā)生枯夜。比如某個線程在處理重要的事務(wù),當(dāng)thread異常終止艰山,我必須要收到異常的報告(email或者短信)湖雹。

在jdk 1.5之前貌似無法直接設(shè)置thread的Uncaught Exception Handler(具體未驗證過),但從1.5開始可以對thread設(shè)置handler曙搬。

1. As the handler for a particular thread

當(dāng) uncaught exception 發(fā)生時, JVM尋找這個線程的異常處理器. 可以使用如下的方法為當(dāng)前線程設(shè)置處理器

public class MyRunnable implements Runnable{

????public void run(){

????????// Other Code

????????Thread.currentThread().setUncaughtExceptionHandler(myHandler);

????????// Other Code

????}

}

2. As the handler for a particular thread group

如果這個線程屬于一個線程組摔吏,且線程級別并未指定異常處理器(像上節(jié)As the handler for a particular thread中那樣),jvm則試圖調(diào)用線程組的異常處理器纵装。

線程組(ThreadGroup)實現(xiàn)了Thread.UncaughtExceptionHandler接口征讲,所以我們只需要在ThreadGroup子類中重寫實現(xiàn)即可

public class ThreadGroup implements Thread.UncaughtExceptionHandler

java.lang.ThreadGroup 類uncaughtException默認(rèn)實現(xiàn)的邏輯如下:

如果父線程組存在, 則調(diào)用它的uncaughtException方法.

如果父線程組不存在, 但指定了默認(rèn)處理器 (下節(jié)中的As the default handler for the application), 則調(diào)用默認(rèn)的處理器

如果默認(rèn)處理器沒有設(shè)置, 則寫錯誤日志.但如果 exception是ThreadDeath實例的話, 忽略。

對應(yīng)JDK的源碼:

public void uncaughtException(Thread t, Throwable e) {

????if (parent!=null) {

????????parent.uncaughtException(t, e);

????}else{? ? ? ? ? ?

????????Thread.UncaughtExceptionHandler ueh = Thread.getDefaultUncaughtExceptionHandler();

????????if(ueh !=null) {? ? ? ? ? ? ? ?

????????????ueh.uncaughtException(t, e);? ? ? ? ? ?

? ? ? ? ?}elseif(!(einstanceofThreadDeath)) {

? ? ? ? ? ? System.err.print("Exception in thread \""+ t.getName() +"\" ");? ? ? ? ? ? ? ? ????????????e.printStackTrace(System.err);? ? ? ? ? ?

????????}? ? ? ?

????}? ?

}

3. As the default handler for the application (JVM)

Thread.setDefaultUncaughtExceptionHandler(myHandler);

線程優(yōu)先級

在Java程序設(shè)計語言中橡娄,每一個線程都有一個優(yōu)先級诗箍。默認(rèn)情況下,一個線程會繼承創(chuàng)建它的那個線程的優(yōu)先級挽唉÷俗妫可以用setPriority方法來設(shè)置線程筷狼。

Java中優(yōu)先級可以設(shè)置為MIN_PRIORITY(1)與MAX_PRIORITY(10)和NORM_PRIORITY(5)。

但是匠童,優(yōu)先級高度依賴于操作系統(tǒng)埂材,不同的操作系統(tǒng)有不同個數(shù)的優(yōu)先級,虛擬機(jī)要映射到平臺的優(yōu)先級俏让。因此現(xiàn)在不要使用優(yōu)先級楞遏。

同步

在大多數(shù)程序中茬暇,會有多個線程共享統(tǒng)一數(shù)據(jù)首昔,如果多個線程對其進(jìn)行修改,就會導(dǎo)致數(shù)據(jù)被破壞糙俗,這種情況叫做競態(tài)條件勒奇。

為了避免多線程破壞共享數(shù)據(jù),就需要學(xué)會同步存取

有個常見但是容易出錯的例子:

多個線程執(zhí)行

i += 1;

問題在于巧骚,這條語句并不是一個原子操作赊颠,它的執(zhí)行步驟分為三布:

1,首先取出i的值劈彪,將其加載到寄存器

2竣蹦,執(zhí)行i+1

3,將結(jié)果寫回 i 沧奴。

相當(dāng)于 i = i +1

當(dāng)一個線程執(zhí)行1痘括,2兩步,這時運行權(quán)被轉(zhuǎn)移滔吠,另一個線程開始執(zhí)行纲菌,就會出現(xiàn)問題。會有一條線程覆蓋結(jié)果導(dǎo)致數(shù)據(jù)破壞疮绷。

在后面的voliate關(guān)鍵詞時會提到翰舌。

鎖對象

有兩種機(jī)制可以防止并發(fā)訪問代碼塊:synchronized關(guān)鍵詞和ReentranLock類。

這里先說ReentranLock類冬骚,基本結(jié)構(gòu)如下

myLock.lock() ;//myLock是一個ReentranLock類的實例

try {

????critical section

}

finally {

myLock.unlock() ;

}

可以用來保護(hù)一個類的方法椅贱,如下

public?class?Bank{

????private?ReentrantLock?banklock?=?new?ReentrantLock();

????...

????public?void?transfer(int?from,?int?to,?int?amount){

????????banklock.lock();

????????try{

????????????...

????????}

????????finally{

????????????banklock.unlock();

????????}

????}

}

每個線程再調(diào)用transfer之前,都要查看是否有鎖只冻。

ReentrantLock類稱為重用(reentrant)鎖庇麦,因為可以在一段有鎖的代碼中調(diào)用另一段有鎖的代碼。鎖有一個持有計數(shù)來跟蹤對lock方法的嵌套調(diào)用属愤。

例如女器,transfer方法調(diào)用getTotalBalance方法,第二個方法也會封鎖bankLock對象住诸,這是bankLock的持有計數(shù)就會加一驾胆,變成2(因為bankLock是Bank類中的私有變量)涣澡。直到鎖的計數(shù)變?yōu)榱悖艜尫沛i丧诺。

條件對象

可以使用一個條件對象來管理那些已經(jīng)獲得了一個鎖卻不能做有用工作的線程入桂。由于歷史原因條件對象經(jīng)常被稱為條件變量。

可以給鎖對象關(guān)聯(lián)一個條件對象condition驳阎。這個對象的功能是可以將一個線程進(jìn)入等待狀態(tài)抗愁,如果滿足條件就可以在其他線程中恢復(fù)等待的線程,使用方法如下:

public?class?Bank{

? ? ...

???private?Lock?bankLock;

???private?Condition?sufficientFunds;

???public?Bank(int?n,?double?initialBalance) {

? ? ? ?...

??????bankLock?=?new?ReentrantLock();

??????sufficientFunds?=?bankLock.newCondition();

???}

???public?void?transfer(int?from,?int?to,?double?amount)?throws?InterruptedException {

??????bankLock.lock();

??????try{

?????????while?(accounts[from]?<?amount)

????????????sufficientFunds.await();

? ? ? ? ...

?????????sufficientFunds.signalAll();

??????}

??????finally {

?????????bankLock.unlock();

??????}

???}

synchronized關(guān)鍵字

先對鎖進(jìn)行總結(jié):

·鎖用來保護(hù)代碼片段呵晚,一次只能有一個線程執(zhí)行被保護(hù)的片段蜘腌。

·鎖可以管理試圖進(jìn)入被保護(hù)代碼段的線程

·一個鎖可以一個或多個相關(guān)聯(lián)的條件對象

·每個條件對象,管理那些已經(jīng)進(jìn)入被保護(hù)代碼段但還不能與逆行的線程饵隙。

Lock和Condition兩個接口已經(jīng)允許程序員充分控制鎖定撮珠,但是大部分情況下這樣比較麻煩〗鹈可以使用一種Java語言的內(nèi)置機(jī)制芯急,synchronized關(guān)鍵字。

Java中的每個對象都有一個內(nèi)部鎖驶俊,這個鎖只有一個關(guān)聯(lián)條件娶耍,可以直接調(diào)用wait()和notifyAll來代替condition.await()和condition.signalAll();

原理是一樣的,用synchronized的內(nèi)部鎖來封鎖整個方法饼酿。

結(jié)構(gòu)類似榕酒。

內(nèi)部鎖和條件存在一些限制:

·不能中斷一個正在嘗試獲得鎖的線程

·不能指定嘗試獲得鎖時的超時時間。

·每個鎖僅有一個條件可能是不夠的

在代碼中使用哪種做法有一些建議:

·最好既不使Lock/Condition也不使用synchronized關(guān)鍵字嗜湃。在許多情況下奈应,可以使用java.util.concurrent包中的某種機(jī)制,它會為你處理所有的鎖定购披。例如杖挣,使用阻塞隊列來同步完成一個共同任務(wù)的線程。還應(yīng)當(dāng)研究并行流

·如果synchronized關(guān)鍵字適合你的程序刚陡,那么盡量使用這種做法惩妇,這樣可以減少編寫的代碼量。

·如果特別需要Lock/Condition結(jié)構(gòu)提供的額外能力則使用Lock/Condition

同步塊

區(qū)別:

同步方法默認(rèn)用this或者當(dāng)前類class對象作為鎖筐乳;

同步代碼塊可以選擇以什么來加鎖歌殃,比同步方法要更細(xì)顆粒度,我們可以選擇只同步會發(fā)生同步問題的部分代碼而不是整個方法蝙云;

同步方法使用關(guān)鍵字 synchronized修飾方法氓皱,而同步代碼塊主要是修飾需要進(jìn)行同步的代碼,用 synchronized(object){代碼內(nèi)容}進(jìn)行修飾;

同步代碼塊波材,即有synchronized修飾符修飾的語句塊股淡,被該關(guān)鍵詞修飾的語句塊,將加上內(nèi)置鎖廷区。實現(xiàn)同步唯灵。

例:synchronized(Object o ){}

同步是高開銷的操作,因此盡量減少同步的內(nèi)容隙轻。通常沒有必要同步整個方法埠帕,同步部分代碼塊即可。

同步方法默認(rèn)用this或者當(dāng)前類class對象作為鎖玖绿。

同步代碼塊可以選擇以什么來加鎖敛瓷,比同步方法要更顆粒化镰矿,我們可以選擇只同步會發(fā)生問題的部分代碼而不是整個方法琐驴。

監(jiān)視器概念

監(jiān)視器(monitor)是一個相互排斥且具備同步能力的對象俘种。監(jiān)視器中的一個時間點上秤标,只能有一個線程執(zhí)行一個方法。線程通過獲取監(jiān)視器上的鎖進(jìn)入監(jiān)視器宙刘,并且通過釋放鎖退出監(jiān)視器苍姜。任意對象都可能是一個監(jiān)視器。一旦一個線程鎖住對象悬包,該對象就成為監(jiān)視器衙猪。加鎖是通過在方法或塊上使用synchronized關(guān)鍵字來實現(xiàn)的。在執(zhí)行同步方法或塊之前布近,線程必須獲得鎖垫释。如果條件不適合線程繼續(xù)在監(jiān)視器內(nèi)執(zhí)行,縣城可能在監(jiān)視器中等待撑瞧】闷可以對監(jiān)視器調(diào)用wait()方法來釋放鎖,這樣其他的一些監(jiān)視器中的線程就可以獲取它预伺,也就有可能改變監(jiān)視器中的狀態(tài)订咸。當(dāng)條件滿足時,另一線程可以調(diào)用notify()方法或notifyAll()方法來通知一個或所有的等待線程重新獲取鎖并且恢復(fù)執(zhí)行酬诀。

關(guān)鍵詞volatile

也可以用volatile來進(jìn)行同步脏嚷,但是,volatile并不保證原子性瞒御。

volatile可以用來修飾線程之間共享的變量父叙,如果只是對這個變量進(jìn)行原子性操作,例如賦值,就可以用這個來避免使用鎖趾唱。

這里的原子性:

volatile int i = 1屿岂;

...

i = 2;//是原子性操作

i += 1;//不是原子性操作,分三步

i = atomiclong.incrementAndGet();//是原子性鲸匿,這是一個用機(jī)器碼寫的自增方法爷怀,沒有用鎖。


final變量

final某些方面也可以保證能安全的訪問一個字段

final var accounts = new HashMap<String, Double>();

上面這個聲明的作用带欢,是保證一個線程在創(chuàng)建accounts時运授,其他線程不會讀到null,只會訪問到新構(gòu)造的HashMap乔煞。但這不是線程安全的吁朦,注意。

死鎖

例如轉(zhuǎn)賬程序渡贾,有幾種可能會導(dǎo)致死鎖的情況逗宜。

比如各賬戶轉(zhuǎn)賬,但是每個賬戶的余額都不夠空骚;或者signalAll方法改為signal纺讲,隨機(jī)喚醒一個等待中的線程,但是這個線程的條件仍然不滿足囤屹,繼續(xù)轉(zhuǎn)賬導(dǎo)致錢不夠熬甚,運行的線程也等待,導(dǎo)致所有線程等待肋坚。

線程局部變量

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末乡括,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子智厌,更是在濱河造成了極大的恐慌诲泌,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,681評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件铣鹏,死亡現(xiàn)場離奇詭異敷扫,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)吝沫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,205評論 3 399
  • 文/潘曉璐 我一進(jìn)店門呻澜,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人惨险,你說我怎么就攤上這事羹幸。” “怎么了辫愉?”我有些...
    開封第一講書人閱讀 169,421評論 0 362
  • 文/不壞的土叔 我叫張陵栅受,是天一觀的道長。 經(jīng)常有香客問我,道長屏镊,這世上最難降的妖魔是什么依疼? 我笑而不...
    開封第一講書人閱讀 60,114評論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮而芥,結(jié)果婚禮上律罢,老公的妹妹穿的比我還像新娘。我一直安慰自己棍丐,他們只是感情好误辑,可當(dāng)我...
    茶點故事閱讀 69,116評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著歌逢,像睡著了一般巾钉。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上秘案,一...
    開封第一講書人閱讀 52,713評論 1 312
  • 那天砰苍,我揣著相機(jī)與錄音,去河邊找鬼阱高。 笑死赚导,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的讨惩。 我是一名探鬼主播辟癌,決...
    沈念sama閱讀 41,170評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼荐捻!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起寡夹,我...
    開封第一講書人閱讀 40,116評論 0 277
  • 序言:老撾萬榮一對情侶失蹤处面,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后菩掏,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體魂角,經(jīng)...
    沈念sama閱讀 46,651評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,714評論 3 342
  • 正文 我和宋清朗相戀三年智绸,在試婚紗的時候發(fā)現(xiàn)自己被綠了野揪。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,865評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡瞧栗,死狀恐怖斯稳,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情迹恐,我是刑警寧澤挣惰,帶...
    沈念sama閱讀 36,527評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響憎茂,放射性物質(zhì)發(fā)生泄漏珍语。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,211評論 3 336
  • 文/蒙蒙 一竖幔、第九天 我趴在偏房一處隱蔽的房頂上張望板乙。 院中可真熱鬧,春花似錦拳氢、人聲如沸亡驰。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,699評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽凡辱。三九已至,卻和暖如春栗恩,著一層夾襖步出監(jiān)牢的瞬間透乾,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,814評論 1 274
  • 我被黑心中介騙來泰國打工磕秤, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留乳乌,地道東北人。 一個月前我還...
    沈念sama閱讀 49,299評論 3 379
  • 正文 我出身青樓市咆,卻偏偏與公主長得像汉操,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子蒙兰,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,870評論 2 361