01.Android線程池實踐基礎

目錄介紹

  • 01.實際開發問題
  • 02.線程池的優勢
  • 03.ThreadPoolExecutor參數
  • 04.ThreadPoolExecutor使用
  • 05.線程池執行流程
  • 06.四種線程池類
  • 07.execute和submit區別
  • 08.線程池的使用技巧

01.實際開發問題

  • 在我們的開發中經常會使用到多線程。例如在Android中,由于主線程的諸多限制,像網絡請求等一些耗時的操作我們必須在子線程中運行。
  • 我們往往會通過new Thread來開啟一個子線程,待子線程操作完成以后通過Handler切換到主線程中運行。這么以來我們無法管理我們所創建的子線程,并且無限制的創建子線程,它們相互之間競爭,很有可能由于占用過多資源而導致死機或者OOM。所以在Java中為我們提供了線程池來管理我們所創建的線程。

02.線程池的優勢

  • ①降低系統資源消耗,通過重用已存在的線程,降低線程創建和銷毀造成的消耗;
  • ②提高系統響應速度,當有任務到達時,無需等待新線程的創建便能立即執行;
  • ③方便線程并發數的管控,線程若是無限制的創建,不僅會額外消耗大量系統資源,更是占用過多資源而阻塞系統或oom等狀況,從而降低系統的穩定性。線程池能有效管控線程,統一分配、調優,提供資源使用率;
  • ④更強大的功能,線程池提供了定時、定期以及可控線程數等功能的線程池,使用方便簡單。

03.ThreadPoolExecutor

  • 可以通過ThreadPoolExecutor來創建一個線程池。
    ExecutorService service = new ThreadPoolExecutor(....);
    
  • 下面我們就來看一下ThreadPoolExecutor中的一個構造方法。
     public ThreadPoolExecutor(int corePoolSize,
        int maximumPoolSize,
        long keepAliveTime,
        TimeUnit unit,
        BlockingQueue<Runnable> workQueue,
        ThreadFactory threadFactory,
        RejectedExecutionHandler handler) 
    
  • ThreadPoolExecutor參數含義
  • 1.corePoolSize
    • 線程池中的核心線程數,默認情況下,核心線程一直存活在線程池中,即便他們在線程池中處于閑置狀態。除非我們將ThreadPoolExecutor的allowCoreThreadTimeOut屬性設為true的時候,這時候處于閑置的核心線程在等待新任務到來時會有超時策略,這個超時時間由keepAliveTime來指定。一旦超過所設置的超時時間,閑置的核心線程就會被終止。
  • 2.maximumPoolSize
    • 線程池中所容納的最大線程數,如果活動的線程達到這個數值以后,后續的新任務將會被阻塞。包含核心線程數+非核心線程數。
  • 3.keepAliveTime
    • 非核心線程閑置時的超時時長,對于非核心線程,閑置時間超過這個時間,非核心線程就會被回收。只有對ThreadPoolExecutor的allowCoreThreadTimeOut屬性設為true的時候,這個超時時間才會對核心線程產生效果。
  • 4.unit
    • 用于指定keepAliveTime參數的時間單位。他是一個枚舉,可以使用的單位有天(TimeUnit.DAYS),小時(TimeUnit.HOURS),分鐘(TimeUnit.MINUTES),毫秒(TimeUnit.MILLISECONDS),微秒(TimeUnit.MICROSECONDS, 千分之一毫秒)和毫微秒(TimeUnit.NANOSECONDS, 千分之一微秒);
  • 5.workQueue
    • 線程池中保存等待執行的任務的阻塞隊列。通過線程池中的execute方法提交的Runable對象都會存儲在該隊列中。我們可以選擇下面幾個阻塞隊列。我們還能夠通過實現BlockingQueue接口來自定義我們所需要的阻塞隊列。
      阻塞隊列 說明
      ArrayBlockingQueue 基于數組實現的有界的阻塞隊列,該隊列按照FIFO(先進先出)原則對隊列中的元素進行排序。
      LinkedBlockingQueue 基于鏈表實現的阻塞隊列,該隊列按照FIFO(先進先出)原則對隊列中的元素進行排序。
      SynchronousQueue 內部沒有任何容量的阻塞隊列。在它內部沒有任何的緩存空間。對于SynchronousQueue中的數據元素只有當我們試著取走的時候才可能存在。
      PriorityBlockingQueue 具有優先級的無限阻塞隊列。
  • 6.threadFactory
    • 線程工廠,為線程池提供新線程的創建。ThreadFactory是一個接口,里面只有一個newThread方法。 默認為DefaultThreadFactory類。
  • 7.handler
    • 是RejectedExecutionHandler對象,而RejectedExecutionHandler是一個接口,里面只有一個rejectedExecution方法。當任務隊列已滿并且線程池中的活動線程已經達到所限定的最大值或者是無法成功執行任務,這時候ThreadPoolExecutor會調用RejectedExecutionHandler中的rejectedExecution方法。在ThreadPoolExecutor中有四個內部類實現了RejectedExecutionHandler接口。在線程池中它默認是AbortPolicy,在無法處理新任務時拋出RejectedExecutionException異常
    • 下面是在ThreadPoolExecutor中提供的四個可選值。
    • 我們也可以通過實現RejectedExecutionHandler接口來自定義我們自己的handler。如記錄日志或持久化不能處理的任務。
      可選值 說明
      CallerRunsPolicy 只用調用者所在線程來運行任務。
      AbortPolicy 直接拋出RejectedExecutionException異常。
      DiscardPolicy 丟棄掉該任務,不進行處理。
      DiscardOldestPolicy 丟棄隊列里最近的一個任務,并執行當前任務。
  • 如下圖所示
    • image

04.ThreadPoolExecutor使用

  • 如下所示
    ExecutorService service = new ThreadPoolExecutor(5, 10, 10, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
    
  • 對于ThreadPoolExecutor有多個構造方法,對于上面的構造方法中的其他參數都采用默認值。可以通過execute和submit兩種方式來向線程池提交一個任務。
  • execute
    • 當我們使用execute來提交任務時,由于execute方法沒有返回值,所以說我們也就無法判定任務是否被線程池執行成功。
    service.execute(new Runnable() {
        public void run() {
            System.out.println("execute方式");
        }
    });
    
  • submit
    • 當我們使用submit來提交任務時,它會返回一個future,我們就可以通過這個future來判斷任務是否執行成功,還可以通過future的get方法來獲取返回值。如果子線程任務沒有完成,get方法會阻塞住直到任務完成,而使用get(long timeout, TimeUnit unit)方法則會阻塞一段時間后立即返回,這時候有可能任務并沒有執行完。
    Future<Integer> future = service.submit(new Callable<Integer>() {
    
        @Override
        public Integer call() throws Exception {
            System.out.println("submit方式");
            return 2;
        }
    });
    try {
        Integer number = future.get();
    } catch (ExecutionException e) {
        e.printStackTrace();
    }
    
  • 線程池關閉
    • 調用線程池的shutdown()shutdownNow()方法來關閉線程池
    • shutdown原理:將線程池狀態設置成SHUTDOWN狀態,然后中斷所有沒有正在執行任務的線程。
    • shutdownNow原理:將線程池的狀態設置成STOP狀態,然后中斷所有任務(包括正在執行的)的線程,并返回等待執行任務的列表。
    • 中斷采用interrupt方法,所以無法響應中斷的任務可能永遠無法終止。 但調用上述的兩個關閉之一,isShutdown()方法返回值為true,當所有任務都已關閉,表示線程池關閉完成,則isTerminated()方法返回值為true。當需要立刻中斷所有的線程,不一定需要執行完任務,可直接調用shutdownNow()方法。

05.線程池執行流程

  • 大概的流程圖如下
    • image
  • 文字描述如下
    • ①如果在線程池中的線程數量沒有達到核心的線程數量,這時候就回啟動一個核心線程來執行任務。
    • ②如果線程池中的線程數量已經超過核心線程數,這時候任務就會被插入到任務隊列中排隊等待執行。
    • ③由于任務隊列已滿,無法將任務插入到任務隊列中。這個時候如果線程池中的線程數量沒有達到線程池所設定的最大值,那么這時候就會立即啟動一個非核心線程來執行任務。
    • ④如果線程池中的數量達到了所規定的最大值,那么就會拒絕執行此任務,這時候就會調用RejectedExecutionHandler中的rejectedExecution方法來通知調用者。

06.四種線程池類

  • Java中四種具有不同功能常見的線程池。
    • 他們都是直接或者間接配置ThreadPoolExecutor來實現他們各自的功能。這四種線程池分別是newFixedThreadPool,newCachedThreadPool,newScheduledThreadPool和newSingleThreadExecutor。這四個線程池可以通過Executors類獲取。

6.1 newFixedThreadPool

  • 通過Executors中的newFixedThreadPool方法來創建,該線程池是一種線程數量固定的線程池。
    ExecutorService service = Executors.newFixedThreadPool(4);
    
  • 在這個線程池中 所容納最大的線程數就是我們設置的核心線程數。
    • 如果線程池的線程處于空閑狀態的話,它們并不會被回收,除非是這個線程池被關閉。如果所有的線程都處于活動狀態的話,新任務就會處于等待狀態,直到有線程空閑出來。
    • 由于newFixedThreadPool只有核心線程,并且這些線程都不會被回收,也就是它能夠更快速的響應外界請求
  • 從下面的newFixedThreadPool方法的實現可以看出,newFixedThreadPool只有核心線程,并且不存在超時機制,采用LinkedBlockingQueue,所以對于任務隊列的大小也是沒有限制的。
    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
            0L, TimeUnit.MILLISECONDS,
            new LinkedBlockingQueue<Runnable>());
    }
    

6.2 newCachedThreadPool

  • 通過Executors中的newCachedThreadPool方法來創建。
    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
            60L, TimeUnit.SECONDS,
            new SynchronousQueue<Runnable>());
    }
    
  • 通過s上面的newCachedThreadPool方法在這里我們可以看出它的 核心線程數為0, 線程池的最大線程數Integer.MAX_VALUE。而Integer.MAX_VALUE是一個很大的數,也差不多可以說 這個線程池中的最大線程數可以任意大。
    • 當線程池中的線程都處于活動狀態的時候,線程池就會創建一個新的線程來處理任務。該線程池中的線程超時時長為60秒,所以當線程處于閑置狀態超過60秒的時候便會被回收。
    • 這也就意味著若是整個線程池的線程都處于閑置狀態超過60秒以后,在newCachedThreadPool線程池中是不存在任何線程的,所以這時候它幾乎不占用任何的系統資源。
    • 對于newCachedThreadPool他的任務隊列采用的是SynchronousQueue,上面說到在SynchronousQueue內部沒有任何容量的阻塞隊列。SynchronousQueue內部相當于一個空集合,我們無法將一個任務插入到SynchronousQueue中。所以說在線程池中如果現有線程無法接收任務,將會創建新的線程來執行任務。

6.3 newScheduledThreadPool

  • 通過Executors中的newScheduledThreadPool方法來創建。
    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }
    public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }
    
  • 它的核心線程數是固定的,對于非核心線程幾乎可以說是沒有限制的,并且當非核心線程處于限制狀態的時候就會立即被回收。
    • 創建一個可定時執行或周期執行任務的線程池:
    ScheduledExecutorService service = Executors.newScheduledThreadPool(4);
    service.schedule(new Runnable() {
        public void run() {
            System.out.println(Thread.currentThread().getName()+"延遲三秒執行");
        }
    }, 3, TimeUnit.SECONDS);
    service.scheduleAtFixedRate(new Runnable() {
        public void run() {
            System.out.println(Thread.currentThread().getName()+"延遲三秒后每隔2秒執行");
        }
    }, 3, 2, TimeUnit.SECONDS);
    
    • 輸出結果:

      pool-1-thread-2延遲三秒后每隔2秒執行

      pool-1-thread-1延遲三秒執行

      pool-1-thread-1延遲三秒后每隔2秒執行

      pool-1-thread-2延遲三秒后每隔2秒執行

      pool-1-thread-2延遲三秒后每隔2秒執行

  • 部分方法說明
    • schedule(Runnable command, long delay, TimeUnit unit):延遲一定時間后執行Runnable任務;
    • schedule(Callable callable, long delay, TimeUnit unit):延遲一定時間后執行Callable任務;
    • scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit):延遲一定時間后,以間隔period時間的頻率周期性地執行任務;
    • scheduleWithFixedDelay(Runnable command, long initialDelay, long delay,TimeUnit unit):與scheduleAtFixedRate()方法很類似,但是不同的是scheduleWithFixedDelay()方法的周期時間間隔是以上一個任務執行結束到下一個任務開始執行的間隔,而scheduleAtFixedRate()方法的周期時間間隔是以上一個任務開始執行到下一個任務開始執行的間隔,也就是這一些任務系列的觸發時間都是可預知的。
  • ScheduledExecutorService功能強大,對于定時執行的任務,建議多采用該方法。

6.4 newSingleThreadExecutor

  • 通過Executors中的newSingleThreadExecutor方法來創建,在這個線程池中只有一個核心線程,對于任務隊列沒有大小限制,也就意味著這一個任務處于活動狀態時,其他任務都會在任務隊列中排隊等候依次執行
  • newSingleThreadExecutor將所有的外界任務統一到一個線程中支持,所以在這個任務執行之間我們不需要處理線程同步的問題。
    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
            0L, TimeUnit.MILLISECONDS,
            new LinkedBlockingQueue<Runnable>()));
    }
    

07.execute和submit區別

  • 先思考一個問題
    • 為了保證項目中線程數量不會亂飆升,不好管理,我們會使用線程池,保證線程在我們的管理之下。
    • 我們也經常說:使用線程池復用線程。那么問題是:線程池中的線程是如何復用的?是執行完成后銷毀,再新建幾個放那;還是始終是那幾個線程(針對 coreSize 線程)。
  • execute和submit
    • 調用線程池的execute方法(ExecutorService的submit方法最終也是調用execute)傳進去的Runnable,并不會直接以new Thread(runnable).start()的方式來執行,而是通過一個正在運行的線程來調用我們傳進去的Runnable的run方法的。
    • 那么,這個正在運行的線程,在執行完傳進去的Runnable的run方法后會銷毀嗎?看情況。
    • 大部分場景下,我們都是通過Executors的newXXX方法來創建線程池的,就拿newCachedThreadPool來說:
      public static ExecutorService newCachedThreadPool() {
          return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
      }
      
      • 看第三個參數(keepAliveTime):60L,后面的單位是秒,也就是說,newCachedThreadPool方法返回的線程池,它的工作線程(也就是用來調用Runnable的run方法的線程)的空閑等待時長為60秒,如果超過了60秒沒有獲取到新的任務,那么這個工作線程就會結束。如果在60秒內接到了新的任務,那么它會在新任務結束后重新等待。
    • 還有另一種常用的線程池,通過newFixedThreadPool方法創建的:
      public static ExecutorService newFixedThreadPool(int nThreads) {
          return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
      }
      
      • 它跟上面的newCachedThreadPool方法一樣,創建的都是ThreadPoolExecutor的對象,只是參數不同而已。
        可以看到第三個參數設置成了0,這就說明,如果當前工作線程數 > corePoolSize時,并且有工作線程在執行完上一個任務后沒拿到新的任務,那么這個工作線程就會立即結束。
        再看第二個參數(maximumPoolSize),它設置成了跟corePoolSize一樣大,也就是說當前工作線程數 永遠不會大于 corePoolSize了,這樣的話,即使有工作線程是空閑的,也不會主動結束,會一直等待下一個任務的到來。
  • ThreadPoolExecutor分析
    • 來探究一下ThreadPoolExecutor是如何管理線程的,先來看精簡后的execute方法:
    • 邏輯很清晰:當execute方法被調用時,如果當前工作線程 < corePoolSize(上面ThreadPoolExecutor構造方法的第一個參數)的話,就會創建新的線程,否則加入隊列。加入隊列后如果沒有工作線程在運行,也會創建一個。
    private final BlockingQueue<Runnable> workQueue;
    
    public void execute(Runnable command) {
        int c = ctl.get();
        //當前工作線程還沒滿
        if (workerCountOf(c) < corePoolSize) {
            //可以創建新的工作線程來執行這個任務
            if (addWorker(command, true)){
                //添加成功直接返回
                return;
            }
        }
    
        //如果工作線程滿了的話,會加入到阻塞隊列中
        if (workQueue.offer(command)) {
            int recheck = ctl.get();
            //加入到隊列之后,如果當前沒有工作線程,那么就會創建一個工作線程
            if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
    }
    
    • 接著看它是怎么創建新線程的:
      • 主要操作是再次檢查,然后創建Worker對象,并且把worker對象店家到HashSet集合中,最后啟動工作線程。
    private final HashSet<Worker> workers = new HashSet<>();
    
    private boolean addWorker(Runnable firstTask, boolean core) {
        //再次檢查
        int wc = workerCountOf(c);
        if (wc >= CAPACITY || wc >= corePoolSize)
            return false;
    
        boolean workerStarted = false;
        Worker w = null;
        //創建Worker對象
        w = new Worker(firstTask);
        //添加到集合中
        workers.add(w);
        final Thread t = w.thread;
        //啟動工作線程
        t.start();
        workerStarted = true;
    
        return workerStarted;
    }
    
    • 看看Worker里面是怎么樣的:
      • 可以看到,這個Worker也是一個Runnable。構造方法里面還創建了一個Thread,這個Thread對象,對應了上面addWorker方法啟動的那個thread。
    private final class Worker extends AbstractQueuedSynchronizer implements Runnable {
        final Thread thread;
        Runnable firstTask;
    
        Worker(Runnable firstTask) {
            this.firstTask = firstTask;
            this.thread = getThreadFactory().newThread(this);
        }
    
        public void run() {
            runWorker(this);
        }
    }
    
    • 再看Worker類中的run方法,它調用了runWorker,并把自己傳了進去:
      • Worker里面的firstTask,就是我們通過execute方法傳進去的Runnable,可以看到它會在這個方法里面被執行
      • 執行完成之后,接著就會通過getTask方法嘗試從等待隊列中(上面的workQueue)獲取下一個任務,如果getTask方法返回null的話,那么這個工作線程就會結束
    final void runWorker(Worker w) {
        Runnable task = w.firstTask;
        w.firstTask = null;
    
        while (task != null || (task = getTask()) != null) {
            try {
                task.run();
            } finally {
                task = null;
                w.completedTasks++;
            }
        }
    }
    
    • 最后看看runWorker方法中的getTask方法
    private Runnable getTask() {
        boolean timedOut = false; // Did the last poll() time out?
    
        for (; ; ) {
            int c = ctl.get();
            int wc = workerCountOf(c);
    
            //如果當前工作線程數大于指定的corePoolSize的話,就要視情況結束工作線程
            boolean timed = wc > corePoolSize;
    
            //(當前工作線程數 > 指定的最大線程數 || (工作線程數 > 指定的核心線程數 && 上一次被標記超時了)) && (當前工作線程數有2個以上 || 等待隊列現在是空的)
            if ((wc > maximumPoolSize || (timed && timedOut)) && (wc > 1 || workQueue.isEmpty())) {
                return null;
            }
            //如果當前工作線程數大于指定的corePoolSize,就看能不能在keepAliveTime時間內獲取到新任務
            //如果線程數沒有 >  corePoolSize的話,就會一直等待
            Runnable r = timed ? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take();
            if (r != null)
                return r;
            //沒能在keepAliveTime時間內獲取到新任務,標記已超時
            timedOut = true;
        }
    }
    

08.線程池的使用技巧

  • 需要針對具體情況而具體處理,不同的任務類別應采用不同規模的線程池,任務類別可劃分為CPU密集型任務、IO密集型任務和混合型任務。(N代表CPU個數)
    任務類別 說明
    CPU密集型任務 線程池中線程個數應盡量少,如配置N+1個線程的線程池。
    IO密集型任務 由于IO操作速度遠低于CPU速度,那么在運行這類任務時,CPU絕大多數時間處于空閑狀態,那么線程池可以配置盡量多些的線程,以提高CPU利用率,如2*N。
    混合型任務 可以拆分為CPU密集型任務和IO密集型任務,當這兩類任務執行時間相差無幾時,通過拆分再執行的吞吐率高于串行執行的吞吐率,但若這兩類任務執行時間有數據級的差距,那么沒有拆分的意義。

Android線程池實踐庫:https://github.com/yangchong211/YCThreadPool

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