线程池之工作原理

流泪撒种的,必欢呼收割。---诗 126:5

最近完成一个功能,当手机板温到达一定值时,触发系统弹出一个警示框,提示用户手机温度过高,系统自动采取降温措施,可能会引起系统流畅性降低,屏幕变暗。由于底层没有提供板温自动上报功能,因此我采取的是从上层主动间隔一段时间去查询板温节点,进而判断是否需要弹框。这里用到了线程池。

本篇以此为契机,总结下线程池的工作原理。

ThreadPoolExecutor是什么

ThreadPoolExecutor是线程池的根基,先上一张围绕ThreadPoolExecutor的类图,主要部分在红色框中。


这里写图片描述

线程池的核心类是ThreadPoolExecutor,我们平时用的最多的是通过工厂类Executors创建出需要的ThreadPoolExecutor。比如

Executors.newCachedThreadPool
Executors.newFixedThreadPool(int n)

Notes:
抽象类不一定都有抽象方法,抽象类不能被实例化,即使是一个没有抽象方法的抽象类,也同样不能被实例化,见AbstractExecutorService类

我们去Executors看对应的方法,会发现其实都是在创建ThreadPoolExecutor对象,只是给ThreadPoolExecutor传入的构造参数不同罢了。

//创建可复用thread的线程池,适用大量短周期的异步任务
public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}
--------------------------------------------------------------------------
//创建至多包含nThreads个可复用thread的线程池
public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

是时候来看看ThreadPoolExecutor的参数了。

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)
参数 意义
corePoolSize 线程池中保留的核心线程数,除非设置了allowCoreThreadTimeOut,否则即使核心线程空闲,其依然不会死亡,当新任务需要执行时,即使核心线程有空闲的,只要数量小于corePoolSize,依然会创建新的线程,直到核心线程数等于corePoolSize。
maximumPoolSize 线程池中所能容纳的最大线程数
keepAliveTime 当池中线程数大于corePoolSize时,其它非核心线程,当没有工作时,最长存活keepAliveTime的时长
unit keepAliveTime的时间单位
workQueue 存放待执行任务的阻塞队列,BlockingQueue是一个接口,可以根据需要选取实现BlockingQueue的阻塞队列,比如SynchronousQueue/LinkedBlockingQueue等
threadFactory 线程池通过threadFactory创建线程
handler 拒绝执行策略,当线程池跟workQueue都饱和的情况下,又有新的任务到来,就会触发拒绝策略。RejectedExecutionHandler是一个接口,ThreadPoolExecutor内部有现成实现。

线程池的好处

  1. 降低资源消耗。java中所有的池化技术都有一个好处,就是通过复用池中的对象,降低系统资源消耗。设想以下如果我们有n多个子任务需要执行,如果我们为每一个待执行对象都创建一个执行线程,而创建线程的过程是需要一定的系统消耗的,最后肯定会拖慢整个系统的处理速度。而通过线程池我们可以做到复用线程,任务有多个,但执行任务的线程可以通过线程池来复用,这样减少了创建线程的开销,系统资源利用率得到了提升。
  2. 降低管理线程的难度。多线程环境下对线程的管理是最容易出现问题的,而线程池通过框架为我们降低了管理线程的难度。我们不用再去担心何时该销毁线程,如何最大限度的避免多线程的资源竞争。这些事情线程池都帮我们代劳了。
  3. 提升任务处理速度。线程池中长期驻留了一定数量的活线程,当任务需要执行时,我们不必先去创建线程,线程池会自己选择利用现有的活线程来处理任务。

线程池何时退出?核心线程常驻,如果不退出岂不是一直有空线程在占资源

如果我们没有手动关闭线程池,线程池一直就会存在,池中的核心线程就会常驻其中,即使没有任何任务需要执行,核心线程也不会销毁。如此肯定会占有一定的系统资源,但是这种情况大多数是值得的,维护一个空转的线程一直等待新任务的到来,比新任务到来后在创建线程更划算。并且线程池也提供了关闭线程池的方法,必要情况下我们可以手动关闭。
来看看下面这段代码

package com.azhengye.testthreadpool;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;

public class MainActivity extends Activity {
    private ExecutorService executor;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 2个核心线程,最多创建8个线程;非核心线程没有任务可执行时,5s后自动销毁;任务队列最多容纳2个任务;任务饱和情况下直接丢弃新任务
        executor = new ThreadPoolExecutor(2, 8, 5, TimeUnit.SECONDS,
                new LinkedBlockingQueue<Runnable>(2), new ThreadPoolExecutor.DiscardPolicy());

        // 执行15个任务
        for (int i = 1; i <= 15; i++) {
            executor.execute(new TestRunable(i * 100));
        }

        // 60s后关闭线程池
        /*try {
            TimeUnit.SECONDS.sleep(60);
            executor.shutdown();
            Log.d("azhengye_test", "ThreadPoolExecutor exit==" + executor.isShutdown());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }*/
    }

    class TestRunable implements Runnable {
        private int flag;

        public TestRunable(int flag) {
            this.flag = flag;
        }

        @Override
        public void run() {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Log.d("azhengye_test", "thread id==" + Thread.currentThread().getId() + " flag=="
                    + flag);
        }
    }
}

上述代码注释掉了关闭线程池的方法,那么我们待执行的15个任务都执行完毕后,线程池中的还有多少线程?
我们用如下命令来观察应用中线程分布情况

$ adb shell top  -d 1 -H|grep azhengye|grep pool-1-thread

最开始的输出片段如下,可以看到池中有8个线程

 9975 10007 u0_a187  20   0   0% S 986380K  39092K  fg pool-1-thread-5 com.azhengye.testthreadpool
 9975 10008 u0_a187  20   0   0% S 986380K  39092K  fg pool-1-thread-6 com.azhengye.testthreadpool
 9975 10009 u0_a187  20   0   0% S 986380K  39092K  fg pool-1-thread-7 com.azhengye.testthreadpool
 9975 10013 u0_a187  20   0   0% S 986380K  39092K  fg pool-1-thread-1 com.azhengye.testthreadpool
 9975 10022 u0_a187  20   0   0% S 986380K  39092K  fg pool-1-thread-2 com.azhengye.testthreadpool
11125 11155 u0_a187  20   0   0% S 990476K  38056K  fg pool-1-thread-1 com.azhengye.testthreadpool
11125 11156 u0_a187  20   0   0% S 990476K  38056K  fg pool-1-thread-2 com.azhengye.testthreadpool
11125 11157 u0_a187  20   0   0% S 990476K  38056K  fg pool-1-thread-3 com.azhengye.testthreadpool
11125 11158 u0_a187  20   0   0% S 990476K  38056K  fg pool-1-thread-4 com.azhengye.testthreadpool
11125 11159 u0_a187  20   0   0% S 990476K  38056K  fg pool-1-thread-5 com.azhengye.testthreadpool
11125 11160 u0_a187  20   0   0% S 990476K  38056K  fg pool-1-thread-6 com.azhengye.testthreadpool
11125 11161 u0_a187  20   0   0% S 990476K  38056K  fg pool-1-thread-7 com.azhengye.testthreadpool
11125 11162 u0_a187  20   0   0% S 990476K  38056K  fg pool-1-thread-8 com.azhengye.testthreadpool

任务结束几分钟以后输出片段如下,池中仅剩余2个线程:

11125 11155 u0_a187  20   0   0% S 983060K  38228K  fg pool-1-thread-1 com.azhengye.testthreadpool
11125 11162 u0_a187  20   0   0% S 983060K  38228K  fg pool-1-thread-8 com.azhengye.testthreadpool
11125 11155 u0_a187  20   0   0% S 983060K  38228K  fg pool-1-thread-1 com.azhengye.testthreadpool
11125 11162 u0_a187  20   0   0% S 983060K  38228K  fg pool-1-thread-8 com.azhengye.testthreadpool

将注释掉的代码打开后,待任务结束几分钟后,我们将观察不到pool-1-thread-x的输出,说明我们主动关闭线程池后,池中一个线程都没有了,即核心线程也被销毁掉。

通过以上实验,我们可以明确看到不关闭线程池,就算很长时间也没有可执行的任务,核心线程还是会常驻。
开始接触线程池时,对keepAliveTime参数存疑,不知道它会不会影响到核心线程,以上也可以释疑了。


这里写图片描述

线程池如何保持核心线程常驻

我们自己创建的单个线程在执行完任务后就自己销毁了,并且也没有调用销毁线程的方法。那么问题来了,线程池中的线程执行完任务后,为什么没有被销毁,它怎么就能常驻线程池中呢?

探究这个问题之前,我们设想下可能的实现方式。肯定在线程的run方法里有一个死循环,循环不退出,线程就不会结束。因为一个线程不可能会start两次,一旦运行结束,线程就会退出销毁掉。这点可以从线程的start方法注释中看出。

* It is never legal to start a thread more than once.
* In particular, a thread may not be restarted once it has completed
* execution.
public synchronized void start() {
    //...
}

任务的执行入口是execute方法,我们从该处作为分析的切入点

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    int c = ctl.get();
    //如果池中线程数<核心线程数,尝试新建一个线程执行当前任务,尝试成功就直接返回
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true)) //A
            return;
        c = ctl.get();
    }
    //线程池正常运行,并且成功将当前任务放入workQueue
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        //任务成功入队后,可能线程池状态发生了变化,因此二次检查,二次检查结果异常则调用reject策略
        if (! isRunning(recheck) && remove(command))
            reject(command);
        //任务成功入队后,检查发现池中线程数量为0,则调用addWorker。
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false); //B
    }
    //如果待执行任务无法加入到workQueue,那么调用addWorker,如果失败,则调用reject策略
    else if (!addWorker(command, false)) //C
        reject(command);
}

以上过程体现了线程池执行任务的流程。我们将其转化成下面的流程图。


这里写图片描述

上述代码片段中标记的A/B/C三个地方都调用了addWorker方法来处理任务,并且三处传入的参数都不同,继续探究addWorker方法。

addWorker方法

// firstTask参数: 新的任务需要通过创建新线程来执行。如果为空则会尝试去workQueue中取任务,然后复用池中已有的线程
// core参数: 为true创建核心线程执行新任务,否则创建非核心线程执行
private boolean addWorker(Runnable firstTask, boolean core) {
    retry: //retry是一个语法标记,类似goto
    for (;;) {//这种写法,相对与while(true),在某些未做过优化的编译器上效率更高。
        int c = ctl.get();
        int rs = runStateOf(c); //获取线程池的状态,这里用到了位操作,后续在展开说明

        // 条件1(rs >= SHUTDOWN): 线程池不是RUNNING状态
        // 条件2(! (rs == SHUTDOWN && firstTask == null && ! workQueue.isEmpty()))
        // 条件2.1(rs == SHUTDOWN): 线程池处在SHUTDOWN状态
        // 条件2.2(firstTask == null): 任务为空,表明要去workQueue中取任务
        // 条件2.3(! workQueue.isEmpty()): workQueue不为空
        // 合起来看条件2表明是: 排除线程池在SHUTDOWN状态,但workQueue里还有待执行任务的情况

        // 加上条件1整个判断语句表明的是: 线程池不是RUNNING状态,并且如果线程池处在SHUTDOWN状态,除非workQueue里还有待执行的任务
        // 否则addWorker失败,待执行的任务不会得到执行。
        if (rs >= SHUTDOWN &&
            ! (rs == SHUTDOWN &&
               firstTask == null &&
               ! workQueue.isEmpty()))
            return false;

        for (;;) {
            int wc = workerCountOf(c); //池中的线程数量
            if (wc >= CAPACITY ||
                wc >= (core ? corePoolSize : maximumPoolSize)) //线程池已经饱和,待执行任务无法执行
                return false;
            if (compareAndIncrementWorkerCount(c)) //可以执行任务,池中线程数+1。用到了CAS操作,后续展开说明
                break retry;// 跳出了两重死循环
            c = ctl.get();  // Re-read ctl
            if (runStateOf(c) != rs)// 重新check 线程池状态,在从最外层循环开始
                continue retry;
        }
    }

    boolean workerStarted = false;
    boolean workerAdded = false;
    Worker w = null;
    try {
        w = new Worker(firstTask);//创建Worker对象,一个Worker就对应一个池中的线程,注意firstTask可以为null
        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());
                //线程池未关闭或者(线程池处在SHUTDOWN状态并且firstTask为null.)
                //后一种情况说明SHUTDOWN状态下,线程池依然要去执行workQueue中的任务
                if (rs < SHUTDOWN ||
                    (rs == SHUTDOWN && firstTask == null)) {
                    if (t.isAlive()) // precheck that t is startable
                        throw new IllegalThreadStateException();
                    workers.add(w);//添加当前worker到workers集合中,workers集合作为整个线程池的一个属性
                    int s = workers.size();
                    if (s > largestPoolSize)
                        largestPoolSize = s;
                    workerAdded = true;// 表明worker已经成功添加到池中
                }
            } finally {
                mainLock.unlock();
            }
            if (workerAdded) {
                t.start();//worker开始工作了
                workerStarted = true;
            }
        }
    } finally {
        if (! workerStarted)//如果worker启动失败,那么执行addWorkerFailed,该方法会1.将当前work从workers集合删除2.池中线程数量-1
            addWorkerFailed(w);
    }
    return workerStarted;
}

上述代码分析完成,我们有一个大概的认知: 新来的任务会被分配给一个Worker对象,Worker对象生成的同时,会自我创建一个线程,任务就会在该线程中得到执行。

可能会有疑问,上述代码看来似乎一个任务会创建一个Worker,而Worker又会创建线程,那么会不会造成多少个任务就多少个线程呢?
当然不会咯,注意addWorker调用的条件,就能消除该疑问,事实上大部分任务都被放入了workQueue中,而不是来一个任务就创建一个Worker。

Worker对象
我们继续跟进Worker对象寻找答案。

private final class Worker extends AbstractQueuedSynchronizer implements Runnable {
    // ...
    /** 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);
    }
    // ...
}

上一节代码中Worker启动调用的是

final Thread t = w.thread;
//...
t.start();//worker开始工作了
//...

Worker继承自AbstractQueuedSynchronizer,重要的是实现了Runnable接口,因此t.start()方法实际上调用的是Worker的run方法。这个模式跟下面的简化版代码本质上一样。

package com.azhengye.test;

public class Test {
    public static void main(String[] args) {
        Worker worker = new Worker("azhengye-thread");
        worker.thread.start();
    }

    static class Worker implements Runnable {
        private Thread thread;

        public Worker(String name) {
            thread = new Thread(this, name);
        }

        public void run() {
            System.out.print("name===" + Thread.currentThread().getName());
        }
    }
}

所以我们需要追查runWorker方法.

runWorker方法

final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;// 从worker中取出task,这个就是外部丢给线程池处理的任务,注意前面提过firstTask可能为空
    w.firstTask = null;
    w.unlock(); // allow interrupts
    boolean completedAbruptly = true;
    try {
        //task为空,或者用getTask方法从workQueue能取出非空的任务
        while (task != null || (task = getTask()) != null) {
            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) ||
                 (Thread.interrupted() &&
                  runStateAtLeast(ctl.get(), STOP))) &&
                !wt.isInterrupted())
                wt.interrupt();
            try {
                beforeExecute(wt, task);
                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);
                }
            } finally {
                task = null;
                w.completedTasks++;
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
        processWorkerExit(w, completedAbruptly);
    }
}

看到这我们最开始的疑问,核心线程如何常驻的马上就要揭晓答案了。runWorker在线程池中的某个线程中运行,如果这里面的while循环一直不退出,那么线程也就不会退出,这不就是常驻的效果嘛。while不退出我们需要查看getTask方法

getTask方法

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())) {
            decrementWorkerCount();
            return null;
        }

        int wc = workerCountOf(c);

        // Are workers subject to culling?
        //允许核心线程超时或者池中线程数大于核心线程数
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

        if ((wc > maximumPoolSize || (timed && timedOut))
            && (wc > 1 || workQueue.isEmpty())) {
            if (compareAndDecrementWorkerCount(c))
                return null;
            continue;
        }

        try {
            Runnable r = timed ?
                //允许超时的情况下,采用poll方式取任务,其取法特点是从workQueue首位取,最长等待keepAliveTime的时长,取不到返回null
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                //未设置核心线程超时,并且池中线程数量<=核心线程数,采用take方式取任务,
                //其取法特点是从workQueue首位取,若workQueue为null,则进入阻塞状态,直到能取出对象为止
                //没有任务执行,为什么核心线程能常驻池中的疑问揭晓了,核心线程被阻塞在该处了
                workQueue.take();
            if (r != null)
                return r;
            timedOut = true;
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
}

总结核心线程能常驻线程池的原因是由于,核心线程在取任务的时候采用了阻塞队列中的take方式,它会一直等待任务到来,而非核心线程采用的是阻塞队列中的poll方式,等待一段时间后,取出的任务为空,runWorker就会退出死循环,进而也就标志着线程的run方法执行完毕,最终等待它的只有销毁。

在探究问题的同时,也发现之前不主动关闭线程池核心线程就不会退出的说法不够严谨,因为线程池有allowCoreThreadTimeOut的参数,可以通过设置它,让核心线程也能超时退出。

线程池基础原理

通过以上分析,基本弄清了线程池的工作原理。我理解的线程池原理就是将待执行任务跟执行线程分开看待,任务放在队列中,而执行线程循环从队列中取任务执行。

线程池的整体实现还是比较复杂的,下面的例子将线程池需要考虑的各种复杂情况剔除,回归到本质。希望能帮助到你对其原理的理解。

package com.azhengye.test;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public class Test {
    public static void main(String[] args) {
        // 模拟线程池工作队列
        BlockingQueue<Runnable> workqueue = new LinkedBlockingQueue<Runnable>();
        for (int i = 0; i <= 10; i++) {
           //模拟待执行任务,让其打印随机数
            Task task = new Task();
            workqueue.offer(task);//任务入队
        }

        //  模拟池中的线程,所有的任务都可以用它来执行,并且它只创建一次,做到了复用
        Worker worker = new Worker("azhengye-thread", workqueue);
        worker.thread.start();
    }

    static class Task implements Runnable {
        public void run() {
            String threadName = Thread.currentThread().getName();
            System.out.print("task-" + Math.random() + " 在线程" + threadName + "中执行\n");
        }
    }

    static class Worker implements Runnable {
        private Thread thread;
        private BlockingQueue<Runnable> queue;

        public Worker(String name, BlockingQueue<Runnable> queue) {
            this.thread = new Thread(this, name);
            this.queue = queue;
        }

        public void run() {
            System.out.print("threadname===" + Thread.currentThread().getName() + "\n");
            for (;;) {
                try {
                    queue.take().run();// take 方法,任务执行完成后,该线程还是不会退出
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

未完待续部分

线程池在concurrent包下,这个下面到处都是Doug Lea大神的身影。我只能说,TMD,为何这么吊。

本文某些地方并没有分析到位,比如

  • BlockingQueue有哪些?各自的适用场景是什么?
  • Executors创建的线程池都有哪些?各自的适用场景是什么?
  • 线程池标记状态跟统计数量非常巧妙的利用了位操作
  • 线程池出了常规的execute方法,还有submit方法,这又会涉及到能返回线程执行结果的Callable

后续一一补充完善。追赶大神的脚步,争取把concurrent包过一遍。

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

推荐阅读更多精彩内容