10分鐘搞定 Java 并發(fā)隊列好嗎?好的

| 好看請贊怔鳖,養(yǎng)成習(xí)慣

  • 你有一個思想蜡感,我有一個思想情连,我們交換后,一個人就有兩個思想

  • If you can NOT explain it simply, you do NOT understand it well enough

現(xiàn)陸續(xù)將Demo代碼和技術(shù)文章整理在一起 Github實踐精選 ,方便大家閱讀查看,本文同樣收錄在此攘轩,覺得不錯,還請Star


前言

如果按照用途與特性進行粗略的劃分蜻势,JUC 包中包含的工具大體可以分為 6 類:

  1. 執(zhí)行者與線程池
  2. 并發(fā)隊列
  3. 同步工具
  4. 并發(fā)集合
  5. 原子變量

并發(fā)系列中撑刺,主要講解了 執(zhí)行者與線程池鹉胖,同步工具握玛, , 在分析源碼時甫菠,或多或少的提及到了「隊列」挠铲,隊列在 JUC 中也是多種多樣存在,所以本文就以「遠看」視角寂诱,幫助大家快速了解與區(qū)分這些看似「雜亂」的隊列

并發(fā)隊列

Java 并發(fā)隊列按照實現(xiàn)方式來進行劃分可以分為 2 種:

  1. 阻塞隊列
  2. 非阻塞隊列

如果你已經(jīng)看完并發(fā)系列鎖的實現(xiàn)拂苹,你已經(jīng)能夠知道他們實現(xiàn)的區(qū)別:

前者就是基于鎖實現(xiàn)的,后者則是基于 CAS 非阻塞算法實現(xiàn)的

常見的隊列有下面這幾種:

瞬間懵逼痰洒?看到這個沒有人性的圖想直接走人瓢棒? 客觀先別急浴韭,一會就柳暗花明了

當(dāng)下你也許有個問題:

為什么會有這么多種隊列的存在

鎖有應(yīng)對各種情形的鎖脯宿,隊列也自然有應(yīng)對各種情形的隊列了, 是不是也有點單一職責(zé)原則的意思呢念颈?

所以我們要了解這些隊列到底是怎么設(shè)計的?以及用在了哪些地方连霉?

先來看下圖

如果你在 IDE 中打開以上非阻塞隊列和阻塞隊列榴芳,查看其實現(xiàn)方法,你就會發(fā)現(xiàn)跺撼,阻塞隊列非阻塞隊列 額外支持兩種操作

  1. 阻塞的插入

    當(dāng)隊列滿時窟感,隊列會阻塞插入元素的線程,直到隊列不滿

  2. 阻塞的移除

    當(dāng)隊列為空時歉井,獲取元素的線程會阻塞柿祈,直到隊列變?yōu)榉强?/p>

綜合說明入隊/出隊操作,看似雜亂的方法酣难,用一個表格就能概括了

拋出異常

  • 當(dāng)隊列滿時谍夭,此時如果再向隊列中插入元素,會拋出 IllegalStateException (這很好理解)
  • 當(dāng)隊列空時憨募,此時如果再從隊列中獲取元素紧索,會拋出 NoSuchElementException (這也很好理解)

返回特殊值

  • 當(dāng)向隊列插入元素時,會返回元素是否插入成功菜谣,成功則返回 true
  • 當(dāng)從隊列移除元素時珠漂,如果沒有則返回 null

一直阻塞

  • 當(dāng)隊列滿時,如果生產(chǎn)者線程向隊列 put 元素尾膊,隊列會一直阻塞生產(chǎn)者線程媳危,直到隊列可用或者響應(yīng)中斷退出
  • 當(dāng)隊列為空時,如果消費者線程 從隊列里面 take 元素冈敛,隊列會阻塞消費者線程待笑,直到隊列不為空

關(guān)于阻塞,我們其實早在 并發(fā)編程之等待通知機制 就已經(jīng)充分說明過了抓谴,你還記得下面這張圖嗎暮蹂?原理其實是一樣一樣滴

超時退出

和鎖一樣,因為有阻塞癌压,為了靈活使用仰泻,就一定支持超時退出,阻塞時間達到超時時間滩届,就會直接返回

至于為啥插入和移除這么多種單詞表示形式集侯,我也不知道,為了方便記憶,只需要記住阻塞的方法形式即可:

單詞 puttake 字母 t 首位相連棠枉,一個放浓体,一個拿

到這里你應(yīng)該對 Java 并發(fā)隊列有了個初步的認(rèn)識了,原來看似雜亂的方法貌似也有了規(guī)律辈讶。接下來就到了瘋狂串知識點的時刻了汹碱,借助前序章節(jié)的知識,分分鐘就理解全部隊列了

ArrayBlockingQueue

之前也說過荞估,JDK中的命名還是很講究滴咳促,一看這名字,底層就是數(shù)組實現(xiàn)了勘伺,是否有界跪腹,那就看在構(gòu)造的時候是否需要指定 capacity 值了

填鴨式的說明也容易忘,這些都是哪看到的呢飞醉?在所有隊列的 Java docs 的第一段冲茸,一句話就概括了該隊列的主要特性,所以強烈建議大家自己在看源碼時缅帘,簡單瞄一眼 docs 開頭轴术,心中就有多半個數(shù)了

在講 Java AQS隊列同步器以及ReentrantLock的應(yīng)用 時我們介紹了公平鎖與非公平鎖的概念,ArrayBlockingQueue 也有同樣的概念钦无,看它的構(gòu)造方法逗栽,就有 ReentrantLock 來輔助實現(xiàn)

public ArrayBlockingQueue(int capacity, boolean fair) {
    if (capacity <= 0)
        throw new IllegalArgumentException();
    this.items = new Object[capacity];
    lock = new ReentrantLock(fair);
    notEmpty = lock.newCondition();
    notFull =  lock.newCondition();
}

默認(rèn)情況下,依舊是不保證線程公平訪問隊列(公平與否是指阻塞的線程能否按照阻塞的先后順序訪問隊列失暂,先阻塞線訪問彼宠,后阻塞后訪問)

到這我也要臨時問一個說過多次的面試送分題了:

為什么默認(rèn)采用非公平鎖的方式?它較公平鎖方式有什么好處弟塞,又可能帶來哪些問題凭峡?

知道了以上內(nèi)容,結(jié)合上面表格中的方法决记,ArrayBlockingQueue 就可以輕松過關(guān)了

和數(shù)組相對的自然是鏈表了

LinkedBlockingQueue

LinkedBlockingQueue 也算是一個有界阻塞隊列 摧冀,從下面的構(gòu)造函數(shù)中你也可以看出,該隊列的默認(rèn)和最大長度為 Integer.MAX_VALUE 系宫,這也就 docs 說 optionally-bounded 的原因了

public LinkedBlockingQueue() {
    this(Integer.MAX_VALUE);
}

public LinkedBlockingQueue(int capacity) {
  if (capacity <= 0) throw new IllegalArgumentException();
  this.capacity = capacity;
  last = head = new Node<E>(null);
}

正如 Java 集合一樣索昂,鏈表形式的隊列,其存取效率要比數(shù)組形式的隊列高笙瑟。但是在一些并發(fā)程序中楼镐,數(shù)組形式的隊列由于具有一定的可預(yù)測性癞志,因此可以在某些場景中獲得更高的效率

看到 LinkedBlockingQueue 是不是也有些熟悉呢往枷? 為什么要使用線程池? 就已經(jīng)和它多次照面了

創(chuàng)建單個線程池

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

創(chuàng)建固定個數(shù)線程池

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

面試送分題又來了

使用 Executors 創(chuàng)建線程池很簡單,為什么大廠嚴(yán)格要求禁用這種創(chuàng)建方式呢?

PriorityBlockingQueue

PriorityBlockingQueue 是一個支持優(yōu)先級的無界的阻塞隊列错洁,默認(rèn)情況下采用自然順序升序排列秉宿,當(dāng)然也有非默認(rèn)情況自定義優(yōu)先級,需要排序屯碴,那自然要用到 Comparator 來定義排序規(guī)則了

可以定義優(yōu)先級描睦,自然也就有相應(yīng)的限制,以及使用的注意事項

  • 按照上圖說明导而,隊列中不允許存在 null 值忱叭,也不允許存在不能排序的元素

  • 對于排序值相同的元素,其序列是不保證的今艺,但你可以繼續(xù)自定義其他可以區(qū)分出來優(yōu)先級的值韵丑,如果你有嚴(yán)格的優(yōu)先級區(qū)分,建議有更完善的比較規(guī)則虚缎,就像 Java docs 這樣

     class FIFOEntry<E extends Comparable<? super E>>
         implements Comparable<FIFOEntry<E>> {
       static final AtomicLong seq = new AtomicLong(0);
       final long seqNum;
       final E entry;
       public FIFOEntry(E entry) {
         seqNum = seq.getAndIncrement();
         this.entry = entry;
       }
       public E getEntry() { return entry; }
       public int compareTo(FIFOEntry<E> other) {
         int res = entry.compareTo(other.entry);
         if (res == 0 && other.entry != this.entry)
           res = (seqNum < other.seqNum ? -1 : 1);
         return res;
       }
     }
    
  • 隊列容量是沒有上限的撵彻,但是如果插入的元素超過負(fù)載,有可能會引起OutOfMemory異常(這是肯定的)实牡,這也是為什么我們通常所說陌僵,隊列無界,心中有界

  • PriorityBlockingQueue 也有 put 方法创坞,這是一個阻塞的方法碗短,因為它是無界的,自然不會阻塞题涨,所以就有了下面比較聰明的做法

    public void put(E e) {
        offer(e); // never need to block  請自行對照上面表格
    }
    
  • 可以給定初始容量豪椿,這個容量會按照一定的算法自動擴充

    // Default array capacity.
    private static final int DEFAULT_INITIAL_CAPACITY = 11;
    
    public PriorityBlockingQueue() {
        this(DEFAULT_INITIAL_CAPACITY, null);
    }
    

    這里默認(rèn)的容量是 11,由于也是基于數(shù)組携栋,那面試送分題又來了

    你通常是怎樣定義容器/集合初始容量的搭盾?有哪些依據(jù)?

DelayQueue

DelayQueue 是一個支持延時獲取元素的無界阻塞隊列

  • 是否延時肯定是和某個時間(通常和當(dāng)前時間) 進行比較
  • 比較過后還要進行排序婉支,所以也是存在一定的優(yōu)先級

看到這也許覺得這有點和 PriorityBlockingQueue 很像鸯隅,沒錯,DelayQueue 的內(nèi)部也是使用 PriorityQueue

上圖綠色框線也告訴你向挖,DelayQueue 隊列的元素必須要實現(xiàn) Depayed 接口:

所以從上圖可以看出使用 DelayQueue 非常簡單蝌以,只需要兩步:

實現(xiàn) getDelay() 方法,返回元素要延時多長時間

public long getDelay(TimeUnit unit) {
    // 最好采用納秒形式何之,這樣更精確
    return unit.convert(time - now(), NANOSECONDS);
}

實現(xiàn) compareTo() 方法跟畅,比較元素順序

public int compareTo(Delayed other) {
    if (other == this) // compare zero if same object
        return 0;
    if (other instanceof ScheduledFutureTask) {
        ScheduledFutureTask<?> x = (ScheduledFutureTask<?>)other;
        long diff = time - x.time;
        if (diff < 0)
            return -1;
        else if (diff > 0)
            return 1;
        else if (sequenceNumber < x.sequenceNumber)
            return -1;
        else
            return 1;
    }
    long diff = getDelay(NANOSECONDS) - other.getDelay(NANOSECONDS);
    return (diff < 0) ? -1 : (diff > 0) ? 1 : 0;
}

上面的代碼哪來的呢?如果你打開 ScheduledThreadPoolExecutor 里的 ScheduledFutureTask溶推,你就看到了 (ScheduledThreadPoolExecutor 內(nèi)部就是應(yīng)用 DelayQueue)

所以綜合來說徊件,下面兩種情況非常適合使用 DelayQueue

  • 緩存系統(tǒng)的設(shè)計:用 DelayQueue 保存緩存元素的有效期奸攻,使用一個線程循環(huán)查詢 DelayQueue,如果能從 DelayQueue 中獲取元素虱痕,說明緩存有效期到了
  • 定時任務(wù)調(diào)度:用 DelayQueue 保存當(dāng)天會執(zhí)行的任務(wù)以及時間睹耐,如果能從 DelayQueue 中獲取元素,任務(wù)就可以開始執(zhí)行了部翘。比如 TimerQueue 就是這樣實現(xiàn)的

SynchronousQueue

這是一個不存儲元素的阻塞隊列硝训,不存儲元素還叫隊列?

沒錯新思,SynchronousQueue 直譯過來叫同步隊列窖梁,如果在隊列里面呆久了應(yīng)該就算是“異步”了吧

所以使用它,每個put() 操作必須要等待一個 take() 操作夹囚,反之亦然窄绒,否則不能繼續(xù)添加元素

實際中怎么用呢?假如你需要兩個線程之間同步共享變量崔兴,如果不用 SynchronousQueue 你可能會選擇用 CountDownLatch 來完成彰导,就像這樣:

ExecutorService executor = Executors.newFixedThreadPool(2);
AtomicInteger sharedState = new AtomicInteger();
CountDownLatch countDownLatch = new CountDownLatch(1);



Runnable producer = () -> {
    Integer producedElement = ThreadLocalRandom
      .current()
      .nextInt();
    sharedState.set(producedElement);
    countDownLatch.countDown();
};



Runnable consumer = () -> {
    try {
        countDownLatch.await();
        Integer consumedElement = sharedState.get();
    } catch (InterruptedException ex) {
        ex.printStackTrace();
    }
};

這點小事就用計數(shù)器來實現(xiàn),顯然很不合適敲茄,用 SynchronousQueue 改造一下位谋,感覺瞬間就不一樣了

ExecutorService executor = Executors.newFixedThreadPool(2);
SynchronousQueue<Integer> queue = new SynchronousQueue<>();

Runnable producer = () -> {
    Integer producedElement = ThreadLocalRandom
      .current()
      .nextInt();
    try {
        queue.put(producedElement);
    } catch (InterruptedException ex) {
        ex.printStackTrace();
    }
};

Runnable consumer = () -> {
    try {
        Integer consumedElement = queue.take();
    } catch (InterruptedException ex) {
        ex.printStackTrace();
    }
};

其實 Executors.newCachedThreadPool() 方法里面使用的就是 SynchronousQueue

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

看到前面 LinkedBlockingQueue 用在 newSingleThreadExecutornewFixedThreadPool 上,而newCachedThreadPool 卻用 SynchronousQueue堰燎,這是為什么呢掏父?

因為單線程池和固定線程池中,線程數(shù)量是有限的秆剪,因此提交的任務(wù)需要在LinkedBlockingQueue隊列中等待空余的線程赊淑;

而緩存線程池中,線程數(shù)量幾乎無限(上限為Integer.MAX_VALUE)仅讽,因此提交的任務(wù)只需要在SynchronousQueue 隊列中同步移交給空余線程即可, 所以有時也會說 SynchronousQueue 的吞吐量要高于 LinkedBlockingQueueArrayBlockingQueue

LinkedTransferQueue

簡單來說陶缺,TransferQueue提供了一個場所,生產(chǎn)者線程使用 transfer 方法傳入一些對象并阻塞洁灵,直至這些對象被消費者線程全部取出饱岸。

你有沒有覺得,剛剛介紹的 SynchronousQueue 是否很像一個容量為 0 的 TransferQueue徽千。

但 LinkedTransferQueue 相比其他阻塞隊列多了三個方法

  • transfer(E e)

    如果當(dāng)前有消費者正在等待消費元素苫费,transfer 方法就可以直接將生產(chǎn)者傳入的元素立刻 transfer (傳輸) 給消費者;如果沒有消費者等待消費元素双抽,那么 transfer 方法會把元素放到隊列的 tail(尾部)

    節(jié)點百框,一直阻塞,直到該元素被消費者消費才返回

  • tryTransfer(E e)

    tryTransfer牍汹,很顯然是一種嘗試铐维,如果沒有消費者等待消費元素柬泽,則馬上返回 false ,程序不會阻塞

  • tryTransfer(E e, long timeout, TimeUnit unit)

    帶有超時限制方椎,嘗試將生產(chǎn)者傳入的元素 transfer 給消費者,如果超時時間到钧嘶,還沒有消費者消費元素棠众,則返回 false

你瞧,所有阻塞的方法都是一個套路:

  1. 阻塞方式
  2. 帶有 try 的非阻塞方式
  3. 帶有 try 和超時時間的非阻塞方式

看到這你也許感覺 LinkedTransferQueue 沒啥特點有决,其實它和其他阻塞隊列的差別還挺大的:

BlockingQueue 是如果隊列滿了闸拿,線程才會阻塞;但是 TransferQueue 是如果沒有消費元素书幕,則會阻塞 (transfer 方法)

這也就應(yīng)了 Doug Lea 說的那句話:

LinkedTransferQueue is actually a superset of ConcurrentLinkedQueue, SynchronousQueue (in “fair” mode), and unbounded
LinkedBlockingQueues. And it’s made better by allowing you to mix and
match those features as well as take advantage of higher-performance i
mplementation techniques.

簡單翻譯:

LinkedTransferQueueConcurrentLinkedQueue, SynchronousQueue (在公平模式下), 無界的LinkedBlockingQueues等的超集; 允許你混合使用阻塞隊列的多種特性

所以新荤,在合適的場景中,請盡量使用LinkedTransferQueue

上面都看的是單向隊列 FIFO台汇,接下來我們看看雙向隊列

LinkedBlockingDeque

LinkedBlockingDeque 是一個由鏈表結(jié)構(gòu)組成的雙向阻塞隊列苛骨,凡是后綴為 Deque 的都是雙向隊列意思,后綴的發(fā)音為deck——/dek/, 剛接觸它時我以為是這個冰激凌的發(fā)音

所謂雙向隊列值得就是可以從隊列的兩端插入和移除元素苟呐。所以:

雙向隊列因為多了一個操作隊列的入口痒芝,在多線程同時入隊是,也就會減少一半的競爭

隊列有頭牵素,有尾严衬,因此它又比其他阻塞隊列多了幾個特殊的方法

  • addFirst
  • addLast
  • xxxxFirst
  • xxxxLast
  • ... ...

這么一看,雙向阻塞隊列確實很高效笆呆,

那雙向阻塞隊列應(yīng)用在什么地方了呢请琳?

不知道你是否聽過 “工作竊取”模式,看似不太厚道的一種方法赠幕,實則是高效利用線程的好辦法俄精。下一篇文章,我們就來看看 ForkJoinPool 是如何應(yīng)用 “工作竊取”模式的

總結(jié)

到這關(guān)于 Java 隊列(其實主要介紹了阻塞隊列)就快速的區(qū)分完了榕堰,將看似雜亂的方法做了分類整理嘀倒,方便快速理解其用途,同時也說明了這些隊列的實際用途局冰。相信你帶著更高的視角來閱讀源碼會更加輕松测蘑,最后也希望大家認(rèn)真看兩個隊列的源碼實現(xiàn),在遇到隊列的問題康二,腦海中的畫面分分鐘就可以搞定了

參考

  1. Java 并發(fā)編程的藝術(shù)
  2. Java 并發(fā)編程之美
  3. https://zhuanlan.zhihu.com/p/27148381

個人博客:https://dayarch.top

加我微信好友, 進群娛樂學(xué)習(xí)交流碳胳,備注「進群」

歡迎持續(xù)關(guān)注公眾號:「日拱一兵」

  • 前沿 Java 技術(shù)干貨分享
  • 高效工具匯總 | 回復(fù)「工具」
  • 面試問題分析與解答
  • 技術(shù)資料領(lǐng)取 | 回復(fù)「資料」

以讀偵探小說思維輕松趣味學(xué)習(xí) Java 技術(shù)棧相關(guān)知識,本著將復(fù)雜問題簡單化沫勿,抽象問題具體化和圖形化原則逐步分解技術(shù)問題挨约,技術(shù)持續(xù)更新味混,請持續(xù)關(guān)注......


?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市诫惭,隨后出現(xiàn)的幾起案子翁锡,更是在濱河造成了極大的恐慌,老刑警劉巖夕土,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件馆衔,死亡現(xiàn)場離奇詭異,居然都是意外死亡怨绣,警方通過查閱死者的電腦和手機角溃,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來篮撑,“玉大人减细,你說我怎么就攤上這事∮浚” “怎么了未蝌?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長茧妒。 經(jīng)常有香客問我树埠,道長,這世上最難降的妖魔是什么嘶伟? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任怎憋,我火速辦了婚禮,結(jié)果婚禮上九昧,老公的妹妹穿的比我還像新娘绊袋。我一直安慰自己,他們只是感情好铸鹰,可當(dāng)我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布癌别。 她就那樣靜靜地躺著,像睡著了一般蹋笼。 火紅的嫁衣襯著肌膚如雪展姐。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天剖毯,我揣著相機與錄音圾笨,去河邊找鬼。 笑死逊谋,一個胖子當(dāng)著我的面吹牛擂达,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播胶滋,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼板鬓,長吁一口氣:“原來是場噩夢啊……” “哼悲敷!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起俭令,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤后德,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后抄腔,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體瓢湃,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年妓柜,在試婚紗的時候發(fā)現(xiàn)自己被綠了箱季。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片涯穷。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡棍掐,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出拷况,到底是詐尸還是另有隱情作煌,我是刑警寧澤,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布赚瘦,位于F島的核電站粟誓,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏起意。R本人自食惡果不足惜鹰服,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望揽咕。 院中可真熱鬧悲酷,春花似錦、人聲如沸亲善。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蛹头。三九已至顿肺,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間渣蜗,已是汗流浹背屠尊。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留耕拷,地道東北人知染。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像斑胜,于是被迫代替她去往敵國和親控淡。 傳聞我的和親對象是個殘疾皇子嫌吠,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,979評論 2 355