JAVA并发编程与高并发解决方案 - 并发编程 六 之 线程池

JAVA并发编程与高并发解决方案 - 并发编程 六

版本 作者 内容
2018.7.4 chuIllusions 线程池

相关文章

JAVA并发编程与高并发解决方案 - 并发编程 一 之 并发相关知识
JAVA并发编程与高并发解决方案 - 并发编程 二 之 线程安全性、安全发布对象
JAVA并发编程与高并发解决方案 - 并发编程 三 之 线程安全策略
JAVA并发编程与高并发解决方案 - 并发编程 四 之 J.U.C之AQS
JAVA并发编程与高并发解决方案 - 并发编程 五 之 J.U.C组件拓展

线程池

  在前面使用的例子用,我们已经使用过线程池,基本上就是初始化线程池实例之后,把任务丢进去,等待调度执行就可以了,使用起来非常简单、方便。虽然使用很简单,但线程池涉及到的知识点非常多。需要分析其实现。

  JAVA中Thread这个类是线程类,在JAVA基础时,对于线程的认识是基于此类,为什么不使用Thread直接执行线程例子呢,而要使用线程池?可以试想,当并发数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。而线程池可以达到这样的效果:线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务。

Thread的弊端:

  1. 每次 new Thread() 新建对象,性能差;
  2. 线程缺乏统一管理,可能无限制的新建线程,相互竞争,有可能占用过多系统资源导致死机或OOM;
  3. 缺少更多的功能,如更多执行、定期执行、线程中断;

线程池的好处

  1. 重用存在的线程,减少对象创建、消亡的开销,性能佳,降低资源消耗;
  2. 可有效控制最大并发线程数,提高系统资源利用率,同时可以避免过多资源竞争,避免阻塞,提高响应速度;
  3. 提供定时执行、定期执行、单线程、并发数控制等功能,以达到提高线程的可管理性。

  阿里发布的 Java 开发手册中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
   Executors利用工厂模式向我们提供了4种线程池实现方式,但是并不推荐使用,原因是使用Executors创建线程池不会传入相关参数而使用默认值所以我们常常忽略了那些重要的参数(线程池大小、缓冲队列的类型等),而且默认使用的参数会导致资源浪费,不可取。

ThreadPoolExecutor

Constructor And Parameters

   java.uitl.concurrent.ThreadPoolExecutor 类是线程池中最核心的一个类,因此如果要透彻地了解Java中的线程池,必须先了解这个类,因此我们直接上源码:

public class ThreadPoolExecutor extends AbstractExecutorService {
    /** 构造函数 1 */
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {}
                              
    /** 构造函数 2 */
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory) {}
                              
    /** 构造函数 3 */
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              RejectedExecutionHandler handler) {}
                              
    /** 构造函数 4 */
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {}
}

   ThreadPoolExecutor 类继承结构是: Executor(I) <- ExecutorService(I) <- AbstractExecutorService(C) <- TreadPoolExecutor

   ThreadPoolExecutor类中提供了四个构造方法,在构造函数4中,参数最多,通过观察其他3个构造函数,发现前面三个构造器都是调用的第四个构造器进行的初始化工作。

  构造器中各个参数的含义:

  • corePoolSize: 核心池的大小,这个参数跟后面讲述的线程池的实现原理有非常大的关系。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了预创建线程的方法,即在没有任务到来之前就创建 corePoolSize 个线程或者 一个线程:

    • prestartCoreThread() : 预创建一个核心线程,使其闲置等待工作。
    • prestartAllCoreThreads() : 启动所有核心线程,导致它们空闲地等待工作。

    默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;

  • maximumPoolSize:线程池最大线程数,这个参数也是一个非常重要的参数,它表示在线程池中最多能创建多少个线程;

  • keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;

  • unit:参数keepAliveTime的时间单位,有7种取值,在TimeUnit类中有7种静态属性:

    • TimeUnit.DAYS : 以 天 为单位 ;
    • TimeUnit.HOURS : 以 小时 为单位 ;
    • TimeUnit.MINUTES : 以 分钟 为单位 ;
    • TimeUnit.SECONDS : 以 秒 为单位 ;
    • TimeUnit.MILLISECONDS : 以 毫秒 为单位 ;
    • TimeUnit.MICROSECONDS : 以 微秒 为单位 ;
    • TimeUnit.NANOSECONDS : 以 纳秒 为单位 ;
  • workQueue: 一个阻塞队列,用来存储等待执行的任务,这个参数的选择也很重要,会对线程池的运行过程产生重大影响,一般来说,这里的阻塞队列有以下几种选择:

    • ArrayBlockingQueue:基于数组的先进先出队列,此队列创建时必须指定大小;
    • LinkedBlockingQueue:基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE;
    • SynchronousQueue :这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。

    一般使用LinkedBlockingQueueSynchronousQueue

  • threadFactory:线程工厂,主要用来创建线程。 线程池最主要的一项工作,就是在满足某些条件的情况下创建线程。而在ThreadPoolExecutor线程池中,创建线程的工作交给ThreadFactory来完成。要使用线程池,就必须要指定ThreadFactory。 如果我们使用的构造函数时并没有指定使用的ThreadFactory,这个时候ThreadPoolExecutor会使用一个默认的ThreadFactory:DefaultThreadFactory(这个类在Executors工具类中);

  • handler:在ThreadPoolExecutor线程池中还有一个重要的接口:RejectedExecutionHandler。当提交给线程池的某一个新任务无法直接被线程池中“核心线程”直接处理,又无法加入等待队列,也无法创建新的线程执行;又或者线程池已经调用shutdown()方法停止了工作;又或者线程池不是处于正常的工作状态;这时候ThreadPoolExecutor线程池会拒绝处理这个任务,触发创建ThreadPoolExecutor线程池时定义的RejectedExecutionHandler接口的实现,

    表示当拒绝处理任务时的策略,有以下四种取值,四种值都为其静态内部类:

    • ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
    • ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
    • ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行新提交的任务。
Operating principle

  介绍完上面的构造函数以及其参数之后,介绍一下ThreadPoolExecutor的运行原理,在网上浏览到一篇相关文章,因此将它直接引用过来

深入理解java线程池—ThreadPoolExecutor,以下内容引用此文章,并且增加了自己的一点理解

ThreadPoolExecutor.execute()

   向线程池中提交一个不需要返回结果的任务

public void execute(Runnable command) {
        //任务为null,则抛出异常  
        if (command == null)
            throw new NullPointerException();
        //取出记录着runState和workerCount 的 ctl的当前值  
        int c = ctl.get();
        
        /**
         * 1.第一步:
         * 通过workerCountOf方法从ctl所表示的int值中提取出低29位的值,也就是当前活动的线程数。
         * 如果当前活动的线程数少于corePoolSize,则通过addWorker(command, true)新建一个线程,并将任务(command)添加到该线程中 

         */
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        
        /**
         * 2.第二步:
         * 2.1 isRunning(c) 当前线程池是否处于运行状态。源代码是通过判断c < SHUTDOWN 来确定返回值。由于RUNNING才会接收新任务,且只有这个值-1才小于SHUTDOWN
         * 2.2 workQueue.offer(command) 任务添加到缓冲队列 
         */
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            /**
             * 如果 线程池已经处于非运行状态,则从缓冲队列中移除任务然后采用线程池指定的策略拒绝任务 
             */
            if (! isRunning(recheck) && remove(command))
                reject(command);
            /**
             * 如果 线程池中任务数量为0,则通过addWorker(null, false)尝试新建一个线程,新建线程对应的任务为null
             */
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        
        /**
         * 3.第三步,也就是以上两个步骤都不满足:
         * 3.1 当前线程池并不处于Running状态
         * 3.2 当前线程池处于Running状态,但是缓冲队列已经满了
         */
        else if (!addWorker(command, false))
            reject(command);
    }

从上面execute()方法中,出现了ctl,跟踪源码分析起作用:

    //将整型的32位分为高3位和低29位,高3位表示线程池的状态,低29位表示活动的线程数  
    private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
    //获得高三位
    private static final int COUNT_BITS = Integer.SIZE - 3;
    //29位能表示的最大二进制整数,也就是活动线程数 
    private static final int CAPACITY   = (1 << COUNT_BITS) - 1;
    
    //运行状态是存储在高三位中
    private static final int RUNNING    = -1 << COUNT_BITS;
    private static final int SHUTDOWN   =  0 << COUNT_BITS;
    private static final int STOP       =  1 << COUNT_BITS;
    private static final int TIDYING    =  2 << COUNT_BITS;
    private static final int TERMINATED =  3 << COUNT_BITS;

线程池是通过Integer类型的高3位表述当前线程池的状态RUNNING,SHUTDOWN,STOP,TIDYING,TERMINATED 。低29位表示当前线程的运行任务数量。然后通过位运算来计算运行状态和任务的数量。

execute方法处理流程

  线程池在执行execute(Runnable),执行流程如下(对应图中的流程)

  1. 如果当前运行的线程少于corePoolSize,则创建新线程来执行任务(需要获得全局锁)
  2. 如果运行的线程等于或多于corePoolSize ,则将任务加入BlockingQueue
  3. 如果无法将任务加入BlockingQueue(队列已满),则创建新的线程来处理任务(需要获得全局锁)
  4. 如果创建新线程将使当前运行的线程超出maxiumPoolSize,任务将被拒绝,并调用RejectedExecutionHandler.rejectedExecution()方法。

线程池采取上述的流程进行设计是为了减少获取全局锁的次数。在线程池完成预热(当前运行的线程数大于或等于corePoolSize)之后,几乎所有的execute方法调用都执行步骤2。

ThreadPoolExecutor.addWorker()
    private boolean addWorker(Runnable firstTask, boolean core) {
        retry:
        for (;;) {
            int c = ctl.get(); //获取运行状态和工作数量
            int rs = runStateOf(c); //获取当前线程池运行的状态

            // Check if queue empty only if necessary.
            //条件代表着以下几个场景,直接返回false说明当前工作线程创建失败
            //1.rs>SHUTDOWN 此时不再接收新任务,且所有的任务已经执行完毕
            //2.rs=SHUTDOWN 此时不再接收新任务,但是会执行队列中的任务
            if (rs >= SHUTDOWN &&
                ! (rs == SHUTDOWN &&
                   firstTask == null &&
                   ! workQueue.isEmpty()))
                return false;

            for (;;) {
                int wc = workerCountOf(c);
                //先判断当前活动的线程数是否大于最大值,如果超过了就直接返回false说明线程创建失败
                //如果没有超过再根据core的值再进行以下判断
                //1. core为true,则判断当前活动的线程数是否大于corePoolSize 
                //2. core为false,则判断当前活动线程数是否大于maximumPoolSize
                if (wc >= CAPACITY ||
                    wc >= (core ? corePoolSize : maximumPoolSize))
                    return false;
                //比较当前值是否和c相同,如果相同,则改为c+1,并且跳出大循环,直接执行Worker进行线程创建
                if (compareAndIncrementWorkerCount(c))
                    break retry;
                c = ctl.get();  // Re-read ctl
                //检查下当前线程池的状态是否已经发生改变
                //如果已经改变了,则进行外层retry大循环,否则只进行内层的循环
                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 {
            //Worker的也是Runnable的实现类
            w = new Worker(firstTask);
            //因为不可以直接在Worker的构造方法中进行线程创建  
            //所以要把它的引用赋给t方便后面进行线程创建
            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)) {
                        if (t.isAlive()) // precheck that t is startable
                            throw new IllegalThreadStateException();
                        workers.add(w);//将创建的线程添加到workers容器中  
                        int s = workers.size();
                        if (s > largestPoolSize)
                            largestPoolSize = s;
                        workerAdded = true;
                    }
                } finally {
                    mainLock.unlock();
                }
                if (workerAdded) {
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
            if (! workerStarted)
                addWorkerFailed(w);
        }
        return workerStarted;
    }
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;
        
    Worker(Runnable firstTask) {
        setState(-1); // inhibit interrupts until runWorker
        this.firstTask = firstTask;
        this.thread = getThreadFactory().newThread(this);
    }
}

WorkerThreadPoolExecutor为一个内部类实现了Runnable接口。只有一个构造方法,在上面的addWorker()final Thread t = w.thread;知道其实是获取了线程的对象,因为在构造方法中,线程的引用即是它自己。
因此在调用t.start()执行的是(Worker类中的方法):

/** Delegates main run loop to outer runWorker  */
public void run() {
    //这里执行的是ThreadPoolExecutor中的runWorker
    runWorker(this);
}
ThreadPoolExecutor.runWorker()
    final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;//获取Worker中的任务
        w.firstTask = null; //将Woeker中的任务置空
        w.unlock(); // allow interrupts
        boolean completedAbruptly = true;
        try {
            //如果当前任务为空  那么就从getTask中获得任务
            /**
             * 如果task不为空,执行完task后则将task置空
             * 继续进入循环,则从getTask中获取任务
             */
            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);
        }
    }

从上面可以简单理解,就是执行任务,只是执行任务需要进行处理,包括获得任务、任务开始前处理、任务执行、任务执行后处理。但是,关键代码还是里面所调用的一个方法getTask()

beforeExecute(Thread t, Runnable r)afterExecute(Runnable r, Throwable t)并未在类中有处理业务的逻辑,即可以通过继承线程池的方式来重写这两个方法,这样就能够对任务的执行进行监控。

这里我有两个疑问?

  • 怎么退出这个While循环,也就是进入到processWorkerExit()
    1. 从While循环体中可以知道,当线程运行时出现异常,那么都会退出循环,进入到processWorkerExit()
    2. getTask()获得结果为null,则也会进到processWorkerExit()
  • 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?
            //如果设置了allowCoreThreadTimeOut(true)
            //或者当前运行的任务数大于设置的核心线程数
            // timed = true
            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(long timeout, TimeUnit unit) 将会在规定的时间内去任务
                    //如果没取到就返回null
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    //take会一直阻塞,等待任务的添加
                    workQueue.take();
                if (r != null)
                    return r;
                timedOut = true;
            } catch (InterruptedException retry) {
                timedOut = false;
            }
        }
    }

到此,终于发现为什么线程池能够保证一直等待任务而不被销毁,其实就是进入了阻塞状态。

ThreadPoolExecutor.processWorkerExit()
    /**
     * @param completedAbruptly 工作线程是否死与执行任务出现的异常
     */
    private void processWorkerExit(Worker w, boolean completedAbruptly) {
        if (completedAbruptly) //如果突然被打断,工作线程数不会被减少
            decrementWorkerCount();

        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            completedTaskCount += w.completedTasks;
            workers.remove(w);
        } finally {
            mainLock.unlock();
        }

        tryTerminate();

        int c = ctl.get();
        //判断运行状态是否在STOP之前
        if (runStateLessThan(c, STOP)) {
            
            if (!completedAbruptly) {//正常退出,也就是task == null
                int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
                if (min == 0 && ! workQueue.isEmpty())
                    min = 1;
                if (workerCountOf(c) >= min)
                    return; // replacement not needed
            }
            //新增一个工作线程,代替原来的工作线程
            addWorker(null, false);
        }
    }
AbstractExecutorService.submit()

   向线程池中提交一个需要返回结果的任务

    public Future<?> submit(Runnable task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<Void> ftask = newTaskFor(task, null);
        execute(ftask);
        return ftask;
    }
    
    public <T> Future<T> submit(Runnable task, T result) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<T> ftask = newTaskFor(task, result);
        execute(ftask);
        return ftask;
    }
    
    public <T> Future<T> submit(Callable<T> task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<T> ftask = newTaskFor(task);
        execute(ftask);
        return ftask;
    }

ThreadPoolExecutor中并未发现submit(),因此从父类,即抽象类AbstractExecutorService中找到submit()的方法实现,从方法实现中,可知:

  1. submit()接收任务参数,并将参数封装为FutureTask任务类
  2. 将封装好的FutureTask提交到execute()

结论:submit()真正实现的任务处理流程跟execute()一样,也可以说submit()就是调用了execute()

线程池的处理流程

  从上面的流程图可以知道,向线程池提交一个任务后,共经历以下流程:

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

推荐阅读更多精彩内容