Java多线程技术之七(JUC之锁框架)

一、基本概念

锁是控制多个线程对共享资源进行访问的工具,对共享资源的所有访问都需要首先获得锁。关于锁有许多概念,现在作一个总结。

内置锁

关键字synchronized实现的锁。

独占锁/互斥锁/排他锁

一次只能有一个线程获得锁,所有对共享资源的访问都需要首先获得锁。

共享锁

该锁可被多个线程所持有。

读写锁

读写锁将对一个共享资源的访问分成了2个锁,一个读锁和一个写锁。读锁是共享锁,写锁是独占锁。

重入锁

如果锁具备重入性,则称该锁为重入锁。重入性是指一个线程对共享资源加锁后,这个线程可以再次对该共享资源加锁。例如,一个线程执行到对象的某个synchronized方法method1时,在method1中又调用了该对象的另外一个synchronized方法method2,线程可以进入方法method2执行。重入性说明锁是基于线程分配的,而不是基于方法调用分配的。因此,内置锁是重入锁。

公平锁/非公平锁

如果一个锁是公平的,那么线程获取锁的顺序符合线程请求锁的时间先后顺序,也就是先来先得。反之就是非公平的。内置锁是非公平锁。

可中断锁/不可中断锁

线程获取锁后或等待锁时可以被别的线程中断,这种就是可中断锁。内置锁是不可中断锁。

乐观锁/悲观锁

乐观锁与悲观锁并不是特指某两种具体的锁,是人们抽象出来的概念,是看待并发与同步的思想。

  • 乐观锁:就是乐观地估计自己读取的数据在稍后的一段时间内大概率不会被别人修改,所以不会对数据加锁,但是在更新的时候需要判断一下在此期间别人有没有修改这个数据,可以使用版本号等机制进行变更检测,如果检测出在此期间这个数据没有被别人修改,那么就可以更新数据。反之,这个过程需要重来一遍。乐观锁适用于读多写少的应用类型,这样可以提高吞吐量。
  • 悲观锁:就是悲观的认为自己读取的数据在稍后的一段时间内大概率会被别人修改,所以每次读取数据的时候都会对数据加锁,这样别人想操作这个数据就得等待直到它拿到锁。内置锁是悲观锁。

二、内置锁的升级过程

内置锁具有偏向锁、轻量级锁和重量级锁三个状态,这三种锁状态是通过在对象头中的Mark Word字段来表示的。Java通过锁升级机制来实现高效内置锁。内置锁初始时是偏向锁,随着锁竞争的加剧而逐步升级。

偏向锁

偏向锁是针对加锁操作的优化手段。在大多数情况下,锁是由同一线程多次获得,因此为了减少同一线程获取锁的代价而引入的偏向锁。如果一个线程获得了锁,那么就进入了偏向模式,此时同步对象的对象头中的Mark word的结构也就变为了偏向锁结构,当这个线程再次请求锁的时候,就不需要再做任何同步操作即可获取锁,省去了大量的有关锁的申请操作了。对于锁竞争不激烈的场合,偏向锁有很好的优化效果,从而就提高了程序的性能。但是对于锁竞争比较激烈的场合,偏向锁优化就失败了,因为这样的场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该再使用偏向锁,否则会得不偿失。偏向锁失败后,锁状态会升级为轻量级锁。

轻量级锁

偏向锁失败后,虚拟机会尝试使用一种称为轻量级锁的优化手段,此时同步对象的对象头中的Mark Word的结构会变为轻量级锁结构。轻量级锁只需要将Mark Word中的部分字节CAS更新指向线程的id,如果更新成功则表示已经成功的获取了锁。否则说明已经有线程获取了轻量级锁,发生了锁竞争,轻量级锁开始变成自旋锁。

自旋锁与自适应自旋锁

当轻量级锁失败之后,虚拟机为了避免线程在操作系统内核层面上被挂起,就尝试使用自旋锁的优化手段。在大多数情况下,线程持有锁的时间都不会太长,如果直接由操作系统内核实现线程之间切换需要从用户态转化为核心态,这个状态转换需要较长的时间,因此自旋锁会假设当前线程在经过很短的时间后就可以获得锁,因此虚拟机会让当前线程做几次空的循环,不断的测试是否可以获取锁。一般在经过短时间的的循环之后,如果得到了锁,就可以进入临界区。在JDK1.5中自旋锁设定为自旋十次,在JDK1.6中优化为根据临界区的代码来确定自旋次数,也就是自适应自旋锁。自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的时间消耗会小于线程挂起再唤醒的消耗。自旋锁是轻量级锁在锁膨胀前做的最后一次挣扎,如果自旋超过一定次数,或者此时有第三个线程来竞争该锁时,锁升级为重量级锁。

重量级锁

重重量级锁让等待锁的线程进入阻塞状态,当锁可用时才唤醒线程,因此重量级锁的进入时间和恢复时间都要比自旋锁略慢。重量级锁的优势是阻塞的线程不会占用CPU资源,在竞争激烈的情况下性能要高于自旋锁。

三、JUC中的锁

在内置锁之外,JUC提供了功能更强大更灵活的锁。JUC的锁框架包括:Lock接口、ReadWriteLock接口、Condition接口、ReentrantLock类,ReentrantReadWriteLock类、StampedLock类,这些是JUC对外提供的锁。除了这些接口和类,还有AbstractOwnableSynchronizer、AbstractQueuedSynchronizer、AbstractQueuedLongSynchronizer三个抽象类和LockSupport类,这些是构成JUC锁框架的基础,其中AbstractQueuedSynchronizer抽象类简称AQS。

JUC锁框架结构图

四、Lock 接口

一个根接口,是锁的抽象。

接口方法

// 获取锁,获取成功之前当前线程等待,等待状态不会被interrupt()方法中断
public void lock()

// 获取锁,获取成功之前当前线程等待,等待状态可以被interrupt()方法中断
public void lockInterruptibly() throws InterruptedException

// 尝试获取锁,如果获取成功,则返回true,如果获取失败,则返回false
boolean tryLock()

// 在指定时间期限内尝试获取锁,如果获取成功,则返回true,如果超时则返回false
public boolean tryLock(long time, TimeUnit unit)

// 释放锁
public void unlock();

// 创建一个绑定到此Lock对象的Condition对象
public Condition newCondition()

为防止线程产生死锁,调用lock()方法获取锁后,操作完共享资源必须释放锁。并且在发生异常时,不会自动释放锁。为了保证锁被释放,必须使用try…catch…finally语句进行锁操作。

// 获取锁
lock.lock();
try {
    // 操作共享资源
} catch (Exception e){

} finally {
    // 释放锁
    lock.unlock();   
}
重入锁 ReentrantLock

ReentrantLock类实现了Lock接口。它是一个可重入的互斥锁,行为和语义与synchronized类似。

构造方法

// 创建一个重入锁对象,默认为非公平锁
ReentrantLock()

// 创建一个重入锁对象,fair表示是否为公平锁
ReentrantLock(boolean fair)

使用示例

public class ReentrantLockDemo {
    public static int count = 0;
    public static Lock lock = new ReentrantLock();

    public static void main(String[] args) {
        for(int i = 0; i < 10; i++){
            new Thread(){
                @Override
                public void run() {
                    // 注意,lock必须是同一个实例
                    ReentrantLockDemo.lock.lock();
                    try {
                        System.out.println(Thread.currentThread().getName() + "得到了锁");
                        Thread.sleep(1000);
                        ReentrantLockDemo.count++;
                        System.out.println(Thread.currentThread().getName() + ":" + ReentrantLockDemo.count);
                    } catch (Exception e) {
                        e.printStackTrace();
                    } finally {
                        System.out.println(Thread.currentThread().getName() + "释放了锁");
                        // 在finally块中释放lock
                        ReentrantLockDemo.lock.unlock();
                    }
                }
            }.start();        
        }
    }
}
Condition接口

Condition是条件的抽象。在线程交互中,当条件改变时,线程之间通过一个Object对象的wait()方法和notify()/notifyAll()方法进行等待和通知,这两个方法必须处于synchronized代码块中,synchronized代码块关联了这个Object对象。类似的,Condition与Lock也是关联的。Lock接口的newCondition()方法创建一个与Lock关联的Condition对象,通过Condition对象可以实现线程之间的等待和通知,从而实现线程交互。不过与Object方式不同的是,一个Lock可以关联多个Condition。

接口方法

// 使当前线程在接收到通知或被中断前一直等待。调用前线程必须持有锁,调用后会释放锁
public void await()

// 使当前线程在接收到通知或被中断前等待最长指定的时间。调用前线程必须持有锁,调用后会释放锁
public boolean await(long time, TimeUnit unit)

// 使当前线程在接收到通知或被中断前等待最长指定的时间。调用前线程必须持有锁,调用后会释放锁
public long awaitNanos(long nanosTimeout)

// 使当前线程在接收到通知前一直等待,等待状态不响应中断。调用前线程必须持有锁,调用后会释放锁
public void awaitUninterruptibly()

// 使当前线程在接收到通知或被中断前等待最长至指定的时间点。调用前线程必须持有锁,调用后会释放锁
public boolean awaitUntil(Date deadline)

// 唤醒一个等待的线程。调用前线程必须持有锁
public void signal()

// 唤醒所有等待的线程。调用前线程必须持有锁
public void signalAll()

使用示例

public class ConditionDemo implements Runnable {
    public static Lock lock = new ReentrantLock();
    public static Condition condition = lock.newCondition();

    @Override
    public void run() {
        // 依赖一个锁对象
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "准备睡觉了");
            // 等待在condition上直到被唤醒
            condition.await();
            System.out.println(Thread.currentThread().getName() + "我醒了");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws Exception {
        Runnable task = new ConditionDemo();
        new Thread(task).start();
        Thread.sleep(5000);
        // 依赖一个锁对象
        lock.lock();
        System.out.println(Thread.currentThread().getName() + "起来吧");
        // 唤醒等待在condition上面的线程
        condition.signal();
        lock.unlock();
    }
}

五、ReadWriteLock 接口

ReadWriteLock接口抽象了读写锁。读写锁把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者只对共享资源进行写操作。它允许同时有多个读者来读取共享资源,而写者的操作是互斥的,一次只允许一个写者来操作共享资源,不能同时既有读者又有写者。由于互斥锁的所有操作都是串行化,而读写锁支持并发读,因此读写锁比互斥锁有更好的并发性。

接口方法

// 获取用于读取操作的锁
Lock readLock();

// 获取用于写入操作的锁
Lock writeLock();
读写锁 ReentrantReadWriteLock

ReentrantReadWriteLock类实现了ReadWriteLock接口。ReentrantReadWriteLock的特性如下:

公平性(公平性是针对不同线程而言的)
在构造ReentrantReadWriteLock对象时,可以传入参数指定读写锁是公平的还是非公平的,默认是非公平的。公平情况下,读锁和写锁按照申请锁的先后顺序获得锁。非公平情况下,写锁的获得优先于读锁,这种安排可以提升吞吐量,但是需要读锁的线程可能产生饥饿现象。

重入性(重入性是针对同一个线程而言的)

  • 获得读锁之后可以再次获取读锁。
  • 获得读锁之后不可以再次获取写锁。
  • 获得写锁之后可以再次获取读锁。
  • 获得写锁之后可以再次获取写锁。

为什么获得读锁之后不可以再次获取写锁?
考虑如下场景,两个线程当前都持有读锁,都想再获取写锁,而获取写锁需要对方释放读锁。显然这个场景下双方都不会释放读锁,因此出现线程相互等待对方的情况,也就是产生了线程死锁现象。因此,获得读锁之后不可以再次获取写锁。

构造方法

// 创建一个读写锁对象,默认为非公平锁
ReentrantReadWriteLock()

// 创建一个读写锁对象,fair表示是否为公平锁
ReentrantReadWriteLock(boolean fair)

使用示例

public class ReentrantReadWriteLockDemo {
    // 共享资源
    private int number = 0;
    // 读写锁
    private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    // 读操作
    public void get(){
        // 获取读锁
        readWriteLock.readLock().lock();
        try {
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName() + "读取到:" + number);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 释放读锁
            readWriteLock.readLock().unlock();
        }
    }

    // 写操作
    public void set() {
        // 获取写锁
        readWriteLock.writeLock().lock();
        try {
            Thread.sleep(2000);
            this.number = (int)(Math.random()*100);
            System.out.println(Thread.currentThread().getName() + "写入了:" + this.number);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 释放写锁
            readWriteLock.writeLock().unlock();
        }
    }

    public static void main(String[] args){
        ReentrantReadWriteLockDemo demo = new ReentrantReadWriteLockDemo();
        // 20个读线程
        for (int i = 0; i < 20; i++) {
            new Thread() {
                @Override
                public void run() {
                    demo.get();
                }
            }).start();
        }
        
        // 3个写线程
        for (int i = 0; i < 3; i++) {
            new Thread() {
                @Override
                public void run() {
                    demo.set();
                }
            }).start();
        }
    }
}

六、标记锁 StampedLock

标记锁是对读写锁的一种改进,它支持在读的同时进行一个写操作,使得读写可以并发执行,因此它的并发性比读写锁更好。

StampedLock的状态是由访问模式和一个数字标记(stamp)组成,它用状态表示和控制线程访问。

StampedLock的stamp:

  • 所有获取锁的方法,都返回一个stamp,stamp为0表示获取锁失败,stamp不为0都表示成功;
  • 所有释放锁的方法,都需要一个stamp,这个stamp必须和获取锁时得到的stamp相同,否则释放锁失败。

StampedLock的三种访问模式:

  • 读模式(Reading):类似读写锁的读锁。
  • 写模式(Writing):类似读写锁的写锁。
  • 乐观读模式(Optimistic reading):这是一种优化的读模式,没有真正去获取读锁,类同数据库的乐观锁机制,它适用于“读取后使用再写入”的场景。如果当前没有线程持有写锁,则简单的返回一个非0的观察stamp,之后在操作数据时还需要验证下该stamp是否有效。验证stamp的目的是检查读取和操作数据这段时间内是否有其他线程改动了数据,如果这段时间内没有其他线程改动数据,则stamp有效,线程可以直接操作数据。如果无效,则线程需要重新进行“读取后使用再写入”,通常这次的读取会使用读锁。所谓“乐观”就是乐观地估计在读取和操作数据这段时间内数据不会被其他线程改动。

StampedLock不支持重入,使用时要特别注意避免锁重入的操作。在使用乐观读时要遵循相应的调用模板,防止出现数据不一致。

乐观读的调用模板:

long stamp = lock.tryOptimisticRead();  // 获取乐观读的观察stamp
copyVaraibale2ThreadMemory();           // 拷贝共享变量到线程本地栈
if(!lock.validate(stamp)){              // 校验观察stamp
    stamp = lock.readLock();            // 获取读锁
    try {
        copyVaraibale2ThreadMemory();   // 拷贝共享变量到线程本地栈
     } finally {
       lock.unlockRead(stamp);          // 释放读锁
    }
}
useThreadMemoryVarables();              // 使用线程本地栈里面的数据进行操作

常用方法

// 创建一个标记锁对象
StampedLock​()

// 获取读锁,返回stamp
long readLock​()

// 释放读锁,stamp必须匹配
void unlockRead​(long stamp)

// 获取写锁,返回stamp
long writeLock​()

// 释放写锁,stamp必须匹配
void unlockWrite​(long stamp)

// 释放锁,stamp必须匹配
void unlock​(long stamp)

// 尝试乐观读,返回一个观察stamp用于稍后的验证,如果当前有线程持有写锁,则返回0。 
long tryOptimisticRead​()

// 如果在发行给定的stamp之后没有线程持有过写锁,则返回true
boolean validate​(long stamp)

// 如果锁定状态与给定的stamp匹配,则原子地执行以下操作之一。
// 如果stamp是读锁或写锁,则释放它并返回一个观察stamp。
// 如果stamp是乐观读,则在验证stamp有效后返回。
// 其他情况下都返回零。
long tryConvertToOptimisticRead​(long stamp)

// 如果锁定状态与给定的stamp匹配,则原子地执行以下操作之一。
// 如果stamp是写锁,则释放它并获取读锁stamp。
// 如果stamp是读锁,则返回它。
// 如果stamp是乐观读,则获取读锁并仅在立即可用时返回读锁stamp。
// 其他情况下,返回零。
long tryConvertToReadLock​(long stamp)

// 如果锁定状态与给定的stamp匹配,则原子地执行以下操作之一。
// 如果stamp是写锁,则返回它。
// 如果stamp是读锁,则释放读锁并返回写锁stamp。
// 如果stamp是乐观读,则获取写锁并仅在立即可用时才返回写锁stamp。
// 其他情况下,此方法返回零。
long tryConvertToWriteLock​(long stamp)

使用示例

public class StampedLockDemo {
    private final StampedLock lock = new StampedLock();
    private double balance;

    // 读锁
    public double read() {
        long stamp = lock.readLock();
        try {
            return balance;
        } finally {
            lock.unlockRead(stamp);
        }

    }
    
    // 写锁
    public void write(double amount) {
        long stamp = lock.writeLock();
        try {
            balance += amount;
        } finally {
            lock.unlockWrite(stamp);
        }
    }
    
    // 乐观读
    public double optimisticRead() {
        // 获取乐观读的观察stamp
        long stamp = lock.tryOptimisticRead();
        // 拷贝共享变量到线程本地栈
        double currentBalance = balance;
        // 校验观察stamp
        if (!lock.validate(stamp)) {
            // 校验失败,升级成读锁重试
            stamp = lock.readLock();
            try {
                // 拷贝共享变量到线程本地栈
                currentBalance = balance;
            } finally {
                // 释放读锁
                lock.unlockRead(stamp);
            }
        }
        // 使用线程本地栈里面的数据进行操作
        return currentBalance;
    }
    
    // 读后写
    public void writeAfterRead() {
        long stamp = lock.readLock();
        try {
            while (balance < 100) {
                long wstamp = lock.tryConvertToWriteLock(stamp);
                if (wstamp != 0L) {
                    stamp = wstamp;
                    balance = 100;
                    break;
                } else {
                    lock.unlockRead(stamp);
                    stamp = lock.writeLock();
                }
            }
        } finally {
            lock.unlock(stamp);
        }
    }

}

七、AQS

AQS俗称抽象队列同步器,实现类是AbstractQueuedSynchronizer抽象类,它是构成JUC锁框架的基础,许多同步类都依赖于它。AQS主是是以继承的方式使用,它没有实现任何接口,仅仅定义了同步状态的获取和释放的方法来供自定义的同步器使用。

AQS的基本原理是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态,如果被请求的共享资源被占用,那么就把这个请求的线程放到一个队列中等待。

AQS内部维护了一个表示共享资源状态的volatile int state变量和一个FIFO线程等待队列,线程在所需的资源被占用时就会进入这个队列。

state可以使用下列三个方法访问:

  • getState()
    获取变量
  • setState()
    设置变量
  • compareAndSetState()
    以原子的方式更新同步状态

AQS将每一个等待获取资源的线程封装成一个队列结点Node,结点Node保存了线程引用和线程状态。线程状态表示线程是否被阻塞、是否等待唤醒、是否已经被取消等,共有5种取值CANCELLED、SIGNAL、CONDITION、PROPAGATE、0。

  • CANCELLED:
    表示后驱结点被中断或超时,需要移出队列
  • SIGNAL:
    表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL。
  • CONDITION:
    Condition专用。表示当前结点在Condition队列中,因为等待某个条件而被阻塞了。
  • PROPAGATE:
    共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。
  • 0:
    新结点入队时的默认状态。

AQS使用了模板方法模式,其中大多数方法都是final或是private的,实现自定义同步器一般是继承AbstractQueuedSynchronizer并覆盖指定的方法。具体实现的时候只需要实现共享资源state的获取和释放方式即可,至于具体线程等待队列的维护,AQS已经在顶层实现好了。AQS定义了两种资源共享方式,即独占和共享。

实现自定义同步器主要实现下列方法:

  • tryAcquire(int)
    独占方式。尝试获取资源,成功则返回true,失败则返回false。
  • tryRelease(int)
    独占方式。尝试释放资源,成功则返回true,失败则返回false。
  • tryAcquireShared(int)
    共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
  • tryReleaseShared(int)
    共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
  • isHeldExclusively()
    该线程是否正在独占资源。只有用到condition才需要去实现它。

以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。

CAS操作

CAS是Compare and Swap的缩写,即比较再交换。CAS是一种无锁操作,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B,当且仅当预期值A和内存值V相同时才将内存值V修改为B,否则什么都不做。CAS是CPU指令级的操作,只有一步原子操作,所以非常快。在Java中CAS操作的实现都委托给一个名为UnSafe类来实现的。Unsafe类是在sun.misc包下,不属于Java标准。但是很多Java的基础类库,包括一些被广泛使用的高性能开发库都是基于Unsafe类开发的。Unsafe类可认为是Java中留下的后门,提供了一些低层次操作,如直接内存访问、线程调度等,该类可以实现对字段的原子操作。

LockSupport类

LockSupport类是JDK1.6引入的一个类,提供了基本的线程同步原语。LockSupport中的park()和unpark()方法分别是阻塞线程和解除阻塞线程,这两个方法也是通过调用Unsafe类里的本地方法实现的。

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