Java线程池ThreadPoolExecutor类

一、简述

在《阿里巴巴java开发手册》中指出了线程资源必须通过线程池提供,不允许在应用中自行显示的创建线程,这样一方面使得线程的创建更加规范,可以合理控制开辟线程的数量;另一方面线程的细节管理交给线程池处理,优化了资源的开销。
线程池不允许使用 Executors 去创建,而要通过 ThreadPoolExecutor 方式,这一方面是由于 jdk 中 Executor 框架虽然提供了如 newFixedThreadPool()、newSingleThreadExecutor()、newCachedThreadPool() 等创建线程池的方法,但都有其局限性,不够灵活;另外由于前面几种方法内部也是通过 ThreadPoolExecutor 方式实现,使用 ThreadPoolExecutor 有助于明确线程池的运行规则,创建符合自己业务场景需要的线程池,避免资源耗尽的风险。ThreadPoolExecutor 的构造函数:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    if (corePoolSize < 0 || maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize || keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.acc = System.getSecurityManager() == null ?
                null : AccessController.getContext();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}

源码中对每个参数的解释:

参数含义如下:
1️⃣corePoolSize:指定了线程池中的线程数量,它的数量决定了添加的任务是开辟新的线程去执行,还是放到 workQueue 任务队列中去。
2️⃣maximumPoolSize:指定了线程池中的最大线程数量,这个参数会根据使用的 workQueue 任务队列的类型,决定线程池会开辟的最大线程数量。
3️⃣keepAliveTime:当线程池中空闲线程数量超过 corePoolSize 时,多余的线程会在多长时间内被销毁。
4️⃣unit:keepAliveTime 的单位。
5️⃣workQueue:任务队列,被添加到线程池中,但尚未被执行的任务;它一般分为直接提交队列、有界任务队列、无界任务队列、优先任务队列。
6️⃣threadFactory:线程工厂,用于创建线程,一般用默认即可。
7️⃣handler:拒绝策略;当任务太多来不及处理时,如何拒绝任务。

线程池执行任务逻辑和线程池参数的关系:

二、corePoolSize 线程池核心线程大小

核心线程数由开发者指定,在启动线程池,并没有往里面添加任务时,池子里面的线程数量为 0,当达到核心线程数时,无论是否向池子里提交任务,池子里至少会保持核心线程数数量。
核心线程数,即线程池维护的最小线程数量,即使这些线程处于空闲状态,也不会被销毁,除非设置了 allowCoreThreadTimeOut。如图:创建了一个核心线程数为 2 的线程池,打印一下线程池信息,可以看到,线程池里面的核心线程数为 0:

接下来,向线程池里添加 5 个任务,当所有任务都执行完之后,线程池里面存活的线程数为 2(核心线程数),并没有变为 0。

三、maximumPoolSize 线程池最大线程数量

最大线程数参数,是在已经达到核心线程池参数,并且任务队列已经满的情况下,才去判断该参数。
该参数定义了一个线程池中最多能容纳多少个线程。当一个任务提交到线程池中时,如果线程数量达到了核心线程数,并且任务队列已满,不能再向任务队列中添加任务时,这时会检查任务是否达到了最大线程数,如果未达到,则创建新线程,执行任务,否则,执行拒绝策略。

源码如下:可以看出,当调用 submit(Runnable task),将任务提交到线程池中时,会调用 execute() 去执行任务,在该方法内,会进行核心线程数,任务队列的判断,最后决定是执行或者是拒绝。
public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    
    int c = ctl.get();
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        if (!isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    } else if (!addWorker(command, false))
        reject(command);
}

四、keepAliveTime 空闲线程存活时间

线程池中大于核心线程数 corePoolSize 的那部分线程如果处于空闲状态,在指定时间 keepAliveTime 后,该空闲线程会被销毁。即执行完任务之后,多余(maximumPoolSize-corePoolSize)线程在线程池中存活的时间。

五、unit 空闲线程存活时间单位

keepAliveTime 的计量单位

六、workQueue 任务队列直接提交队列、有界任务队列、无界任务队列、优先任务队列

该参数主要是在核心线程数 corePoolSize 的线程都在执行任务,不够提交到线程池中的任务用时,才起作用的参数。用来缓存任务的队列。等待线程去执行。JDK 提供了四种工作队列:

1️⃣【直接提交队列】SynchronousQuene

一个不缓存任务的阻塞队列,没有容量,生产者放入一个任务必须等到消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务。

如果用于执行任务的线程数量小于 maximumPoolSize,则尝试创建新的进程,如果达到 maximumPoolSize 设置的最大值,则根据设置的 handler 执行拒绝策略。因此这种方式提交的任务不会被缓存起来,而是会被马上执行,在这种情况下,需要对程序的并发量有个准确的评估,才能设置合适的 maximumPoolSize 数量,否则很容易就会执行拒绝策略。

public class ThreadPool {
    private static ExecutorService pool;
    public static void main(String[] args)  {
        //maximumPoolSize设置为2 ,拒绝策略为AbortPolic策略,直接抛出异常
        pool = new ThreadPoolExecutor(1, 2, 1000, TimeUnit.MILLISECONDS, 
                               new SynchronousQueue<Runnable>(),
                               Executors.defaultThreadFactory(),
                               new ThreadPoolExecutor.AbortPolicy());
        for(int i=0;i<3;i++) {
            pool.execute(new ThreadTask());
        }   
    }
}
public class ThreadTask implements Runnable{
    public ThreadTask() {}  
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

输出结果为:

pool-1-thread-1
pool-1-thread-2
Exception in thread "main" java.util.concurrent.RejectedExecutionException: 
Task com.hhxx.test.ThreadTask@55f96302 
rejected from java.util.concurrent.ThreadPoolExecutor@3d4eac69
[Running, pool size = 2, active threads = 0, queued tasks = 0, completed tasks = 2]
    at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(Unknown Source)
    at java.util.concurrent.ThreadPoolExecutor.reject(Unknown Source)
    at java.util.concurrent.ThreadPoolExecutor.execute(Unknown Source)
    at com.hhxx.test.ThreadPool.main(ThreadPool.java:17)

可以看到,当任务队列为 SynchronousQueue,创建的线程数大于 maximumPoolSize 时,直接执行了拒绝策略抛出异常。

2️⃣【有界任务队列】ArrayBlockingQueue

基于数组的有界阻塞队列,可以防止资源耗尽问题,按 FIFO 排序。当线程池中线程数量达到 corePoolSize 后,再有新任务进来,则会将任务放入该队列的队尾,等待被调度。如果队列已经是满的,则创建一个新线程,如果线程数量达到 maximumPoolSize,则会执行拒绝策略。如下:

pool = new ThreadPoolExecutor(1, 2, 1000, TimeUnit.MILLISECONDS, 
                              new ArrayBlockingQueue<Runnable>(10), 
                              Executors.defaultThreadFactory(),
                              new ThreadPoolExecutor.AbortPolicy());

使用 ArrayBlockingQueue 有界任务队列,线程数量的上限与该任务队列的状态有直接关系。如果有界队列初始容量较大或者没有达到超负荷的状态,线程数将一直维持在 corePoolSize 以下,反之当任务队列已满时,则会以 maximumPoolSize 为最大线程数上限。

3️⃣【无界阻塞队列】LinkedBlockingQuene

基于链表的无界阻塞队列(其实最大容量为 Interger.MAX),按照 FIFO 排序。由于该队列的近似无界性,当线程池中线程数量达到 corePoolSize 后,再有新任务进来,会一直存入该队列,而不会去创建新线程直到 maximumPoolSize,因此使用该工作队列时,参数 maximumPoolSize 其实是不起作用的。当使用这种任务队列模式时,一定要注意任务提交与处理之间的协调与控制,不然会出现队列中的任务由于无法及时处理导致一直增长,直到最后资源耗尽的问题。

pool = new ThreadPoolExecutor(1, 2, 1000, TimeUnit.MILLISECONDS, 
                              new LinkedBlockingQueue<Runnable>(),
                              Executors.defaultThreadFactory(),
                              new ThreadPoolExecutor.AbortPolicy());

4️⃣【优先任务队列】PriorityBlockingQueue

具有优先级的无界阻塞队列,优先级通过参数 Comparator 实现。

public class ThreadPool {
    private static ExecutorService pool;
    public static void main(String[] args) {
        //优先任务队列
        pool = new ThreadPoolExecutor(1, 2, 1000, TimeUnit.MILLISECONDS, 
                                      new PriorityBlockingQueue<Runnable>(),
                                      Executors.defaultThreadFactory(),
                                      new ThreadPoolExecutor.AbortPolicy());
        for(int i=0;i<20;i++) {
            pool.execute(new ThreadTask(i));
        }    
    }
}
public class ThreadTask implements Runnable,Comparable<ThreadTask>{
    private int priority;
    public int getPriority() {
        return priority;
    }
    public void setPriority(int priority) {
        this.priority = priority;
    }
    public ThreadTask() {}
    public ThreadTask(int priority) {
        this.priority = priority;
    }
//当前对象和其他对象做比较,当前优先级大就返回-1,优先级小就返回1,值越小优先级越高
    public int compareTo(ThreadTask o) {
         return  this.priority>o.priority?-1:1;
    }    
    public void run() {
        try {
            //让线程阻塞,使后续任务进入缓存队列
            Thread.sleep(1000);
System.out.println("priority:"+this.priority+",ThreadName:"+Thread.currentThread().getName());
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

执行结果:

priority:0,ThreadName:pool-1-thread-1
priority:9,ThreadName:pool-1-thread-1
priority:8,ThreadName:pool-1-thread-1
priority:7,ThreadName:pool-1-thread-1
priority:6,ThreadName:pool-1-thread-1
priority:5,ThreadName:pool-1-thread-1
priority:4,ThreadName:pool-1-thread-1
priority:3,ThreadName:pool-1-thread-1
priority:2,ThreadName:pool-1-thread-1
priority:1,ThreadName:pool-1-thread-1

可以看到除了第一个任务直接创建线程执行外,其他的任务都被放入了优先任务队列,按优先级进行了重新排列执行,且线程池的线程数一直为 corePoolSize,也就是只有一个。

通过运行的代码可以看出 PriorityBlockingQueue 它其实是一个特殊的无界队列,它其中无论添加了多少个任务,线程池创建的线程数也不会超过 corePoolSize 的数量,只不过其他队列一般是按照先进先出的规则处理任务,而 PriorityBlockingQueue 队列可以自定义规则根据任务的优先级顺序先后执行。

七、ThreadFactory 线程工厂

线程池中线程就是通过 ThreadPoolExecutor 中的 ThreadFactory 线程工厂创建的。通过自定义 ThreadFactory,可以按需要对线程池中创建的线程进行一些特殊的设置,如命名、优先级、是否为 daemon 线程等等,下面代码通过 ThreadFactory 对线程池中创建的线程进行记录与命名:

public class ThreadPool {
    private static ExecutorService pool;
    public static void main( String[] args )  {
        //自定义线程工厂
        pool = new ThreadPoolExecutor(2, 4, 1000, 
                                     TimeUnit.MILLISECONDS, 
                                     new ArrayBlockingQueue<Runnable>(5),
                                     new ThreadFactory() {
            public Thread newThread(Runnable r) {
                System.out.println("线程"+r.hashCode()+"创建");
                //线程命名
                Thread th = new Thread(r,"threadPool"+r.hashCode());
                return th;
            }
        }, new ThreadPoolExecutor.CallerRunsPolicy());
        for(int i=0;i<10;i++) {
            pool.execute(new ThreadTask());
        }    
    }
}
public class ThreadTask implements Runnable{    
    public void run() {
        //输出执行线程的名称
        System.out.println("ThreadName:"+Thread.currentThread().getName());
    }
}

输出结果:

线程118352462创建
线程1550089733创建
线程865113938创建
ThreadName:threadPool1550089733
ThreadName:threadPool118352462
线程1442407170创建
ThreadName:threadPool1550089733
ThreadName:threadPool1550089733
ThreadName:threadPool1550089733
ThreadName:threadPool865113938
ThreadName:threadPool865113938
ThreadName:threadPool118352462
ThreadName:threadPool1550089733
ThreadName:threadPool1442407170

可以看到线程池中,每个线程的创建都进行了记录输出与命名。

八、handler 拒绝策略

一般,为防止资源被耗尽,任务队列都会选择有界任务队列,但该模式下如果出现任务队列已满且线程池中线程数达到 maximumPoolSize 时,这时就需要指定 ThreadPoolExecutor 的 RejectedExecutionHandler 参数即合理的拒绝策略,来处理线程池“超载”的情况。jdk 中 ThreadPoolExecutor 自带的四种拒绝策略如下:

1️⃣【CallerRunsPolicy】如果线程池的线程数量达到上限,该策略会在调用者线程中直接执行被拒绝任务的 run 方法,除非线程池已经 shutdown,则直接抛弃任务。

2️⃣【AbortPolicy】该策略直接抛出RejectedExecutionException,阻止系统正常工作。

3️⃣【DiscardPolicy】该策略下,直接丢弃无法处理的任务,什么都不做。使用此策略,业务场景需允许任务的丢失。

4️⃣【DiscardOldestPolicy】该策略下,抛弃进入队列最早的那个任务,然后尝试把这次拒绝的任务放入队列。

以上内置的策略均实现了 RejectedExecutionHandler,当然也可以自己扩展 RejectedExecutionHandler,定义自己的拒绝策略,看下示例代码:

public class ThreadPool {
    private static ExecutorService pool;
    public static void main(String[] args) {
        //自定义拒绝策略
        pool = new ThreadPoolExecutor(1, 2, 1000, TimeUnit.MILLISECONDS, 
                                      new ArrayBlockingQueue<Runnable>(5),
                                      Executors.defaultThreadFactory(), 
                                      new RejectedExecutionHandler() {
            public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                System.out.println(r.toString()+"执行了拒绝策略");
                
            }
        });
        for(int i=0;i<10;i++) {
            pool.execute(new ThreadTask());
        }    
    }
}
public class ThreadTask implements Runnable{    
    public void run() {
        try {
            //让线程阻塞,使后续任务进入缓存队列
            Thread.sleep(1000);
            System.out.println("ThreadName:"+Thread.currentThread().getName());
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

输出结果:

com.hhxx.test.ThreadTask@33909752执行了拒绝策略
com.hhxx.test.ThreadTask@55f96302执行了拒绝策略
com.hhxx.test.ThreadTask@3d4eac69执行了拒绝策略
ThreadName:pool-1-thread-2
ThreadName:pool-1-thread-1
ThreadName:pool-1-thread-1
ThreadName:pool-1-thread-2
ThreadName:pool-1-thread-1
ThreadName:pool-1-thread-2
ThreadName:pool-1-thread-1

可以看到由于任务加了休眠阻塞,执行需要花费一定时间,导致会有一定的任务被丢弃,从而执行自定义的拒绝策略。

九、ThreadPoolExecutor扩展

ThreadPoolExecutor 扩展主要是围绕 beforeExecute()、afterExecute() 和 terminated() 三个接口实现的:
1️⃣beforeExecute:线程池中任务运行前执行
2️⃣afterExecute:线程池中任务运行完毕后执行
3️⃣terminated:线程池退出后执行

通过这三个接口可以监控每个任务的开始和结束时间,或者其他一些功能。下面可以通过代码实现一下:

public class ThreadPool {
    private static ExecutorService pool;
    public static void main( String[] args ) throws InterruptedException
    {
        //实现自定义接口
        pool = new ThreadPoolExecutor(2, 4, 1000, 
                                      TimeUnit.MILLISECONDS, 
                                      new ArrayBlockingQueue<Runnable>(5),
                                      new ThreadFactory() {
            public Thread newThread(Runnable r) {
                System.out.println("线程"+r.hashCode()+"创建");
                //线程命名
                Thread th = new Thread(r,"threadPool"+r.hashCode());
                return th;
            }
        }, new ThreadPoolExecutor.CallerRunsPolicy()) {
    
            protected void beforeExecute(Thread t,Runnable r) {
                System.out.println("准备执行:"+ ((ThreadTask)r).getTaskName());
            }
            
            protected void afterExecute(Runnable r,Throwable t) {
                System.out.println("执行完毕:"+((ThreadTask)r).getTaskName());
            }
            
            protected void terminated() {
                System.out.println("线程池退出");
            }
        };
          
        for(int i=0;i<10;i++) {
            pool.execute(new ThreadTask("Task"+i));
        }    
        pool.shutdown();
    }
}

public class ThreadTask implements Runnable{    
    private String taskName;
    public String getTaskName() {
        return taskName;
    }
    public void setTaskName(String taskName) {
        this.taskName = taskName;
    }
    public ThreadTask(String name) {
        this.setTaskName(name);
    }
    public void run() {
        //输出执行线程的名称
        System.out.println("TaskName"+this.getTaskName()+"---
                            ThreadName:"+Thread.currentThread().getName());
    }
}

输出结果:

线程118352462创建
线程1550089733创建
准备执行:Task0
准备执行:Task1
TaskNameTask0---ThreadName:threadPool118352462
线程865113938创建
执行完毕:Task0
TaskNameTask1---ThreadName:threadPool1550089733
执行完毕:Task1
准备执行:Task3
TaskNameTask3---ThreadName:threadPool1550089733
执行完毕:Task3
准备执行:Task2
准备执行:Task4
TaskNameTask4---ThreadName:threadPool1550089733
执行完毕:Task4
准备执行:Task5
TaskNameTask5---ThreadName:threadPool1550089733
执行完毕:Task5
准备执行:Task6
TaskNameTask6---ThreadName:threadPool1550089733
执行完毕:Task6
准备执行:Task8
TaskNameTask8---ThreadName:threadPool1550089733
执行完毕:Task8
准备执行:Task9
TaskNameTask9---ThreadName:threadPool1550089733
准备执行:Task7
执行完毕:Task9
TaskNameTask2---ThreadName:threadPool118352462
TaskNameTask7---ThreadName:threadPool865113938
执行完毕:Task7
执行完毕:Task2
线程池退出

可以看到通过对 beforeExecute()、afterExecute()和terminated() 的实现,对线程池中线程的运行状态进行了监控,在其执行前后输出了相关打印信息。另外使用 shutdown 方法可以比较安全的关闭线程池,当线程池调用该方法后,线程池中不再接受后续添加的任务。但是,此时线程池不会立刻退出,直到添加到线程池中的任务都已经处理完成,才会退出。

十、线程池线程数量

线程池线程数量的设置没有一个明确的指标,根据实际情况,只要不是设置的偏大和偏小都问题不大,结合下面这个公式即可:

/**
  * Nthreads=CPU数量
  * Ucpu=目标CPU的使用率,0<=Ucpu<=1
  * W/C=任务等待时间与任务计算时间的比率
  */
  Nthreads = Ncpu*Ucpu*(1+W/C)

十一、提交任务时,线程池队列已满,会发生什么

1️⃣无界队列 LinkedBlockingQueue
继续添加任务到阻塞队列中等待执行,因为无界队列可以近乎认为是一个无穷大的队列,可以无限存放任务。

2️⃣有界队列
如果使用的是有界队列比如 ArrayBlockingQueue,任务首先会被添加到 ArrayBlockingQueue 中,ArrayBlockingQueue 满了,会根据 maximumPoolSize 的值增加线程数量,如果增加了线程数量还是处理不过来,ArrayBlockingQueue 继续满,则会使用拒绝策略 RejectedExecutionHandler 处理满了的任务,默认是 AbortPolicy。

十二、如何定义线程池参数

1️⃣【CPU密集型】=> 线程池的大小推荐为 CPU 数量 + 1,CPU 数量可以根据Runtime.availableProcessors方法获取

2️⃣【IO密集型】=> CPU 数量 * CPU 利用率 * (1 + 线程等待时间/线程CPU时间)

3️⃣【混合型】=> 将任务分为 CPU 密集型和 IO 密集型,然后分别使用不同的线程池去处理,从而使每个线程池可以根据各自的工作负载来调整

4️⃣【阻塞队列】=> 推荐使用有界队列,有界队列有助于避免资源耗尽的情况发生

5️⃣【拒绝策略】=> 默认采用的是 AbortPolicy 拒绝策略,直接在程序中抛出RejectedExecutionException,因为是运行时异常,不强制 catch,这种处理方式不够优雅。处理拒绝策略有以下几种比较推荐:

  1. 在程序中捕获RejectedExecutionException,在捕获异常中对任务进行处理。针对默认拒绝策略。

  2. 使用 CallerRunsPolicy 拒绝策略,该策略会将任务交给调用 execute 的线程执行【一般为主线程】,此时主线程将在一段时间内不能提交任何任务,从而使工作线程处理正在执行的任务。此时提交的线程将被保存在 TCP 队列中,TCP 队列满将会影响客户端,这是一种平缓的性能降低。

  3. 自定义拒绝策略,只需要实现RejectedExecutionHandler接口即可

  4. 如果任务不是特别重要,使用 DiscardPolicy 和 DiscardOldestPolicy 拒绝策略将任务丢弃也是可以的。

如果使用 Executors 的静态方法创建 ThreadPoolExecutor 对象,可以通过使用 Semaphore 对任务的执行进行限流也可以避免出现 OOM

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