ReentrantLock之FairSync公平鎖加鎖過程

下面會通過一步步的場景進行分析ReentrantLock公平鎖的源碼,文章為比較長,做好準備。。。

首先ReentrantLock默認創建的就是非公平鎖,所以要在ReentrantLock的構造函數中傳入true,創建公平鎖

ReentrantLock lock = new ReentrantLock(true);

lock.lock();

下面我們來一步步分析公平鎖進行加鎖 lock 的源碼實現:

注意,會通過一個個線程進入lock的形式進行分析;

當線程T1調用 lock():注意T1是第一個去競爭鎖的線程,后面的T2,T3。。默認都是在T1還沒執行完的情況下去分析的;

public voidlock() {

?? ?sync.lock();

}

繼而進入FairSync里面的lock()方法:

final voidlock() {

?? ?acquire(1);

}

接著進入acquire(1)方法,這里面才是重點:

public final voidacquire(intarg) {

?? ?if(!tryAcquire(arg) &&

?? ?acquireQueued(addWaiter(Node.EXCLUSIVE),arg))

?? ?selfInterrupt();

}

這個acquire(int arg)由四個方法構成:

tryAcquire:嘗試獲得鎖

addWaiter:將當前線程封裝成Node對象

acquireQueued:將當前線程表示的Node對象放到鏈表中進行排隊,同時park當前線程;

selfInterrupt:相應線程中斷;

代碼塊1 :分析acquire方法;

public final voidacquire(intarg) {

//首先會進入tryAcquire(arg)方法,看到這行注釋,請把目光移到下面代碼塊2中。。。

//代碼塊2 返回了true,而!tryAcquire(arg) 將返回結果取了反,也就是true變為了false,所以該方法結束,當前線程T1已經占有鎖;

?? ?if(!tryAcquire(arg) &&

?? ?? ??acquireQueued(addWaiter(Node.EXCLUSIVE),arg))

?? ??? ??? ?selfInterrupt();

}

代碼塊2 :

tryAcquire中的源碼:

protected final boolean tryAcquire(int acquires) {

//current是當前來獲取鎖的這個線程,也就是我們說的T1線程

????????final Thread current = Thread.currentThread();

? ? //state 表示鎖狀態,默認是0

//因為T1是第一個進來加鎖的,所以這里getState()得到的結果肯定等于0,所以 c = 0

????????int c = getState();

????????if (c == 0) {

?? ??? ?? ? //因為c=0,那么進入到這里

?? ??? ?? ? //hasQueuedPredecessors:作用主要是判斷當前AQS隊列中有沒有其他線程在排隊,具體看下面詳細說明,請將目光移到?代碼塊3

//通過分析代碼塊3,得知hasQueuedPredecessors會返回false,而!hasQueuedPredecessors()取了反,所以這里的結果變成了true,

?? ??? ?? ? //接著進入了compareAndSetState,進行CAS操作,將state改變為1,表示占有鎖 ,返回true

//compareAndSetState?返回了true ,進入到?setExclusiveOwnerThread,這里是將當前線程設置為占有鎖線程,用來支持鎖重入

?? ??? ?? ? //結果返回true,將true帶回到代碼塊1

????????????if (!hasQueuedPredecessors() &&

????????????????????compareAndSetState(0, acquires)) {

????????????????setExclusiveOwnerThread(current);

????????????????return true;

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

????????}

????????else if (current == getExclusiveOwnerThread()) {

????????????int nextc = c + acquires;

????????????if (nextc < 0)

????????????????throw new Error("Maximum lock count exceeded");

????????????setState(nextc);

????????????return true;

????????}

????????return false;

????}

}

代碼塊3:

hasQueuedPredecessors中的源碼:

public final boolean hasQueuedPredecessors() {

? //因為T1是第一個進來加鎖的線程,那么這里的tail = null , head = null,因為還沒有線程去初始化他們,所以肯定是null;

????Node t = tail;

????Node h = head;

????Node s;

//h != t 相當于 head != tail 相當于 null != null,所以這里肯定返回的是false,下面的判斷就不用再看了,先分析T1進來的情況

????return h != t &&

????????((s = h.next) == null || s.thread != Thread.currentThread());

}

然后返回false;帶著false將目光回到代碼塊2

總結:

當第一個線程進來獲得鎖的時候,會將state由原來默認的0改為1,注意,此時并沒有初始化鏈表,也就是說,如果線程都是交替執行,那么永遠跟AQS隊列無關;

假設 T1 還沒有執行完,此時 T2 來了,也調用了 lock() ,T2 也要獲得鎖:

通過上面的分析,我們直接進入到這個方法:注意這里是T2線程,T1還沒有執行完,鎖還在T1那里

代碼塊1 :分析acquire方法;

public final voidacquire(intarg) {

? ? //首先會進入tryAcquire(arg)方法,看到這行注釋,請把目光移到下面代碼塊2中。。。

? ? //代碼塊2 返回了false,而?!tryAcquire(arg) 將返回結果取了反,也就是false變為了true,這邏輯將進入到acquireQueued(addWaiter(Node.EXCLUSIVE),arg) ,

//首先會先進入到addWaiter(Node.EXCLUSIVE),請將目光轉向代碼塊3

//addWaiter(Node.EXCLUSIVE)經過代碼塊3和代碼塊4的分析,這里返回了當前線程T2表示的node對象,帶著這個返回的node進入到acquireQueued方法當中,帶著node 和 1,將目光移到代碼塊5

?? ?if(!tryAcquire(arg) &&

???? ??acquireQueued(addWaiter(Node.EXCLUSIVE),arg))

?? ??? ??? ?selfInterrupt();

}

代碼塊2 :

tryAcquire中的源碼:

protected final boolean tryAcquire(int acquires) {

? ? //當前線程,也就是T2線程

????final Thread current = Thread.currentThread();

? ? //因為T1線程還沒有執行完,所以T1線程還持有鎖,那么getState()返回的必然不是0,假設T1沒有重入,那么getState()返回的是1 , c = 1?

????int c = getState();

? ? // 因為 c = 1 ,所以邏輯不會進入到這里

????if (c == 0) {

????????if (!hasQueuedPredecessors() &&

????????????????compareAndSetState(0, acquires)) {

????????????setExclusiveOwnerThread(current);

????????????return true;

????????}

????}

// 因為 c = 1 所以來到這個判斷,getExclusiveOwnerThread返回的當前持有鎖的線程,也就是T1, current = T2,那么T2 == T1?當然不等于,返回false,帶著false回到代碼塊1中

????else if (current == getExclusiveOwnerThread()) {

????????int nextc = c + acquires;

????????if (nextc < 0)

????????????throw new Error("Maximum lock count exceeded");

????????setState(nextc);

????????return true;

????}

????return false;

}

代碼塊3:

private Node addWaiter(Node mode) {?? ?

? ? //將當前線程封裝為Node對象,這個Node當中維護了當前線程的狀態、前一個節點,后一個節點,當前線程的引用等等

????Node node = new Node(Thread.currentThread(), mode);?? ?

? ? //因為T1獲得鎖的時候,并沒有初始化AQS隊列,所以head節點和tail節點都是null,所以pred = tail = null;

????Node pred = tail;

? ? // pred = null ,所以這個條件不成立,將進入下面的enq方法

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

????????node.prev = pred;

????????if (compareAndSetTail(pred, node)) {

????????????pred.next = node;

????????????return node;

????????}

????}

? ? //當線程第一次初始化AQS隊列的時候,也就是隊列中還沒有任何等待的線程,enq方法很重要,請將目光移到代碼塊4:

????enq(node);

? ? //代碼塊4執行完,返回了當前線程T2表示的node節點,返回node,帶著node回到代碼塊1中

????return node;

}

代碼塊4:

private Node enq(final Node node) {

? ? //這里是個死循環

????for (;;) {

?? ?? ? //第一次進入循環,tail 肯定為 null ,因為隊列中沒有其他等待的線程,所以 t = tail = null

?? ?? ? //第二次進入循環,t = tail = head = new Node();

????????Node t = tail;

//第一次進入循環,t = null,所以會進入到里面的邏輯

?//第二次進入循環,t = tail = head = new Node(),也就是不等于null,所以進入到else 邏輯;

????????if (t == null) { // Must initialize

//第一次進入循環,當tail = null 的時候,說明隊列還沒有被初始化,也就是AQS還沒有任何線程進來排隊過,通過compareAndSetHead 為 head 創建一個空Node,哨兵節點

//第一次進入循環,同時tail = head = new Node(),接著進入下一次循環

????????????if (compareAndSetHead(new Node()))

????????????????tail = head;

????????} else {

?? ??? ?? ? //第二次循環會進入到這里,因為第一次循環中創建了哨兵節點head

?? ??? ?? ? // node.prev = t :當前節點的前驅節點指向 t = tail = head,也就是指向head節點

????????????node.prev = t;

?? ??? ?? ? // 通過CAS將當前T2線程表示的節點node設置tail節點

????????????if (compareAndSetTail(t, node)) {

?? ??? ??? ?? ? //t.next = node ,t = head,所以將head節點的后繼節點指向當前T2線程表示的節點

????????????????t.next = node;

?? ??? ??? ?//返回當前節點,帶著當前線程T2表示的node,返回到代碼塊3

????????????????return t;

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

????????}

????}

}

代碼塊5:

final boolean acquireQueued(final Node node, int arg) {

????boolean failed = true;

????try {

????????boolean interrupted = false;

?? ?? ? //這里是一個死循環

????????for (;;) {

?? ??? ?? ? //第一次循環,獲得當前節點的前驅節點,也就是head節點

?? ??? ?? ? //第二次循環,獲得當前節點的前驅節點,也就是head節點

????????????final Node p = node.predecessor();

//第一次循環,p == head ? 當然等于,所以進入tryAcquire嘗試獲得鎖,因為前面的處理過程中,可能T1已經執行完了,這里其實也相當于自旋,我們假設T1還沒有執行完,這里沒有獲取到鎖返回false

//第二次循環,P == head ? 當然等于,所以再次進入到tryAcquire中嘗試獲得鎖,又一次自旋,假設還是沒有獲得到鎖,因為T1還沒有執行完,鎖還沒有被釋放

????????????if (p == head && tryAcquire(arg)) {

????????????????setHead(node);

????????????????p.next = null; // help GC

????????????????failed = false;

????????????????return interrupted;

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

?? ??? ?//第一次循環,帶著當前節點node的前驅節點head,和當前節點node,進入到shouldParkAfterFailedAcquire方法之中,這里很重要,請看代碼塊6

//第一次循環,從代碼塊6中的shouldParkAfterFailedAcquire返回了false,那么第一次循環結束

//第二次循環,帶著當前節點node的前驅節點head,和當前節點node,再次進入到shouldParkAfterFailedAcquire方法之中,請看代碼塊6

//第二次循環,從代碼塊6中的shouldParkAfterFailedAcquire返回了true,代表著將進入parkAndCheckInterrupt方法將當前線程休眠,等待被喚醒,代碼阻塞在里面,至此,T2進入睡眠;

????????????if (shouldParkAfterFailedAcquire(p, node) &&

????????????????????parkAndCheckInterrupt())

????????????????interrupted = true;

????????}

????} finally {

????????if (failed)

????????????cancelAcquire(node);

????}

}

代碼塊6:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {

????//外面第一次循環:pred = head , node 為 T2,因為T2剛剛才初始化了head節點,所以head節點中的waitStatus = 0;

? ? //外面第一次循環:ws = 0 ;

? ? //外面第二次循環:因為在第一次的時候,我們已經將head節點中waitStatus改為-1

????int ws = pred.waitStatus;

? ? //外面第一次循環:ws == Node.SIGNAL == -1?ws = 0,所以不會進入到這里面

? ? //外面第二次循環:ws == Node.SIGNAL == -1?ws = -1,所以會進入到這里面

????if (ws == Node.SIGNAL)

????????/*

???????? * This node has already set status asking a release

???????? * to signal it, so it can safely park.

???????? */

?? ?? ? //返回true,帶著這個true,回到代碼塊5中

????????return true;

????if (ws > 0) {

????????/*

???????? * Predecessor was cancelled. Skip over predecessors and

???????? * indicate retry.

???????? */

????????do {

????????????node.prev = pred = pred.prev;

????????} while (pred.waitStatus > 0);

????????pred.next = node;

????} else {

//外面第一次循環:會來到這里,通過CAS將head節點中的waitStatus 改 -1,然后返回false,帶著false回到代碼塊5

????????compareAndSetWaitStatus(pred, ws, Node.SIGNAL);

????}

????return false;

}

總結:T2在經過一次獲取鎖,兩次自旋后還是沒有獲得到鎖,初始化了head哨兵節點,并將head節點中的waitStatus = -1 ,T2進入了睡眠,等待喚醒;

此時,T1還是沒有執行完,T3又來競爭鎖:

代碼塊1:

public final void acquire(int arg) {

//還是通過tryAcquire嘗試獲得鎖,這里不在分析tryAcquire,因為T1還沒有被執行完,T2正在睡眠,tryAcquire還是沒有獲得到鎖,返回false,則進入代碼塊2

//代碼塊2中,addWaiter成功將當前T3表示的node加入到鏈表尾部,并返回當前node

? ? //帶著當前線程T3表示的node進入到acquireQueued,這里面其實就是自旋和當前是否休眠的操作,請看代碼塊3

????if (!tryAcquire(arg) &&

????????acquireQueued(addWaiter(Node.EXCLUSIVE), arg))

????????selfInterrupt();

}

代碼塊2:

private Node addWaiter(Node mode) {?? ?

? ? //將當前線程封裝為Node對象,這個Node當中維護了當前線程的狀態、前一個節點,后一個節點,當前線程的引用等等

????Node node = new Node(Thread.currentThread(), mode);?? ?

? ? //因為T2剛剛已經初始化了AQS隊列,并創建了head哨兵節點,同時將tail 設置了等于T2線程表示的node,所以tail = T2表示的node;

????Node pred = tail;

? ? // pred = T2node? ,所以這個條件成立,將進入方法

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

?? ?? ? //將當前節點的前驅節點指向T2表示的node

????????node.prev = pred;

?? ?? ? //將tail表示的節點等于當前節點T3表示的node,相當于T3表示node是尾節點

????????if (compareAndSetTail(pred, node)) {

?? ??? ?? ? //pred = T2表示的node,所以這里是將T2表示的node的下一個節點指向當前T3表示的node,這就是一個雙向鏈表的數據結構,即使后面T4,T5。。。也是這樣鏈接起來

????????????pred.next = node;

// 返回當前node,帶著node回到代碼塊1中

????????????return node;

????????}

????}

? ? //當線程第一次初始化AQS隊列的時候,也就是隊列中還沒有任何等待的線程,enq方法很重要,請將目光移到代碼塊4:

????enq(node);

? ? //代碼塊4執行完,返回了當前線程T2表示的node節點,返回node,帶著node回到代碼塊1中

????return node;

}

代碼塊3:

final boolean acquireQueued(final Node node, int arg) {

????boolean failed = true;

????try {

????????boolean interrupted = false;

?? ?? ? //這里是一個死循環

????????for (;;) {

?? ??? ?? ? //第一次循環,獲得當前節點的前驅節點,也就是T2表示的node節點

?? ??? ?//第二次循環,獲得當前節點的前驅節點,也就是T2表示的node節點

????????????final Node p = node.predecessor();

?? ??? ?? ? //第一次循環,p == head ? 當然不等于,因為現在當前節點的前驅節點是T2表示的node節點,直接返回了false,進入到下面的shouldParkAfterFailedAcquire當中,請看代碼塊4

?? ??? ?? ? //第二次循環,P == head ? 當然不等于,所以再次進入到tryAcquire中嘗試獲得鎖,又一次自旋,假設還是沒有獲得到鎖,因為T1還沒有執行完,鎖還沒有被釋放

????????????if (p == head && tryAcquire(arg)) {

????????????????setHead(node);

????????????????p.next = null; // help GC

????????????????failed = false;

????????????????return interrupted;

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

?? ??? ?? ? //第一次循環,帶著當前節點node的前驅節點head,和當前節點node,進入到shouldParkAfterFailedAcquire方法之中,這里很重要,請看代碼塊4

?? ??? ?? ? //第一次循環,從代碼塊4中的shouldParkAfterFailedAcquire返回了false,那么第一次循環結束

?? ??? ?? ? //第二次循環,帶著當前節點T3表示的node的前驅節點T2 node,和當前節點T3 node,再次進入到shouldParkAfterFailedAcquire方法之中,請看代碼塊4

?? ??? ?? ? //第二次循環,從代碼塊4中的shouldParkAfterFailedAcquire返回了true,代表著將進入parkAndCheckInterrupt方法將當前線程休眠,等待被喚醒,代碼阻塞在里面,至此,T3進入睡眠;

????????????if (shouldParkAfterFailedAcquire(p, node) &&

????????????????????parkAndCheckInterrupt())

????????????????interrupted = true;

????????}

????} finally {

????????if (failed)

????????????cancelAcquire(node);

????}

}

代碼塊4:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {

????//外面第一次循環:pred = T2表示的node , node 為 T2,因為T2剛剛將head節點的waitStatus = -1之后自己進入了睡眠,但是T2的waitStatus還是等于0;

? ? //外面第一次循環:ws = 0 ;

? ? //外面第二次循環:因為在第一次的時候,我們已經將T2 node節點中waitStatus改為-1

????int ws = pred.waitStatus;

? ? //外面第一次循環:ws == Node.SIGNAL == -1?ws = 0,所以不會進入到這里面

? ? //外面第二次循環:ws == Node.SIGNAL == -1?ws = -1,所以會進入到這里面

????if (ws == Node.SIGNAL)

????????/*

???????? * This node has already set status asking a release

???????? * to signal it, so it can safely park.

???????? */

?? ?? ? //返回true,帶著這個true,回到代碼塊3中

????????return true;

????if (ws > 0) {

????????/*

???????? * Predecessor was cancelled. Skip over predecessors and

???????? * indicate retry.

???????? */

????????do {

????????????node.prev = pred = pred.prev;

????????} while (pred.waitStatus > 0);

????????pred.next = node;

????} else {

?? ?? ? //外面第一次循環:會來到這里,通過CAS將head節點中的waitStatus 改 -1,然后返回false,帶著false回到代碼塊5中

????????compareAndSetWaitStatus(pred, ws, Node.SIGNAL);

????}

????return false;

}

總結:T3 和 T2的過程差不多,只是T2多了個初始化head節點的操作;

至此,當前AQS中,T1正在實行,T2 ,T3正在隊列中睡眠,這就是當線程競爭鎖不到時排隊的情景;

下面我們來看看另外一個場景:

假設T2在獲取不到鎖的時候,準備加入隊列中的時候,T1執行完了,釋放了鎖,這個時候是怎樣的情況?

代碼塊1:

public final void acquire(int arg) {

//當T1未執行完時,T2線程來了,并且tryAcquire在嘗試獲得鎖失敗,在經過addWaiter封裝成了node對象進入acquireQueued準備排隊,我們來看看此時acquireQueued的情況

????if (!tryAcquire(arg) &&

????????????acquireQueued(addWaiter(Node.EXCLUSIVE), arg))

????????selfInterrupt();

}

代碼塊2:

final boolean acquireQueued(final Node node, int arg) {

????boolean failed = true;

????try {

????????boolean interrupted = false;

????????for (;;) {

?? ??? ?//由于在addWaiter中已經初始化了head節點,當前node排在head之后

?? ??? ?? ? // p = head = node.predecessor()

????????????final Node p = node.predecessor();

?? ??? ?// p == head ? 明顯是等于的,這個時候進入tryAcquire嘗試獲得鎖,在這之前T1執行完了,所以這里T2獲得到了鎖,返回true,進入

????????????if (p == head && tryAcquire(arg)) {

?? ??? ??? ?? ? //將當前節點node設置為head節點,因為他已經獲得鎖了,不用在排隊了,所以把head=null,讓GC回收,當前節點node變為了head

?? ??? ??? ?? ? //setHead當中做了三個事情:1.將head等于當前node,2.將當前節點node的前驅節點=null,3.將當前節點的thread等于null

????????????????setHead(node);

?? ??? ??? ?? ? //因為當前節點node已經變為了head,所以head的next也是等于空

????????????????p.next = null; // help GC

????????????????failed = false;

????????????????return interrupted;

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

????????????if (shouldParkAfterFailedAcquire(p, node) &&

????????????????????parkAndCheckInterrupt())

????????????????interrupted = true;

????????}

????} finally {

????????if (failed)

????????????cancelAcquire(node);

????}

}

以上就是ReentrantLock之公平鎖的加鎖過程,后續我們會分析加鎖過程

總結:

1.公平鎖在調用lock的時候總共嘗試獲得鎖幾次?

答:

1.如果是第一個線程,則一次就獲得成功

2.如果是第二個線程,第一個線程還沒有執行完,那么會經過三次嘗試獲得鎖,在得不到鎖的情況進入休眠

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,030評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,310評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,951評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,796評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,566評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,055評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,142評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,303評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,799評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,683評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,899評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,409評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,135評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,520評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,757評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,528評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,844評論 2 372

推薦閱讀更多精彩內容