线程池的使用 Java并发编程实战总结

        第6章介绍了任务执行框架, 它不仅能简化任务与线程的生命周期管理, 而且还提供一 种简单灵活的方式将任务的提交与任务的执行策略解耦开来。 第7章介绍了在实际应用程序中使用任务执行框架时出现的一些与服务生命周期相关的细节问题。 本章将介绍对线程池进行配置与调优的一些高级选项, 并分析在使用任务执行框架时需要注意的各种危险, 以及一些使用 Executor的高级示例。

在任务与执行策略之间的隐性耦合

        我们已经知道,Executor框架可以将任务的提交与任务的执行策略解耦开来。 就像许多对 复杂过程的解耦操作那样, 这种论断多少有些言过其实了。 虽然Executor框架为制定和修改执行策略都提供了相当大的灵活性, 但并非所有的任务都能适用所有的执行策略。 有些类型的任 务需要明确地指定执行策略, 包括:

a.依赖性任务。 大多数行为正确的任务都是独立的: 它们不依赖于其他任务的执行时序、 执 行结果或其他效果。 当在线程池中执行独立的任务时, 可以随意地改变线程池的大小和配置, 这些修改只会对执行性能产生影响。 然而,如果提交给线程池的任务需要依赖其他的任务, 那么就隐含地给执行策略带来了约束, 此时必须小心地维持次些执行策略以避免产生活跃性问题。

b.使用线程封闭机制的任务。 与线程池相比, 单线程的Executor能够对并发性做出更强的承诺。 它们能确保任务不会并发地执行, 使你能够放宽代码对线程安全的要求。 对象可以封闭在 任务线程中, 使得在该线程中执行的任务在访问该对象时不需要同步, 即使这些资源不是线程 安全的也没有问题。 这种情形将在任务与执行策略之间形成隐式的耦合- 􀁎任务要求其执行所在的Executor是单线程的e。如果将Executor从单线程环境改为线程池环境, 那么将会失去线程安全性。

c.对响应时间敏感的任务。GUI应用程序对于响应时间是敏感的:如果用户在点击按钮后需要很长延迟才能得到可见的反馈, 那么他们会感到不满。如果将一个运行时间较长的任务提交到单线程的Executor中, 或者将多个运行时间较长的任务提交到一个只包含少量线程的线程池 中, 那么将降低由该Executor管理的服务的响应性。

d.使用ThreadLocal的任务。ThreadLocal使每个线程都可以拥有某个变量的一个私有“版本“。然而,只要条件允许,Executor可以自由地重用这些线程。在标准的Executor实现中,当执行需求较低时将回收空闲线程,而当需求增加时将添加新的线程,并且如果从任务中抛出了一个未检查异常,那么将用一个新的工作者线程来替代抛出异常的线程。只有当线程本地值 的生命周期受限于任务的生命周期时,在线程池的线程中使用ThreadLocal才有意义,而在线 程池的线程中不应该使用 ThreadLocal在任务之间传递值。

        只有当任务都是同类型的并且相互独立时,线程池的性能才能达到最佳。如果将运行时间较长的与运行时间较短的任务混合在一起,那么除非线程池很大,否则将可能造成 “拥塞 ”。如果提交的任务依赖于其他任务,那么除非线程池无限大,否则将可能造成死锁。幸运的是, 在基于网络的典型股务器应用程序中一一网页服务器、邮件服务器以及文件服务器等,它们的 请求通常都是同类型的并且相互独立的。

        在一些任务中,需要拥有或排除某种特定的执行策略。如果某些任务依赖于其他的任务,那么会要求线程池足够大,从而确保它们依赖任务不会被放入等待队列中或被拒绝,而采用线程封闭机制的任务需要串行执行。通过将这些需求写入文档,将来的代码维护人员就不会由于使用了某种不合适的执行策略而破坏安全性或活跃性。

线程饥饿死锁

        在线程池中,如果任务依赖于其他任务,那么可能产生死锁。在单线程的Executor中,如 果一个任务将另一个任务提交到同一个Executor,并且等待这个被提交任务的结果,那么通常会引发死锁。第二个任务停留在工作队列中,并等待第一个任务完成,而第一个任务又无法完 成,因为它在等待第二个任务的完成。在更大的线程池中, 只要线程池中的任务需要无限期地等待一些必须由池中 其他任务才能提供的资源或条件,例如某个任务等待另一个任务的返回值或执行结果,那么除 非线程池足够大,否则将发生线程饥饿死锁。 

        在程序清单8-1的 ThreadDeadlock中给出了线程饥饿死锁的示例。RenderPageTask向Executor提交了两个任务来获取网页的页眉和页脚,绘制页面,等待获取页眉和页脚任务的结果,然后将页眉、页面主体和页脚组合起来并形成最终的页面。如果使用单线程的Executor,那么ThreadDeadlock会经常发生死锁。同样,如果线程池不够大,那么当多个任务通过栅栏 (Barrier)机制来彼此协调时,将导致线程饥饿死锁。


        每当提交了一个有依赖性Executor任务时,要清楚地知道可能会出现线程“饥饿”死锁,因此需要在代码或配置Executor的配置文件中记录线程池的大小限制或配置限制。

        除了在线程池大小上的显式限制外, 还可能由于其他资源上的约束而存在一些隐式限制。如果应用程序使用一个包含10个连接的JDBC连接池, 并且每个任务需要一个数据库连接, 那么线程池就好像只有10个线程, 因为当超过10个任务时, 新的任务需要等待其他任务释放连接。

运行时间较长的任务

        如果任务阻塞的时间过长, 那么即使不出现死锁, 线程池的响应性也会变得糟糕。执行时 间较长的任务不仅会造成线程池堵塞, 甚至还会增加执行时间较短任务的服务时间。如果线程 池中线程的数量远小于在稳定状态下执行时间较长任务的数量, 那么到最后可能所有的线程都会运行这些执行时间较长的任务, 从而影响整体的响应性。

        有一项技术可以缓解执行时间较长任务造成的影响, 即限定任务等待资源的时间, 而不要无限制地等待。在平台类库的大多数可阻塞方法中, 都同时定义了限时版本和无限时版本, 例如Thread.join、BlockingQueue.put、CountDownLatch.await以及Selector.select等。如果等待超时,那么可以把任务标识为失败,然后中止任务或者将任务重新放回队列以便随后执行。这样, 无论任务的最终结果是否成功, 这种办法都能确保任务总能继续执行下去, 并将线程释放 出来以执行一些能更快完成的任务。如果在线程池中总是充满了被阻塞的任务, 那么也可能表明线程池的规模过小。


设置线程池的大小

        线程池的理想大小取决于被提交任务的类型以及所部署系统的特性。在代码中通常不会固定线程池的大小, 而应该通过某种配置机制来提供,或者根据Runtime.availableProcessors来动态计算。

        幸运的是, 要设置线程池的大小也并不困难, 只需要避免 “过大” 和 “过小” 这两种极端情况。如果线程池过大,那么大量的线程将在相对很少的CPU和内存资源上发生竞争, 这不仅会导致更高的内存使用量, 而且还可能耗尽资源。如果线程池过小, 那么将导致许多空闲的处理器无法执行工作,从而降低吞吐率。

        要想正确地设置线程池的大小,必须分析计算环境、资源预算和任务的特性。在部署的系统中有多少个CPU? 多大的内存?任务是计算密集型、I/0密集型还是二者皆可?它们是否需要像JDBC连接这样的稀缺资源?如果需要执行不同类别的任务,井且它们之间的行为相差很大,那么应该考虑使用多个线程池,从而使每个线程池可以根据各自的工作负载来调整。

        对于计算密集型的任务,在拥有N个处理器的系统上,当线程池的大小为N+1时,通常能实现最优的利用率。(即使当计算密集型的线程偶尔由于页缺失故障或者其他原因而暂停时,这个 “额外 ” 的线程也能确保CPU的时钟周期不会被浪费。)要正确地设置线程 池的大小,你必须估算出任务的等待时间与计算时间的比值。这种估算不需要很精确,并且可 以通过一些分析或监控工具来获得。你还可以通过另一种方法来调节线程池的大小:在某个基准负载下,分别设置不同大小的线程池来运行应用程序,并观察CPU利用率的水平。

        当然,CPU周期并不是唯一影响线程池大小的资源,还包括内存、文件句柄、套接字句柄和数据库连接等。计算这些资源对线程池的约束条件是更容易的:计算每个任务对该资源的需求量,然后用该资源的可用总量除以每个任务的需求量,所得结果就是线程池大小的上限。

        当任务需要某种通过资源池来管理的资源时,例如数据库连接,那么线程池和资源池的大小将会相互影响。如果每个任务都需要一个数据库连接,那么连接池的大小就限制了线程池的 大小。同样,当线程池中的任务是数据库连接的唯一使用者时,那么线程池的大小又将限制连 接池的大小。

配置ThreadPoolExecutor

        ThreadPoolExecutor为一些Executor提供了基本的实现,这些Executor是由 Executors 中 的newCachedThreadPool、newFixedThreadPool和newScheduledThreadExecutor等工厂方法返回的。 ThreadPoolExecutor是一个灵活的、稳定的线程池,允许进行各种定制。

        如果默认的执行策略不能满足需求,那么可以通过 ThreadPoolExecutor的构造函数来实例化一个对象,并根据自己的需求来定制,并且可以参考Executors的源代码来了解默认配置下的执行策略, 然后再以这些执行策略为基础进行修改。ThreadPoolExecutor定义了很多构造数, 在程序清单8-2中给出了最常见的形式。


线程的创建与销毁

        线程池的基本大小(Core Pool Size)、最大大小(Maximum Pool Size)以及存活时间等因素共同负责线程的创建与销毁。 基本大小也就是线程池的目标大小, 即在没有任务执行时线程池的大小, 并且只有在工作队列满了的情况下才会创建超出这个数益的线程。 线程池的最大大小表示可同时活动的线程数最的上限。 如果某个线程的空闲时间超过了存活时间, 那么将被标记为可回收的, 并且当线程池的当前大小超过了基本大小时, 这个线程将被终止。

        通过调节线程池的基本大小和存活时间, 可以帮助线程池回收空闲线程占有的资源,从而使得这些资源可以用于执行其他工作。(显然, 这是种折衷: 回收空闲线程会产生额外的延迟, 因为当需求增加时, 必须创建新的线程来满足需求。)

        newFixedThreadPool工厂方法将线程池的基本大小和最大大小设置为参数中指定的值, 而且创建的线程池不会超时。newCachedThreadPool工厂方法将线程池的最大大小设置为Integer. MAX_VALUE, 而将基本大小设置为零, 并将超时设置为1分钟, 这种方法创建出来的线 程池可以被无限扩展, 并且当需求降低时会自动收缩。其他形式的线程池可以通过显式的 ThreadPoolExecutor构造函数来构造。

管理队列任务

        在有限的线程池中会限制可并发执行的任务数量。(单线程的Executor是一种值得注意的特例:它们能确保不会有任务并发执行, 因为它们通过线程封闭来实现线程安全性。)

         如果无限制地创建线程, 那么将导致不稳定性, 并通过采用固定大小的线程池(而不是每收到一个请求就创建一个新线程 )来解决这个问题。然而,这个方案并不完整。在高负载情况下, 应用程序仍可能耗尽资源, 只是出现问题的概率较小。 如果新请求的到达速率超过了线程池的处理速率, 那么新到来的请求将累积起来。在线程池中,这些请求会在一个由 Executor管理的 Runnable 队列中等待,而不会像线程那样去竞争 CPU资源。 通过 一个 Runnable 和一个链表节点来表现一个等待中的任务, 当然比使用线程来表示的开销低很多, 但如果客户提交给服务器请求的速率超过了服务器的处理速率,那么仍可能会耗尽资源。

        即使请求的平均到达速率很稳定, 也仍然会出现请求突增的情况。 尽管队列有助于缓解任 务的突增问题,但如果任务持续高速地到来,那么最终还是会抑制请求的到达率以避免耗尽内存。甚至在耗尽内存之前,响应性能也将随着任务队列的增长而变得越来越糟。

        ThreadPoolExecutor 允许提供一个 BlockingQueue 来保存等待执行的任务。 基本的任务排队方法有 3 种: 无界队列、有界队列和同步移交 (Synchronous Handoff)。队列的选择与其他的配置参数有关,例如线程池的大小等。

        newFixedThreadPool 和 newSingleThreadExecutor在默认情况 下将使用一个无界的 LinkedBlockingQueue。如果所有工作者线程都处于忙碌状态, 那么任务将在队列中等候。如果任务持续快速地到达, 并且超过了线程池处理它们的速度, 那么队列将无限制地增加。

        一种更稳妥的资源管理策略是使用有界队列,例如 ArrayBlockingQueue、有界的LinkedBlockingQueue、 PriorityBlockingQueue。有界队列有助于避免资源耗尽的情况发生, 但它又带来了新的问题: 当队列填满后,新的任务该怎么办? (有许多饱和策略 (Saturation Policy] 可以解决这个问题。请参见 8.3.3 节。)在使用有界的工作队列时,队列的大小与线程池的大小必须一起调节。如果线程池较小而队列较大,那么有助于减少内存使用量,降低 CPU 的使用率,同时还可以减少上下文切换, 但付出的代价是可能会限制吞吐量

        对于非常大的或者无界的线程池,可以通过使用 SynchronousQueue 来避免任务排队以及直接将任务从生产者移交给工作者线程。 SynchronousQueue 不是一个真正的队列, 而是一 种在线程之间进行移交的机制。 要将一个元素 放入 SynchronousQueue 中, 必须有另一个线程正在等待接受这个元素。如果没有线程正在等待, 并且线程池的当前大小小于最大值, 那么ThreadPoolExecutor 将创建一个新的线程, 否则根据饱和策略, 这个任务将被拒绝。 使用直接移交将更高效, 因为任务会直接移交给执行它的线程, 而不是被首先放在队列中,然后由工作 者线程从队列中提取该任务。 只有当线程池是无界的或者可以拒绝任务时, SynchronousQueue才有实际价值。在 newCachedThreadPool 工厂方法中就使用了 SynchronousQueue。

        当使用像 LinkedBlockingQueue 或 ArrayBlockingQueue 这样的 FIFO(先进先出)队列时, 任务的执行顺序与它们的到达顺序相同。如果想进一步控制任务执行顺序, 还可以使用PriorityBlockingQueue, 这个队列将根据优先级来安排任务。任务的优先级是通过自然顺序或Comparator (如果任务实现了Comparable)来定义的。

        只有当任务相互独立时, 为线程池或工作队列设置界限才是合理的。如果任务之间存在依赖性, 那么有界的线程池或队列就可能导致线程 ” 饥饿” 死锁问题。此时应该使用无界的线程 池, 例如newCachedThreadPool

饱和策略

        当有界队列被填满后, 饱和策略开始发挥作用。ThreadPoolExecutor 的饱和策略可以通过调用setRejectedExecutionHandler 来修改。(如果某个任务被提交到一个巳被关闭的Executor时,也会用到饱和策略。) JDK提供了几种不同的RejectedExecutionHandler实现,每种实现都包含有不固的饱和策略: AbortPolicy、CallerRunsPolicy、DiscardPolicy和DiscardOldestPolicy。

        "中止(Abort)"策略是默认的饱和策略,该策略将抛出未检查的RejectedExecution­-Exception。调用者可以捕获这个异常,然后根据需求编写自己的处理代码。当新提交的任务法保存到队列中等待执行时,“抛弃(Discard)"策略会悄悄抛弃该任务。 “抛弃最旧的( Discard-Oldest)"策略则会抛弃下一个将被执行的任务, 然后尝试重新提交新的任务。(如果工 作队列是一个优先队列, 那么 “抛弃最旧的” 策略将导致抛弃优先级最高的任务, 因此最好不要将 “抛弃最旧的" 饱和策略和优先级队列放在一起使用。)

        “调用者运行(Caller-Runs)"策略实现了一种调节机制, 该策略既不会抛弃任务, 也不会抛出异常, 而是将某些任务回退到调用者, 从而降低新任务的流量。 它不会在线程池的某个线程中执行新提交的任务, 而是在一个调用了execute的线程中执行该任务。 我们可以将WebServer示例修改为使用有界队列和 “ 调用者运行” 饱和策略, 当线程池中的所有线程都被占用, 并且工作队列被填满后, 下一个任务会在调用execute 时在主线程中执行门 由于执行任 务需要一定的时间, 因此主线程至少在一段时间内不能提交任何任务, 从而使得工作者线程有时间来处理完正在执行的任务。在这期间, 主线程不会调用accept, 因此到达的请求将被保存 在TCP层的队列中而不是在应用程序的队列中。 如果持续过载, 那么TCP层将最终发现它的 请求队列被填满, 因此同样会开始抛弃请求。 当服务器过载时, 这种过载情况会逐渐向外蔓延开来-从线程池到工作队列到应用程序再到TCP层, 最终达到客户端, 导致服务器在高负载下实现一种平缓的性能降低。

        当创建 Executor 时, 可以选择饱和策略或者对执行策略进行修改。 程序消单 8-3 给出了如何创建一个固定大小的线程池, 同时使用 “调用者运行” 饱和策略。

        当工作队列被填满后, 没有预定义的饱和策略来阻塞 execute 。然而, 通过使用 Semaphore(信号量)来限制任务的到达率,就可以实现这个功能。 在程序清单 8-4 的 BoundedExecutor中给出了这种方法。 该方法使用了一个无界队列(因为不能限制队列的大小和任务的到达率), 并设置信号量的上界设置为线程池的大小加上可排队任务的数量, 这是因为信号量需要控制正在执行的和等待执行的任务数量。

线程工厂

        每当线程池需要创建一个线程时, 都是通过线程工厂方法(请参见程序清单8-5)来完成的。默认的线程工厂方法将创建一个新的、非守护的线程, 并且不包含特殊的配置信息。通过指定一个线程工厂方法, 可以定制线程池的配置信息。在ThreadFactoi;y 中只定义了一个方法newThread, 每当线程池需要创建一个新线程时都会调用这个方法。

        然而, 在许多情况下都需要使用定制的线程工厂方法。例如, 你希望为线程池中的线程指定一个UncaughtExceptionHandler, 或者实例化一个定制的Thread 类用于执行调试信息的记录。你还可能希望修改线程的优先级(这通常并不是一个好主意。请参见10.3.1节)或者守护状态(同样, 这也不是一个好主意。请参见7.4.2节)。或许你只是希望给线程取一个更有意义的名称, 用来解释线程的转储信息和错误日志。


        在程序清单8-6 的MyThreadFactory 中给出了一个自定义的线程工厂。它创建了一个新的My App Thread 实例, 并将一个特定千线程池的名字传递给MyAppThread 的构造函数, 从而可以在线程转储和错误日志信息中区分来自不同线程池的线程. 在应用程序的其他地方也可以使用MyAppThread, 以便所有线程都能使用它的调试

        在MyApp Thread 中还可以定制其他行为, 如程序清单8-7所示,包括: 为线程指定名字,设置自定义UncaughtExceptionHandler 向Logger 中写入信息, 维护一些统计信息(包括有多少个线程被创建和销毁), 以及在线程被创建或者终止时把调试消息写入日志。

        如果在应用程序中需要利用安全策略来控制对某些特殊代码库的访问权限, 那么可以通过 Executor 中的 privilegedThreadFactory 工厂来定制自己的线程工厂。 通过这种方式创建出来的 线程, 将与创建 privilegedThreadFactory 的线程拥有相同的访问权限、 AccessControlContext 和 contextClassLoader。 如果不使用 privilegedThreadFactory, 线程池创建的线程将从在需要新 线程时调用 execute 或 submit 的客户程序中继承访问权限, 从而导致令人困惑的安全性异常。

在调用构造函数后再定制 ThreadPoolExecutor

        在调用完 ThreadPoolExecutor 的构造函数后, 仍然可以通过设置函数 (Setter) 来修改大多数传递给它的构造函数的参数(例如线程池的基本大小、 最大大小、 存活时间、 线程工厂以及拒绝执行处理器 (Rejected Execution Handler)) 。 如果Executor 是通过 Executors 中 的某个 (newSingleTbreadExecutor 除外)工厂方法创建的, 那么可以将结果的类型转换为 ThreadPoolExecutor 以访问设置器, 如程序清单 8-8 所示。

        在 Executors中包含一个 unconfiurableExecutorService 工厂方法, 该方法对一个现有的 ExecutorService 进行包装, 使其只暴露出 ExecutorService 的方法, 因此不能对它进行配置。 newSingleThreadExecutor 返回按这种方式封装的 ExecutorService, 而不是最初的 ThreadPoolExecutor。虽然单线程的 Executor 实际上被实现为一个只包含唯一线程的线程池,但它同样确保了不会并发地执行任务。如果在代码中增加单线程 Executor 的线程池大小, 那么将破坏它的执行语义。

        你可以在自己的 Executor 中使用这项技术以防止执行策略被修改。如果将 ExecutorService 暴露给不信任的代码, 又不希望对其进行修改,就可以通过 unconfigurableExecutorService 来 包装它。·

展ThreadPoolExecutor

        ThreadPoolExecutor 是可扩展的, 它提供了几个可以在子类化中改写的方法: beforeExecute、 afteExecute 和 terminated, 这些方法可以用于扩展 ThreadPoolExecutor 的行为。

        在执行任务的线程中将调用 beforeExecute 和 afterExecute 等方法, 在 这些方法中还可以添加日志、计时、监视或统计信息收集的功能。无论任务是从 run 中正常返回,还是抛出一个 异常而返回, afterExecute 都会被调用。(如果任务在完成后带有一个 Error, 那么就不会调用 after Execute。)如果 beforeExecute 抛出一个 RuntimeException, 那么任务将不被执行, 并且 afterExecute 也不会被调用。

        在线程池完成关闭操作时调用 terminated, 也就是在所有任务都已经完成并且所有工作者 线程也巳经关闭后。 terminated 可以用来释放 Executor 在其生命周期里分配的各种资源, 此外还可以执行发送通知、 记录日志或者收集 finalize 统计信息等操作。、

递归算法的并行化

        我们对6.3节的页面绘制程序进行了一系列的改进以便不断发掘可利用的并行性。 第一次是使程序完全串行执行, 第二次虽然使用了两个线程, 但仍然是串行地下载所有图像:在最后一次实现中将每个图像的下载操作视为一个独立任务, 从而实现了更高的并行性。 如果在循环体中包含了 些密集计算, 或者需要执行可能阻塞的I/0操作, 那么只要每次迭代是独立的, 都可以对其进行并行化。

        如果循环中的迭代操作都是独立的, 并且不需要等待所有的迭代操作都完成再继续执行,那么就可以使用Executor将串行循环转化为并行循环, 在程序清单8-10的processSequentially 和processlnParallel中给出了这种方法。

        调用 processlnParallel 比调用 processSequentially 能更快地返回, 因为 processinParallel 会 在所有下载任务都进入了Executor 的队列后就立即返回, 而不会等待这些任务全部完成。 如果需要提交一个任务集并等待它们完成, 那么可以使用 ExecutorService.invokeAll, 并且在所有任 务都执行完成后调用 CompletionService 来获取结果,如第 6 章的 Renderer 所示。

        当串行循环中的各个迭代操作之间彼此独立, 并且每个迭代操作执行的工作篮比管理一个 新任务时带来的开销更多, 那么这个串行循环就适合并行化。

小结

        对于井发执行的任务,Executor框架是一种强大且灵活的框架。 它提供了大量可调节的选项, 例如创建线程和关闭线程的策略, 处理队列任务的策略, 处理过多任务的策略, 井且提供了几个钓子方法来扩展它的行为。 然而, 与大多数功能强大的框架一样, 其中有些设置参数并不能很好地工作,某些类型的任务需要特定的执行策略, 而 些参数组合则可能产生奇怪的结果。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容