编程思想 -- 第21章 -- 并发

并发

并行编程可以让程序执行速度得到极大提高,或者为设计某些类型的程序提供更易用的模型,或者两者皆有。了解并发可以使你意识到明显正确的程序可能会展示出不正确的行为。理解并发编程,其难度与理解面向对象编程差不多。

一、并发的多面性

用并发解决的问题大体上可以分为速度和设计可管理型两种。

1,更快的执行:

并发是用于多处理器编程的基本工具,并发通常是提高运行在单处理器上的程序的性能。事实上,从性能角度看,如果没有任务会阻塞,那么在单处理器上使用并发就没有任何意义。

在单处理器系统中的性能提高的常见示例是事件驱动的编程。实现并发最直接的方式就是在操作系统级别使用进程。进程是运行在它自己的地址空间内的自包容的程序。多任务操作系统可以通过周期性地将CPU从一个进程切换到另一个进程,来实现同时运行多个进程,尽管这使得每个进程看起来在执行过程中都是歇歇停停。

在多任务操作系统,可以将每个复制操作当作单独的进程来启动,并让它们并行地运行,这样可以加速整个程序的执行速度,当一个进程受阻时,另一个进程可以继续前进。这是并发的理想示例

每个任务都作为进程在其自己的地址空间中执行,因此任务之间根本不可能相互干涉,更重要的是,对进程来说,它们之间没有任何彼此通信的需要,因为它们都是完全独立的。操作系统会处理确保文件正确复制的所有细节,因此,不会有任何风险,你可以获得更快的程序,并且完全免费。

某些编程语言被设计为可以将并发任务彼此隔离,这些语言通常被称为函数型语言,其中每个函数调用都不会产生任何副作用,并因此可以当做独立的任务来驱动。如果程序必须大量使用并发,可以考虑ErLang这类专门的并发语言来创建这个部分。

2,改进代码设计

并发听过一个重要的结构上的好处:你的程序设计可以极大简化。某些类型的问题,没有并发的支持很难解决,如仿真。java的线程机制是抢占式的,这表示调度机制会周期性地中断线程,将上下文切换到另一线程,从而为每个线程都提供时间片,使得每个线程都会分配到数量合理的时间去驱动它的任务。

协作式系统的优势是双重的:上下文切换的开销通常比抢占式系统要低廉,并且可以同时执行的线程数量在理论上没有任何限制。

并发需要代价,包含复杂性代价,但是这些代价与在程序设计,资源负载均衡以及用户方便使用方面的改进相比,显得微不足道。通常,线程使你能够创建更加松散耦合的设计,否则代码中各个部分必须显示地关注通常由线程来处理的任务。

二、基本的线程机制

并发编程使我们将程序划分为多个分离的,独立运行的任务。通过使用多线程机制,这些独立任务中的每一个都将由执行线程来驱动。线程简化了在单一程序中同时交织在一起的多个操作的处理。使用线程机制是一种建立透明的可扩展的程序的方法。多任务和多线程往往是使用多处理器系统的最合理的方式。

1,定义任务。

线程可以驱动任务,因此需要一种描述任务的方式,可以由Runnable接口来提供。定义惹怒我,实现Runnable接口并编写run()方法,使得该任务可以执行你的命令。当从Runnable导出一个类时,它必须具有Run()方法。要实现线程行为,你必须显示地将一个任务附着到线程上。

2,Thread类

将Runnable对象转变为工作任务的传统方式是把它提交给一个Thread构造器。Thread构造器只需要一个Runnable对象,调用Thread对象的start()方法为该线程执行必需的初始化操作,然后调用Runnable的run()方法,以便在新线程中启动该任务。一个线程会创建一个单独的执行线程,在对start()调用完成后,它仍旧会存在。

使用Thread时,每个Thread先注册了自己,确保有一个对它的引用,而且在它的任务退出其run()并死亡之前,垃圾回收器无法清楚它。

3,使用Executor

java.util.concurrent包中的执行器(Executor)将为你管理Thread对象,从而简化了并发编程。Executor允许你管理异步任务的执行,而无须显式得管理线程的生命周期,Executor是启动任务的优选方法。与命令模式一样,它暴露了要执行的单一方法。单个的Executor被用来创建和管理系统中所有的任务。

ExecutorService是使用静态的Executor方法创建的,shutdown()方法防止新任务交给这个Executor,当前线程将继续运行在shutdown()被调用之前提交的所有任务。

CachedThreadPool()将为每个任务都创建一个线程,创建于所需数量相同的线程,然后它回收旧线程时停止创建新线程。FixedThreadPool()使用了有限的线程集来执行所提交的任务,可以一次性预先执行代价高昂的线程分配,可以限制线程的数量。SingleThreadExecutor()是线程数量为1的FixedThreadPool(),提交多个任务时,将任务排队,每个任务结束前运行,所有任务使用相同线程,它序列化所有提交的任务,并维护它的悬挂任务队列。

4,从任务中产生返回值

Runnable是执行工作的独立任务,但是它不返回任何值。如果希望返回一个值,可以实现Callable接口而不是Runnable接口。Callable是一种具有类型参数的泛型,它的类型参数表示从方法call()中返回的值,并且必须使用ExecutorService.submit()方法调用它。submit()方法产生Future对象,它用Callable返回结果的特定类型进行参数化,用isDone()方法查询Future是否已经完成。任务完成,调用get()来获取该结果。

5,休眠

影响任务行为的一种简单方法是调用sleep(),这将使任务中止执行给定的时间。对sleep()调用可以抛出InterruptedException异常,异常不能跨线程传播。所以你必须在本地处理所有在任务内部产生的异常。顺序行为依赖于底层的线程机制,这种机制在不同的操作系统之间是有差异的,因此你不能依赖它。

6,优先级

线程的优先级将该线程的重要性传递给了调度器。尽管CPU处理现有线程集的顺序是不确定的,但是调度器将倾向于让优先权最高的线程先执行,优先级较低的线程仅仅是执行的频率较低。可以使用getPriority()来读取现有线程的优先级,并自任何时刻都通过setPriority()来修改它。

7,让步

调用yield()时,是建议具有相同优先级的其他线程可以运行。代表可以让别的线程使用CPU,不代表被采纳执行。

8,后台线程

所谓后台线程,是指在程序运行的时候在后台提供一种通用服务的线程,并且这种线程不属于程序中不可或缺的部分。当所有的非后台线程结束时,程序就终止了,同事会杀死进程中的所有后台线程,只要有任何非后台线程还在运行,程序就不会终止。

9,编码的变体

任务类除了实现Runnable接口外,在非常简单的情况下,你可以直接从Thread继承这种可替换的方式。

10,术语

Thread类自身不执行任何操作,它只是驱动赋予它的任务,Runnable接口类执行能做的事情。

11,加入一个线程

一个线程可以在其它线程上调用join()方法,效果是等待一段时间直到第二个线程结束才继续执行。join()方法可以被中断,在线程调用中调用interrupt()方法。要实现线程行为,你必须显示地将一个任务附着到线程上。

12,创建有响应的用户界面

使用线程的动机就是建立一个有响应的用户界面。

13线程组

线程组持有一个线程集合。(最好把线程组看成一次不成功的尝试,你只要忽略它就好了)

14,捕获异常

由于线程的本质特征,使得你不能捕获从线程中逃逸的异常。Thread.UncaughtExceptionHandler接口,允许你在每个Thread对象对附着一个异常处理器。

三、共享受限资源

可以把单线程程序当作在问题域求解的单一实体,每次只能做一件事情。有了并发可以同时做多件事情,但是两个或多个线程彼此相互干涉的问题就出现了。

1,不正确的访问资源

共享公共资源,消除所谓竞争条件,即两个或更多的任务竞争响应某个条件,因此产生冲突或不一致结果的情况。

2,解决共享资源竞争

使用线程的一个基本问题:你永远不知道一个线程何时在运行。线程被挂起是你在编写并发编程时需要处理的问题。对于并发工作,你需要某种方式防止两个任务访问相同资源,至少关键阶段不能出现这种状况。

防止这种冲突的办法是当资源被一个任务使用时,为其加锁。解锁时,另一个任务可以锁定并使用它。基本上所有的并发模式在解决线程冲突问题的时候,都是采用序列化访问共享资源的方案,这意味着在给定的时刻只允许一个任务访问资源,通常是在代码前加一条锁语句实现,使得一端时间内只有一个任务可以运行这段代码,锁语句实现了互相排斥的效果,这种机制常常称为互斥量。

java提供synchronized的形式,为防止资源冲突提供内置支持。当任务要被synchronied关键字保护的代码片段时,它将检查代码是否可用,然后获取锁,执行代码,释放锁。在当前线程从该方法返回之前,其他所有调用类中任何标记为synchronized方法的线程都会被阻塞。

注意:在使用并发时,将域设置为private是非常重要的,否则,synchronized关键字就不能防止其它任务直接访问域,这样就会产生冲突。JVM负责跟踪对象被加锁的次数,对象被解锁,计数变为0,对象加锁,计数变为1,每当相同的任务在这个对象上获得锁时,计数都会递增,当任务离开synchronized方法,计数递减,计数为零的时候,锁被完全释放,别的任务就可以使用此资源。

如果在你的类中有超过一个方法处理临街数据,你必须同步所有相关的方法。每个访问临界共享资源的方法都必须被同步,否则它们就不会正确地工作。

java.util.concurrent.locks中的显式互斥机制。Lock对象必须被显式地创建,锁定和释放,它与内建的锁形式相比,代码缺乏优雅性,但对于某些类型的问题来说,它更加灵活。

大体上,当你使用synchronized关键字时,需要写的代码量更少,并且用户错误出现的可能性也会降低,因此通常只有在解决特殊问题时,才使用显式的Lock对象。显式的Lock对象在加锁和解锁方面,相对于synchronized锁来说,还赋予更细粒度的控制力,这对于实现专有同步结构是很有用的。

3,原子性与易变性

有关java线程的讨论中,一个常不正确的知识是:原子操作不需要进行同步控制。原子操作是不能被线程调度机制中断的操作。一旦操作开始,那么它一定可以在可能发生的上下文切换之前执行完毕。原子操作可由线程机制来保证其不可中断。应用中不具备用原子替换同步的能力。

相对于单处理器系统而言,可视性问题远比原子性问题多得多。volatile关键字还确保了应用中的可视性,如果将一个域声明为volatile的,那么只要对这个域产生了写操作,那么所有的读操作都可以看到这个修改。如果一个域完全由synchronized方法或语句块来防护,那就不必将其设置为是volatile的。

使用volatile而不是synchronized的唯一安全情况是类中只有一个可变的域。如果你将域定义为volatile,它会告诉编译器不要执行任何移除读取和写入操作的优化,这些操作的目的是用线程中的局部变量维护对这个域的精准同步,实际上,读取和写入都是直接针对内存的,而却没有缓存,但是volatile并不能对递增不是原子性操作这一事实产生影响。

4,原子类

Java中如AtomicInteger、AtomicLong、AtomicReference等特殊的原子性变量类,在使用它们时,通常不需要担心,涉及到性能调优时,它们就大有用武之地。Athomic类被设计用来构建java.util.concurrent中的类,通常头特殊情况下才使用它们,使用了也要确保不存在可能出现的问题,通常依赖于锁更安全些。

5,临界区

有时,你只是希望防止多个线程同时访问方法内部的部分代码而不是防止访问整个方法。通过这种方式分离出来的代码段被称为临界区,它也是使用synchronized关键字建立。这也被称为同步代码块。可以使多个任务访问对象的时间性能得到显著提高。

6,在其他对象上同步

synchronized块必须给定一个在其上进行同步的对象,并且最合理的方式是,使用其方法正在被调用的当前对象:synchronized(this)。如果在this上同步,临界区的效果就会直接缩小在同步的范围内。

7,线程本地存储

防止任务在共享资源上产生冲突的第二种方式是根除对变量的共享。线程本地存储是一种自动化机制,可以为使用相同变量的每个不同的线程都创建不同的存储。ThreadLoca对象通常当做静态域存储。

四、终结任务

某些情况下,任务必须突然终止。

1,装饰性花园

通过控制同步代码块内部计数的方式来统计线程数量,并以此进行中断,让步。

2,在阻塞时终结

sleep()使任务从执行状态变为被阻塞状态,而有时你必须中止被阻塞的任务,或者强制任务跳出阻塞状态。

线程状态:新建(new)-->就绪(Runnable)-->阻塞(Blocked)-->死亡(Dead)

进入阻塞状态:原因如下: 1,调用sleep()使任务进入休眠。 2,通过wait()使线程挂起。 3,任务在等待某个输入输出完成。    4,任务试图调用同步控制方法,但另一个任务获取此锁。

stop()方法已被废除,因为它不释放线程获得的锁。suspend()和resume()用来阻塞和唤醒线程,但是已被废止,因为可能导致死锁。

3,中断

当你打断被阻塞的资源时,可能需要清理资源,在任务的run()方法中间打断,更像是抛出的异常,因此在java线程中的这种类型的异常中断中用到了异常。Thread类包含interrupt()方法,因此可以终止被阻塞的任务,将方法设置线程的中断状态,可以打断被互斥所阻塞的调用。

4,检查中断

当你在线程上调用interrupt()时,中断发生的唯一时刻是在任务要进入到阻塞操作中,或者已经在阻塞操作内部时(除了不饿中断的I/O或被阻塞的synchronized方法之外)。

被设计用来响应interrupt()的类必须建立一种策略,来确保它将保持一致的状态。这通常意味着所有需要创建的对象操作的后面,都必须紧跟try-finally字句,从而使得无论run()循环如何退出,清理都会发生。

五、线程之间的协作

当你使用线程来同时运行多个任务时,可以通过使用锁来同步两个任务的行为,从而使得一个任务不会干涉另一个任务的资源。当任务写作时,关键问题是这些任务之间的握手。为了实现这种握手,我们使用了相同的基础特性:互斥。互斥能够确保只有一个任务可以响应某个信号,这样就可以根除任何可能的竞争条件。

1,wait()与notifyAll()

wait()使你可以等待某个条件发生变化,而改变这个条件超出了当前方法的控制能力。wait()会在外部世界产生变化的时候将任务挂起,并且只有在notify()或notifyAll()发生时,表示发生了某些感兴趣的事物,这个任务才会被唤醒并去检查所产生的变化,因此wait()提供了一种在任务之间对活动同步的方式。

调用sleep()的时候,锁并没有被释放,调用yield()也属于这种情况。当任务在方法里对wait()的调用,线程的执行被挂起,对象上的锁被释放,另外一个任务可以获得这个锁,因此在改对象中的其他synchronized方法可以在wait()期间被调用。

与sleep()不同,对wait()而言,1,在wait()期间对象所都是释放的。2,可以通过notify(),notifyAll()或者令时间到期,从wait()中恢复执行。

wait(),notify()和notifyAll()有一个比较特殊的方面,这些方法是基类Object的一部分,而不是属于Thread的一部分。实际上,只能在同步代码块里调用wait(),notify()和notifyAll()。消息的意思是:调用wait(),notify()和notifyAll()的任务在调用这些方法前必须拥有对象的锁。可以让另一个对象执行某种操作以维护自己的锁,要做么做的话,必须首先得到对象的锁。

当连个线程使用notify()/wait()或notifyAll()/wait()进行协作时,有可能或错过某个信号。可以在开启线程的时候通过设置竞争条件控制流程。

2,notify()和notifyAll()

在技术上,可能会有多个任务在单个对象上处于wait()状态,因此调用notifyAll()比notify()更安全。如果只有一个任务处于wait()的状态,可以使用notify()代替notifyAll()。

3,生产者与消费者

对于一个任务而言,只有一个单一的地点用于存放对象,从而使得另一个任务稍后可以使用这个对象。但是在典型的生产者-消费者实现中,应使用先进先出队列来存储被生产和消费的对象。

使用显示的Lock和Condition对象:使用互斥并允许任务挂起的基本类是Condition,你可以通过Condition上调用await()来挂起一个任务,当外部条件发生变化,以为着某个任务应该继续执行时,你可以通过signal()来通知这个任务,从而唤醒一个任务,或者调用signalAll()来唤醒所有在这个Condition上被其自身挂起的任务。Lock和Condition对象只有在更加困难的对现场问题中才是必需的。

4,生产者-消费者与队列

wait()和notifyAll()方法以一种非常低级的方式解决了任务互操作问题,即每次互交时都握手。你可以使用同步队列来解决任务协作问题,同步队列在任何时刻都只允许一个任务插入或移除元素。在java.util.concurrent.BlockingQueue接口中提供了这个队列,这个接口有大量的标准实现,你可以使用LingkedBlockingQueue是一个无届队列,ArrayBlockingQueue具有固定尺寸,你可以在被阻塞之前,向其中防止有线数量的元素。

阻塞队列可以解决非常大量的问题,而其方式与wait()和notifyAll()相比,则简单并可靠的多。

5,任务间使用管道进行输入输出

通过输入输出在线程间进行通信通常很有用。提供线程功能的类库以管道的形式对线程简的输入输入提供了支持。在java输入输出对应物是PipedWriter类和PipedReader类。管道基本上是一个阻塞队列,存在于BlockingQueue的版本中。

六、死锁

一个任务之间相互等待的连续循环,没有哪个线程能继续,称为死锁。

满足四个条件发生死锁:1,互斥条件,任务使用的资源中至少有一个是不能共享的。2,至少有一个任务它必须持有一个资源且在等待一个当前被别的任务持有的资源。3,资源不能被任务抢占,任务必须把资源释放当做普通事件。4,必须有循环等待。

java对死锁并没有提供语言层面上的支持,能否通过仔细设计程序来避免死锁,这取决于你自己。

七、新类库中的构件

java.util.concurrent引入了大量设计来解决并发的问题的新类,学习使用它们将有助于编写更加简单而健壮的并发程序。

1,CountDownLatch

它被用来同步一个或多个任务,强制它们等待由其他任务执行的一组操作完成。可以向CountDownLatch的任务在产生这个电泳时并没有被阻塞,只有对await()的调用会被阻塞,直至计数值到达0。Random.nextInt()是安全的。

2,CyclicBarrier

CyclicBarrier适用于:创建一组任务,并行执行工作,然后在进行下一个步骤前等待,直至所有任务都完成,它使得所有并行任务都将在栅栏处列队,因此可以一致地向前移动。CountDownLathc只触发一次,而ByclicBarrier可以多次重复使用。

3,DelayQueue

这是一个无界的BlckingQueue,用于放置实现了Delayed接口的对象,其中的对象只能在其到期时才能从队列中取走。这种队列是有序的,即对头对象的延迟到期的时间最长。如果没有任何延迟到期,那么就不会有任何头元素,并且poll()将放回null。

4,PriorityBlockingQueue

这是一个很基础的优先级队列,它具有可阻塞的读取操作。在优先级队列中的对象是按照优先级顺序从队列中出现的任务。

5,使用ScheduledExecutor的温室控制器

可以应用于假想温室的控制系统的实例,可以控制各种设施的开关,或者是对它们进行调节。这被看作是一种并发问题,每个期望的温室时间都是在一个预定时间运行的任务。

6,Semaphore

正常的锁在任何时刻都只允许一个任务访问一项资源,而技术信号量允许n个任务同时访问这个资源。可以将信号量看作是在向外分发使用资源的许可证。考虑对象池的概念,它管理着数量有限的对象,使用对象时签出它们,使用完毕后,将它们签回。

7,Exchanger

Exchanger是在两个任务之间交换对象的栅栏。任务进入栅栏时,各自拥有一个对象,它们离开时,它们都拥有之前对象持有的对象。典型的应用场景是:一个任务在创建对象,这些对象的生产代价很高昂,另一个任务在消费这些对象,通过这种方式,可以有更多对象在被创建的同时被消费。

八、仿真

并发最有趣也是最令人兴奋的用法就是创建仿真,通过使用并发,仿真的每个构建都可以成为其自身的任务,这使得仿真更容易编程。

1,银行出纳仿真

这个经典的仿真可以表示任务号属于下面这种类型的情况:对象随机出现,并且要求由数量有限的服务器提供随机数量的服务时间。通过构建仿真可以确定理想的服务器数量。

2,饭店仿真

这个仿真添加了更多的仿真组件。

3,分发工作

烤炉假想汽车的组装线,每个汽车都分多个阶段构建,从创建底盘开始,紧跟着安装发动机,车厢,轮子。

九、性能调优

java.util.concurrent类存在着数量庞大的用于性能提高的类。

Java包括老式的synchronized关键字和Lock类和Atomic类,那么比较这些不同的方式,跟过理解它们各自的价值和使用范围,就会显得很有意义。使用Lock通常要比synchronized要高效的多,而且synchronized的开销看起来变化范围太大,而Lock相对比较一致。

因此从synchronized关键字入手,只有在性能调优时才替换为Lock对象的做法,是有实际意义的。

免锁容器:容器是所有编程中的基础工具,这其中也包括并发编程。对容器的修改可以与读取操作同事发生,只要读者只能看到完成修改的结果即可。

乐观锁:只要你主要是从免锁容器中读取,那么它就会比其synchronized对应物快许多,因为获取和释放锁的开销被省掉了。

乐观加锁:尽管Atomic对象执行原子性操作,但是某些Atomic类还允许执行所谓的乐观加锁。

ReadWriterLock是否能够提高程序的性能是完全不可确定的,它取决于诸如数据被读取的频率与被修改的频率相比较的结果,读取和写入操作的时间,有多少线程竞争以及是否在多处理机器上运行等因素。

十、活动对象

尽管多个任务可以并行工作,但是你必须花很大的利器去实现防止这些任务彼此相互干涉的技术。

有一种可替换的方式被称为活动对象或行动者。之所以称为是活动的,因为每个对象都维护着它自己的工作器线程和消息队列,并且所有对这种对象的请求都将进入队列排队,任何时刻都只能运行其中一个。有了活动对象,就可以串行化消息而不是方法,这意味着不再需要防备一个任务在其循环的中间被中断这种问题。

活动对象:1,每个对象都可以有自己的工作期线程。2,每个对象都将维护对它自己的域的全部控制权。3,所有在活动对象之间的通信都将以在这些对象之间的消息形式发生。4,活动对象之间的所有消息都要排队。

十一、总结

本章的目标是想你提供使用Java线程进行并发程序设计的基础知识以使你理解:1,可以运行多个独立的任务,2,必须考虑这些任务关闭时,可能出现的所有问题。3,任务可能会在共享资源上彼此干涉,互斥是用来防止这种冲突的基本工具。4,如果任务设计的不够仔细,就有可能发生死锁。

明白什么时候应该使用并发,什么时候应该避免使用并发是非常关键的。使用它的主要原因是:

1,要处理很多任务,它们交织在一起,应用并发可以更有效地使用计算机。2,要能够更好地组织代码。3,要更便于用户使用。

线程的一个额外好处是它们提供了轻量级的执行上下文切换,而不是重量级的进程上下文切换。因为一个给定进程内的所有线程共享相同的内存空间,轻量级的上下文切换只是改变程序的执行序列和局部变量。进程切换必须改变所有内存空间。

多线程的主要缺陷:1,等待共享资源的时候性能降低。2,需要处理线程的额外CPU花费。3,糟糕的程序设计导致不必要的复杂度。4,有可能产生一些病态行为,如饿死,竞争,死锁和活锁。5,不同平台导致的不一致性。

因为多线程可能共享一个资源,比如一个对象的内存,而且你必须确定多线程不会同时读取和改变这个资源,这是线程产生的最大难题。这需要明智使用可用的加锁机制,它们会引入潜在的死锁条件,要对它们有透彻的理解。

线程的应用上也有一些技巧,java允许你建立足够都的对象来解决你的问题。然而,你创建的线程数目要有个上界,因为达到了一定的数量,线程性能会很差。这个临界点很难监测,通常依赖于操作系统和JVM;不过我们通常创建少数线程来解决问题,所以这个限制并不严重。

通常使用线程机制需要非常仔细和保守。如果你的线程变得大而复杂,那么就应该考虑使用ErLang这样的语言,这是专门用于线程机制的几种函数型语言之一。

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

推荐阅读更多精彩内容