Android线程池学习笔记(二)

ThreadPoolExecutor

线程池的实现类。

构造方法

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)
  • corePoolSize——最大核心线程数。核心线程即使空闲也不会被销毁,除非调用allowCoreThreadTimeOut(true)。
  • maximumPoolSize——线程池最多可运行的线程数。该值不小于corePoolSize。
  • keepAliveTime——非核心线程在空闲状态的存活时间。
  • unit——keepAliveTime的时间单位。
  • workQueue——存放等待执行的任务的队列,只有通过execute(Runnable)提交的任务才可能进入该队列。
  • threadFactory——线程池创建通过该工厂创建线程。
  • handler——在线程池满载的情况下,提交的任务交由handler处理。

以上各参数均有响应的setter方法。

ThreadPoolExecutor提供了四种handler:

  1. CallerRunsPolicy——在线程池未关闭的情况,直接在调用execute(Runnable)方法的线程执行任务。
  2. AbortPolicy——抛出一个RejectedExecutionException。
  3. DiscardPolicy——丢弃
  4. DiscardOldestPolicy——丢弃等待队列中最早提交的那个任务,然后重新提交新的任务。

ThreadPoolExecutor默认使用AbortPolicy。

线程池状态和线程数量的指示字段

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

ctl是一个32位原子整型,高3位表示线程状态(runState),剩余位代码线程数量(workerCount)。所以线程池可容纳的最大线程数是(2^29)-1。
wokerCount未必和存活的线程数一致,比如使用ThreadFactory创建线程失败时,或者线程终结前仍然进行着某些工作时。

线程池状态有以下几种:

  • RUNNING(-1)——可接收新的任务,可执行队列中的任务。
  • SHUTDOWN(0)——不再接收新的任务,仍可执行队列中的任务。
  • STOP(1)——不再接收新的任务,不再执行队列中的任务,中断正在执行的任务。
  • TIDYING(2)——过渡状态。所有任务都已终结,workerCount等于0,terminated()方法执行之前的状态。可以覆写terminated()做一些清理工作。
  • TERMINATED(3)——terminated()执行之后的状态。

状态的更改是递增的,可能的状态改变如下:

  • RUNNING -> SHUTDOWN——调用了shutdown()方法,可能是通过finalize()隐式调用的。
  • (RUNNING or SHUTDOWN) -> STOP——调用了shutdownNow()。
  • SHUTDOWN -> TIDYING——线程池和队列都为空。
  • STOP -> TIDYING——线程池为空。
  • TIDYING -> TERMINATED——terminated()方法执行完毕。

awaitTermination()方法在状态为TERMINATED时返回。

public void execute(Runnable command)

提交任务有几种情况:

  1. 工作线程数 < 核心线程数——创建新的核心线程,并处理该任务。
  2. 工作线程数 >= 核心线程数,队列未满——添加到队列。
  3. 队列满,工作线程数 < 最大线程数——创建非核心线程处理任务。
  4. 交由handler处理。

public boolean prestartCoreThread()

手动启动一个核心线程,这样新来的任务就可以直接运行,从而减少线程启动的时间。如果所有核心线程都已启动,返回false。

public int prestartAllCoreThreads()

手动启动所有核心线程。返回启动的核心线程数。

public void allowCoreThreadTimeOut(boolean value)

设置空闲时,核心线程是否允许超时关闭。存活时间同非核心线程。

public boolean allowsCoreThreadTimeOut()

查询核心线程在空闲时,是否允许超时关闭。

public BlockingQueue<Runnable> getQueue()

获取等待队列。

public boolean remove(Runnable task)

从等待队列中删除task。

public void purge()

将等待队列中所有已经取消的Future任务立即移除。

public int getActiveCount()

返回正在执行任务的线程数。

public int getLargestPoolSize()

返回线程池中出现过的最大的线程数量,不大于最大线程数。

public long getTaskCount()

返回线程池总共执行过的以及正在执行的任务数,该值是个大概值。

public long getCompletedTaskCount()

返回线程池总共执行过的任务数,该值是个大概值。


以上是线程池的基本理解和使用,不想深究的话,到这里也就可以了。

下面是需要注意的点。

关于新建线程

线程池使用ThreadFactory来创建线程,如果没有显示提供ThreadFactory,则使用默认的Executors#DefaultThreadFactory来创建线程:

    private static class DefaultThreadFactory implements ThreadFactory {
        private static final AtomicInteger poolNumber = new AtomicInteger(1);
        private final ThreadGroup group;
        private final AtomicInteger threadNumber = new AtomicInteger(1);
        private final String namePrefix;

        DefaultThreadFactory() {
            SecurityManager s = System.getSecurityManager();
            group = (s != null) ? s.getThreadGroup() :
                                  Thread.currentThread().getThreadGroup();
            namePrefix = "pool-" +
                          poolNumber.getAndIncrement() +
                         "-thread-";
        }

        public Thread newThread(Runnable r) {
            Thread t = new Thread(group, r,
                                  namePrefix + threadNumber.getAndIncrement(),
                                  0);
            if (t.isDaemon())
                t.setDaemon(false);
            if (t.getPriority() != Thread.NORM_PRIORITY)
                t.setPriority(Thread.NORM_PRIORITY);
            return t;
        }
    }

从以上代码可以得到如下信息:

  1. 线程名称均是“pool-XX-thread-XX”形式。
  2. 均是非daemon线程。
  3. 优先级均是Thread.NORM_PRIORITY

关于队列选择

常用的队列选择策略有以下几种:

  1. 直接传递——使用SynchronousQueue同步队列实现。该队列不保存任务,可以理解为一个单纯的管道。其“放入”和“取出”都是阻塞的,每个“放入”操作都要等待一个“取出”操作,反之亦然。直接传递,通常要求最大线程数量不受限制,以避免新提交的任务交由handler处理;但这就会导致线程数量不可控。该策略在任务间有内部依赖时,可避免锁住。
  2. 无限队列——使用一个无容量限制的队列,比如LinkedBlockingQueue(FIFO队列)或者PriorityBlockingQueue(可自定义Comparator)。这样在核心线程都在工作时,新的任务会被添加到队列中,最大线程数不会超过核心线程数,也就是说maximumPoolSize这个参数将不起作用。该策略适合任务间完全独立,相互不影响的情况。所谓无限,并非是真的无限,只是容量非常大而已,可能是Integer.MAX_VALUE,也可能是其他值。
  3. 有限队列——比如ArrayBlockingQueue。可以避免资源的过度消耗,但控制起来比较复杂,需要权衡队列容量和最大线程数的关系:使用大队列和小线程数量,会减少CPU的使用,以及操作系统资源的占用,但会降低吞吐率;使用小队列和大线程数,可以充分利用CPU资源,但可能会加大调度开支,同样降低吞吐率。

关于RejectedExecutionHandler

上面介绍了四种默认的RejectedExecutionHandler,同样也可以自己定义。需要注意的是,RejectedExecutionHandler的选择需要参照线程数量和队列选择策略。比如无限队列的情况,可以随意设置。

关于覆写ThreadPoolExecutor

ThreadPoolExecutor提供了3个protected的hook方法。beforeExecute(Thread, Runnable)afterExecute(Runnable, Throwable)分别在每个任务的执行前/后调用,terminated()方法在线程池终结时调用。

以下是一个覆写的例子,添加了pause|resume方法:

class PausableThreadPoolExecutor extends ThreadPoolExecutor {
    private boolean isPaused;
    private ReentrantLock pauseLock = new ReentrantLock();
    private Condition unpaused = pauseLock.newCondition();

    public PausableThreadPoolExecutor(...) { 
        super(...);
     }

    protected void beforeExecute(Thread t, Runnable r) {
        super.beforeExecute(t, r);
        pauseLock.lock();
        try {
            while (isPaused) 
                unpaused.await();
        } catch (InterruptedException ie) {
            t.interrupt();
        } finally {
            pauseLock.unlock();
        }
    }

    public void pause() {
        pauseLock.lock();
        try {
            isPaused = true;
        } finally {
            pauseLock.unlock();
        }
    }

    public void resume() {
        pauseLock.lock();
        try {
            isPaused = false;
            unpaused.signalAll();
        } finally {
            pauseLock.unlock();
        }
    }
}

下面学习线程池的实现原理。

线程在哪里

线程池的线程由内部类Worker持有。

    private final class Worker
        extends AbstractQueuedSynchronizer
        implements Runnable
    {
        /**
         * This class will never be serialized, but we provide a
         * serialVersionUID to suppress a javac warning.
         */
        private static final long serialVersionUID = 6138294804551838833L;

        /** Thread this worker is running in.  Null if factory fails. */
        final Thread thread;
        /** Initial task to run.  Possibly null. */
        Runnable firstTask;
        /** Per-thread task counter */
        volatile long completedTasks;

        /**
         * Creates with given first task and thread from ThreadFactory.
         * @param firstTask the first task (null if none)
         */
        Worker(Runnable firstTask) {
            setState(-1); // inhibit interrupts until runWorker
            this.firstTask = firstTask;
            this.thread = getThreadFactory().newThread(this);
        }

        /** Delegates main run loop to outer runWorker. */
        public void run() {
            runWorker(this);
        }

        // Lock methods
        //
        // The value 0 represents the unlocked state.
        // The value 1 represents the locked state.

        protected boolean isHeldExclusively() {
            return getState() != 0;
        }

        protected boolean tryAcquire(int unused) {
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        protected boolean tryRelease(int unused) {
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }

        public void lock()        { acquire(1); }
        public boolean tryLock()  { return tryAcquire(1); }
        public void unlock()      { release(1); }
        public boolean isLocked() { return isHeldExclusively(); }

        void interruptIfStarted() {
            Thread t;
            if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
                try {
                    t.interrupt();
                } catch (SecurityException ignore) {
                }
            }
        }
    }

Worker类继承自AbstractQueuedSynchronizer,该父类这里不深究,只需要知道它是一个锁的实现即可。Worker类还实现了Runnable接口。
由构造方法可知,Worker在创建时会通过ThreadFactory.newThread方法创建一个线程,并将自身作为Runnable对象传递给该线程。
protected修饰的方法是覆写的父类方法,暂且不用管。lock、tryLock、unlock、isLocked是自身锁的调用方法。

Worker的run方法中调用了runWorker方法。如下:

    final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();//获取Worker类的thread
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock(); // allow interrupts
        boolean completedAbruptly = true;
        try {
            while (task != null || (task = getTask()) != null) {//getTask为阻塞方法
                w.lock();
                // If pool is stopping, ensure thread is interrupted;
                // if not, ensure thread is not interrupted.  This
                // requires a recheck in second case to deal with
                // shutdownNow race while clearing interrupt
                if ((runStateAtLeast(ctl.get(), STOP) ||//当前线程池状态至少为STOP,不考虑offset,其值为1。则满足的状态为STOP、TIDYING、TERMINATED。
                     (Thread.interrupted() &&
                      runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())
                    wt.interrupt();
                try {
                    beforeExecute(wt, task);//hook方法,可重写
                    Throwable thrown = null;
                    try {
                        task.run();
                    } catch (RuntimeException x) {
                        thrown = x; throw x;
                    } catch (Error x) {
                        thrown = x; throw x;
                    } catch (Throwable x) {
                        thrown = x; throw new Error(x);
                    } finally {
                        afterExecute(task, thrown);//hook方法,可重写
                    }
                } finally {
                    task = null;
                    w.completedTasks++;//已完成的任务数加1
                    w.unlock();
                }
            }
            completedAbruptly = false;
        } finally {
            processWorkerExit(w, completedAbruptly);//Worker退出时的清理操作
        }
    }

由源码可知,该方法不断从队列中取出任务,交由该Worker所在的线程执行,是执行任务的最终场所。
顺腾摸瓜,这里涉及到getTaskprocessWorkerExit两个方法。

先看processWorkerExit

    /**
     * @param completedAbruptly if the worker died due to user exception
     */
    private void processWorkerExit(Worker w, boolean completedAbruptly) {
        if (completedAbruptly) // If abrupt, then workerCount wasn't adjusted
            decrementWorkerCount();//由异常引起的,需要ctl变量中存储的工作线程数量减1。

        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            completedTaskCount += w.completedTasks;
            workers.remove(w);//workers是一个Set,存储了所有活动的Worker。
        } finally {
            mainLock.unlock();
        }

        tryTerminate();//该方法留意一下,下面会介绍

        int c = ctl.get();
        if (runStateLessThan(c, STOP)) {//线程池的状态低于STOP,包括RUNNING、SHUTDOWN
            if (!completedAbruptly) {//非异常引起的死亡,比如空闲状态超过了存活时间。
                int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
                if (min == 0 && ! workQueue.isEmpty())
                    min = 1;
                if (workerCountOf(c) >= min)
                    return; // replacement not needed
            }
            addWorker(null, false);//重新添加一个Worker。
        }
    }

该方法为死掉的Worker做一些清理工作。首先将死亡的Worker已经完成的任务数添加到线程池已完成的任务数中,紧接着从线程池的Worker集合中删除该Worker,然后调用tryTerminate,最后根据线程池的状态等,判断是否需要重新添加Worker到Worker集合中。

getTask方法从队列中取出一个任务并返回。在Worker由于一些原因退出时,返回null。这些原因包括:

  1. 线程数量超过maximumPoolSize(比如通过setMaximumPoolSize修改了最大线程数量)。
  2. 线程池被stop。
  3. 线程池被关闭,并且队列为空。
  4. 等待取出任务超时,而超时的Worker需要终结。
    private Runnable getTask() {
        boolean timedOut = false; // Did the last poll() time out?

        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);//线程池状态

            // Check if queue empty only if necessary.
            if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {//对应情况2、3
            //该条件等价于if (rs >= STOP || (rs >= SHUTDOWN && workQueue.isEmpty()))
                decrementWorkerCount();
                return null;
            }

            int wc = workerCountOf(c);//当前线程数量

            // Are workers subject to culling?
            boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;//当前Worker是否允许超时关闭

            //对应情况1、4
            if ((wc > maximumPoolSize || (timed && timedOut))//线程数量大于maximumPoolSize或者达到超时条件
                && (wc > 1 || workQueue.isEmpty())) {//并且此时队列为空,或者线程数大于1(在队列非空时,至少需要保留一个Worker来执行任务)
                if (compareAndDecrementWorkerCount(c))
                    return null;
                continue;
            }

            try {
                Runnable r = timed ?
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) ://允许超时关闭则调用poll超时阻塞方法
                    workQueue.take();//否则调用take,一直阻塞下去,直到新任务到来
                if (r != null)
                    return r;
                timedOut = true;
            } catch (InterruptedException retry) {
                timedOut = false;
            }
        }
    }

在processWorkerExit方法中,我们接触到了addWorker方法。
该方法根据线程池的状态的最大线程数量,决定是否向线程池添加新的Worker。遇到以下条件之一时,添加失败,返回false:

  1. 线程池已经stop或者达到shutdown的条件。
  2. 当前线程数达到上限。
  3. ThreadFactory创建线程失败。

Worker创建失败时,会回滚一些数据。

    /**
     * @param firstTask 新Worker需要执行的第一个任务,可为null
     * @param core 是否是核心线程
     */
    private boolean addWorker(Runnable firstTask, boolean core) {
        retry://添加一个标签
        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);

            // Check if queue empty only if necessary.
            if (rs >= SHUTDOWN &&
                ! (rs == SHUTDOWN && firstTask == null && ! workQueue.isEmpty()))//对应情况1
            //等价于if (rs >= SHUTDOWN && (rs != SHUTDOWN || firstTask != null || workQueue.isEmpty()))
            //之所以有firstTask != null是因为SHUTDOWN后不再接收新任务。
                return false;

            for (;;) {
                int wc = workerCountOf(c);
                if (wc >= CAPACITY ||//CAPACITY为线程池所能容纳的最大线程数量2^29-1
                    wc >= (core ? corePoolSize : maximumPoolSize))//对应情况2
                    return false;
                if (compareAndIncrementWorkerCount(c))//ctl字段中的Worker数量加1,此时Worker还没有被实际创建,
                //也证实了讲ctl字段时提到的“wokerCount未必和存活的线程数一致”
                    break retry;
                c = ctl.get();  // Re-read ctl
                if (runStateOf(c) != rs)//线程池状态发生变化时,重新执行前面的逻辑
                    continue retry;
                // else CAS failed due to workerCount change; retry inner loop
            }
        }

        boolean workerStarted = false;
        boolean workerAdded = false;
        Worker w = null;
        try {
            w = new Worker(firstTask);
            final Thread t = w.thread;
            if (t != null) {//线程创建成功
                final ReentrantLock mainLock = this.mainLock;
                mainLock.lock();
                try {
                    // Recheck while holding lock.
                    // Back out on ThreadFactory failure or if
                    // shut down before lock acquired.
                    int rs = runStateOf(ctl.get());

                    if (rs < SHUTDOWN ||
                        (rs == SHUTDOWN && firstTask == null)) {//SHUTDOWN后不再接收新任务
                        if (t.isAlive()) // precheck that t is startable
                            //t.isAlive()返回true,则表明t.start已被调用过,而正常来说,此时还未调用t.start
                            throw new IllegalThreadStateException();
                        workers.add(w);
                        int s = workers.size();
                        if (s > largestPoolSize)//largestPoolSize是线程池中出现过的最大线程的数量
                            largestPoolSize = s;
                        workerAdded = true;
                    }
                } finally {
                    mainLock.unlock();
                }
                if (workerAdded) {
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
            if (! workerStarted)
                addWorkerFailed(w);//执行数据回滚
        }
        return workerStarted;
    }

接下来看addWorkerFailed方法。

    private void addWorkerFailed(Worker w) {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            if (w != null)
                workers.remove(w);
            decrementWorkerCount();//讲ctl字段的workerCount减1
            tryTerminate();//查看是否需要终结线程池
        } finally {
            mainLock.unlock();
        }
    }

processWorkerExit和addWorkerFailed都涉及到了tryTerminate方法。
仅tryTerminate方法会将线程池状态置为TIDYING和TERMINATED,且需要满足以下条件之一:

  1. 当前状态为SHUTDOWN,并且线程池和队列都为空。
  2. 当前状态为STOP,并且队列为空。

在执行了可能导致线程池TERMINATED的操作后,必须调用该方法,比如减少了worker的数量,或者shutdown之后从队列中移除了任务。

    final void tryTerminate() {
        for (;;) {
            int c = ctl.get();
            if (isRunning(c) ||//线程池正在运行中
                runStateAtLeast(c, TIDYING) ||//线程池正在terminate
                (runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty()))//或者不满足条件1
                return;
            if (workerCountOf(c) != 0) { // Eligible to terminate
                interruptIdleWorkers(ONLY_ONE);//workerCount不为0时,中断一个空闲的worker,将中断信号传递下去(如何传递请往下看)
                return;
            }

            final ReentrantLock mainLock = this.mainLock;
            mainLock.lock();
            try {
                if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
                    try {
                        terminated();//hook方法,可重写
                    } finally {
                        ctl.set(ctlOf(TERMINATED, 0));//此处可以看出TIDYING只是一个过渡态。
                        termination.signalAll();//用于唤醒awaitTermination阻塞方法
                    }
                    return;
                }
            } finally {
                mainLock.unlock();
            }
            // else retry on failed CAS
        }
    }

至此我们就见到了可重写的3个hook方法:beforeExecuteafterExecuteterminate

termination.signalAll()这一语句,简单看一下awaitTermination

    public boolean awaitTermination(long timeout, TimeUnit unit)
        throws InterruptedException {
        long nanos = unit.toNanos(timeout);
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            while (!runStateAtLeast(ctl.get(), TERMINATED)) {
                if (nanos <= 0L)
                    return false;
                nanos = termination.awaitNanos(nanos);//状态不是TERMINATED,则阻塞下去,等待termination.signalAll()唤醒
            }
            return true;
        } finally {
            mainLock.unlock();
        }
    }

tryTerminate方法中调用了interruptIdleWorkers,仅此处调用传递的onlyOne参数为true:

    private void interruptIdleWorkers(boolean onlyOne) {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            for (Worker w : workers) {
                Thread t = w.thread;
                if (!t.isInterrupted() && w.tryLock()) {
                    try {
                        t.interrupt();
                    } catch (SecurityException ignore) {
                    } finally {
                        w.unlock();
                    }
                }
                if (onlyOne)
                    break;
            }
        } finally {
            mainLock.unlock();
        }
    }

前面说了,调用interruptIdleWorkers方法是为了将终结信号传递下去,那究竟是如何传递的呢?

  1. interruptIdleWorkers会中断一个Worker。
  2. Worker中断,则runWorker方法就会调用finally块中的processWorkerExit方法,参数completedAbruptly为true。
  3. processWorkerExit方法中会再次调用tryTerminate方法,从而完成终结信号的传递。

至此我们就学习了线程池的实现原理。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350