原理剖析(第 009 篇)ReentrantReadWriteLock工作原理分析

原理剖析(第 009 篇)ReentrantReadWriteLock工作原理分析

一、大致介绍

1、在前面章节了解了AQS和Semaphore后,想必大家已经对获取独占锁、获取共享锁有了一定的了解了;
2、而JDK中有一个关于读锁写锁分离的工具类,读锁是共享锁,写锁是排他锁,也是基于AQS实现的;
3、那么本章节就和大家分享分析一下JDK1.8的ReentrantReadWriteLock的工作原理; 

二、简单认识ReentrantReadWriteLock

2.1 何为ReentrantReadWriteLock?

1、ReentrantReadWriteLock从英文字面上可理解为可重入的读写锁;

2、ReentrantReadWriteLock具备读锁与写锁,读锁是共享锁使用共享模式,写锁是排它锁使用独占模式;

3、ReentrantReadWriteLock具备公平与非公平策略,"读-写"互斥、"写-写"互斥;

2.2 ReentrantReadWriteLock的state关键词

1、ReentrantReadWriteLock的state关键字,有点像ThreadPoolExecutor工作线程数量值ctl的味道;

2、ReentrantReadWriteLock高16位为读锁的计数值,低16位为写锁的计数值;

2.3 常用重要的成员属性

1、private final ReentrantReadWriteLock.ReadLock readerLock;
   // 读锁对象

2、private final ReentrantReadWriteLock.WriteLock writerLock;
   // 写锁对象

3、final Sync sync;
   // 同步器

4、static final int SHARED_SHIFT   = 16; // 分界线偏移值,用来向左或向右偏移尾数,以此来获取读写锁计数值
   static final int SHARED_UNIT    = (1 << SHARED_SHIFT); // 读锁需要加1时递增的增量
   static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1; // 读锁、写锁的最大计数值数量
   static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1; // 写锁掩码,用于写锁计数值的低16位有效值

5、private transient ThreadLocalHoldCounter readHolds;
   // 保存当前线程重入读锁次数的容器,当读锁重入次数为0时则会被移除掉

6、private transient HoldCounter cachedHoldCounter;
   // 最近一个成功获取读锁的线程的计数对象

2.4 常用重要的方法

1、public ReentrantReadWriteLock()
   // 创建一个读写锁的对象,默认的策略是非公平策略

2、public ReentrantReadWriteLock(boolean fair)
   // 创建一个读写锁的对象,且是否公平方式由传入的fair布尔参数值决定

3、public ReentrantReadWriteLock.WriteLock writeLock()
   // 获取写锁对象
   
4、public ReentrantReadWriteLock.ReadLock  readLock()
   // 获取读锁对象
   
5、public final boolean isFair() 
   // 查看当前的读写锁对象用的策略方式是啥,是公平策略,还是非公平策略
   
6、protected Thread getOwner()
   // 获取持有独占锁的线程对象
   
7、public int getReadLockCount()
   // 获取持有读锁计数值
   
8、public boolean isWriteLocked()
   // 查看是否有线程持有写锁
   
9、public boolean isWriteLockedByCurrentThread()
   // 查看当前的线程是不是持有独占写锁的线程
   
10、public int getWriteHoldCount()
    // 获取当前线程在此写锁上保持的重入锁数量
   
11、public int getReadHoldCount()
    // 获取当前线程在此读锁上保持的重入锁数量
    
12、protected Collection<Thread> getQueuedWriterThreads()
    // 返回一个 collection,它包含可能正在等待获取写入锁的线程

13、protected Collection<Thread> getQueuedReaderThreads()
    // 返回一个 collection,它包含可能正在等待获取读取锁的线程

14、public final boolean hasQueuedThreads()
    // 查看是否有阻塞的线程队列

15、public final boolean hasQueuedThread(Thread thread)
    // 查询给定的线程是否正处于阻塞队列中

16、abstract boolean readerShouldBlock();
    // 抽象方法,由AQS的子类实现,读锁是否需要阻塞

17、abstract boolean writerShouldBlock();
    // 抽象方法,由AQS的子类实现,写锁是否需要阻塞

18、static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
    // 获取读锁的计数值

19、static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
    // 获取写锁的计数值

2.5 设计与实现伪代码

1、获取写锁:
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

    acquire{
        如果尝试获取独占锁失败的话( 尝试获取独占锁的各种方式由AQS的子类实现 ),
        那么就新增独占锁结点通过自旋操作加入到队列中,并且根据结点中的waitStatus来决定是否调用LockSupport.park进行休息
    }
    
    
2、释放写锁:
    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
    
    release{
        如果尝试释放独占锁成功的话( 尝试释放独占锁的各种方式由AQS的子类实现 ),
        那么取出头结点并根据结点waitStatus来决定是否有义务唤醒其后继结点
    }

3、获取读锁:
    public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }
    
    acquireShared{
        如果尝试获取共享锁失败的话( 尝试获取共享锁的各种方式由AQS的子类实现 ),
        那么新增共享锁结点通过自旋操作加入到队尾中,并且根据结点中的waitStatus来决定是否调用LockSupport.park进行休息
    }

4、释放读锁:
    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }
    
    releaseShared{
        如果尝试释放共享锁失败的话( 尝试释放共享锁的各种方式由AQS的子类实现 ),
        那么通过自旋操作唤完成阻塞线程的唤起操作
    }

三、源码分析ReentrantReadWriteLock

3.1、Sync同步器

1、AQS --> Sync ---> FairSync // 公平策略
                  |
                  |> NonfairSync // 非公平策略
                  
2、ReentrantReadWriteLock内的同步器都是通过Sync抽象接口来操作调用关系的,细看会发现基本上都是通过sync.xxx之类的这种调用方式的;

3.2、ReentrantReadWriteLock构造器

1、构造器源码:
    // 构造方法一:
    /**
     * Creates a new {@code ReentrantReadWriteLock} with
     * default (nonfair) ordering properties.
     */
    public ReentrantReadWriteLock() {
        this(false);
    }
    
    // 构造方法二:
    /**
     * Creates a new {@code ReentrantReadWriteLock} with
     * the given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        readerLock = new ReadLock(this);
        writerLock = new WriteLock(this);
    }   
    

2、xxxxxxxxxxxxxxxxxxx

3.3、tryAcquire(int)

1、源码:
    // ReentrantReadWriteLock 的静态内部类 Sync 的 tryAcquire 方法,写锁获取锁的核心方法
    protected final boolean tryAcquire(int acquires) {
        /*
         * Walkthrough:
         * 1. If read count nonzero or write count nonzero
         *    and owner is a different thread, fail.
         * 2. If count would saturate, fail. (This can only
         *    happen if count is already nonzero.)
         * 3. Otherwise, this thread is eligible for lock if
         *    it is either a reentrant acquire or
         *    queue policy allows it. If so, update state
         *    and set owner.
         */
        Thread current = Thread.currentThread(); // 获取当前的线程对象
        int c = getState(); // 获取最新的读写锁资源值
        int w = exclusiveCount(c); // 然后查看独占锁的占有线程数量
        if (c != 0) { // c不为零,说明有线程占有写锁
            // (Note: if c != 0 and w == 0 then shared count != 0)
            // 如果w=0,说明已经有线程占有了读锁,那么当前想获取写锁的话没必要了,直接返回false到队列中排队去
            // 如果w!=0,说明已经有线程占有了写锁,那么再看看当前线程是不是那个正在持有写锁的线程?
            // 如果当前线程不是持有写锁的那个线程,则返回false到队列中排队去;
            if (w == 0 || current != getExclusiveOwnerThread())
                return false;
            // 执行到此,说明同一个线程先是持有了写锁,然后还想继续占有写锁,那么就是重入的概念,随时欢迎重入锁
            if (w + exclusiveCount(acquires) > MAX_COUNT) // 当持有写锁的数量超过MAX_COUNT=65535时,则抛出异常,试想一下这个写锁如果达到了65535这个数量级的话,
                // 可能是递归导致的,可能是其他原因导致的,反正不管怎么着总不至于都溢出了吧;
                // 因此是有问题的,所以这里抛出了异常让调用方去查查到底是什么原因;
                throw new Error("Maximum lock count exceeded");
            // Reentrant acquire
            setState(c + acquires); // 对于写锁的再次重入,来一个收一个,赋值写锁状态值,然后返回true继续执行临界区的代码
            return true;
        }
        
        // 执行到此,c=0,也就是说目前还没有线程占用读锁和写锁
        if (writerShouldBlock() || // 抽象方法需要子类来实现,根据写锁是否需要阻塞的标志来判断,true则需要阻塞,false则不需要阻塞
        // writerShouldBlock()在公平策略中,当有阻塞队列时则返回true需要阻塞,无阻塞队列时返回false不需要阻塞;
        // writerShouldBlock()在非公平策略中,永远都返回false写锁不需要阻塞;
            !compareAndSetState(c, c + acquires)) 
            // 如果需要阻塞,则直接返回false到队列中排队去
            // 如果需要阻塞,则通过CAS尝试占用写锁资源,如果尝试占用写锁失败,说明由于并发c的值已经被改动了,所以还是乖乖到队列中排队去
            return false;
        setExclusiveOwnerThread(current); // 走到这里,说明写锁经过千辛万苦终于拿到写锁的执行权了,则可以继续执行临界区代码块了
        return true;
    }

2、通过写锁writeLock.lock()最终调用的是Sync的tryAcquire尝试获取锁方式,从而可以得出几个结论:
    • 已持有读锁的线程不能再持有写锁;
    • 已持有写锁的线程可以再持有写锁,这和ReentrantLock的重入锁概念是一致的;
    • 已持有读锁的线程,其他线程是不能持有写锁的;
    • 已持有写锁的线程,其他线程是不能持有写锁的;
    
3、至于返回false后面是如何进入阻塞队列的话,这里就不多讲了,因为前面已经讲过了,见( 原理剖析(第 005 篇)AQS工作原理分析 );

3.4、writerShouldBlock/readerShouldBlock

1、源码:
    /**
     * Fair version of Sync:公平策略版本的同步器;
     */
    static final class FairSync extends Sync {
        private static final long serialVersionUID = -2274990926593161451L;
        
        /**
         * 公平策略的写锁是否需要阻塞,阻塞的判断依据就是:当有阻塞队列时则返回true需要阻塞,无阻塞队列时返回false不需要阻塞;
         */
        final boolean writerShouldBlock() {
            return hasQueuedPredecessors();
        }
        
        /**
         * 公平策略的读锁是否需要阻塞,阻塞的判断依据就是:当有阻塞队列时则返回true需要阻塞,无阻塞队列时返回false不需要阻塞;
         */
        final boolean readerShouldBlock() {
            return hasQueuedPredecessors();
        }
    }
    
    /**
     * Nonfair version of Sync:非公平策略版本的同步器;
     */
    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = -8159625535654395037L;
        
        /**
         * 非公平策略的写锁是否需要阻塞,阻塞的判断依据就是:直接是默认返回false,永远都返回false写锁不需要阻塞;
         */
        final boolean writerShouldBlock() {
            return false; // writers can always barge
        }
        
        /**
         * 非公平策略的读锁是否需要阻塞,阻塞的判断依据就是:阻塞队列中的第一个结点是不是独占式结点,如果是则返回true表明读锁需要阻塞,否则返回false不需要阻塞;
         */
        final boolean readerShouldBlock() {
            /* As a heuristic to avoid indefinite writer starvation,
             * block if the thread that momentarily appears to be head
             * of queue, if one exists, is a waiting writer.  This is
             * only a probabilistic effect since a new reader will not
             * block if there is a waiting writer behind other enabled
             * readers that have not yet drained from the queue.
             */
            return apparentlyFirstQueuedIsExclusive();
        }
    }
    
2、FairSync/NonfairSync主要重写了父类Sync的读锁、写锁是否需要阻塞,在公平策略与非公平策略中都有各自的实现;

3.5、tryRelease(int)

1、源码:
    // ReentrantReadWriteLock 的静态内部类 Sync 的 tryRelease 方法,写锁释放锁的核心方法
    protected final boolean tryRelease(int releases) {
        if (!isHeldExclusively()) // 如果当前线程没有获取独占式锁的话,也就是说当前线程没有持有写锁的话,那么就直接抛异常
            throw new IllegalMonitorStateException();
            // 之所以抛异常,是因为本来该方法就是持有写锁的线程来调用释放操作的,但是结果却发现当前线程自己没有吃有写锁,
            // 那岂不是尴尬,所以期间肯定出现了其他未知的问题,因此直接抛异常,告诉调用方,肯定有地方用错了还是啥啥啥的
        int nextc = getState() - releases; // 获取最新的锁资源值并且做减法操作,减去releases
        boolean free = exclusiveCount(nextc) == 0; // 如果得出的nextc=0,那么说明持有写锁的线程已经完全被释放了
        if (free) // 一般情况下,通过锁资源值做减法操作,一般都会得到结果零,则设置独占式线程对象exclusiveOwnerThread为空
            setExclusiveOwnerThread(null); // 
        setState(nextc); // 如果能执行到此,说明是重入锁,需要多重释放才能降低为零,反正如果没减至零最后都需要更新减后的结果值
        return free; // 返回true说明已经没有线程持有写锁了,返回false说明还有线程持有写锁
    }

2、该方法主要讲解了写锁如何进行释放资源,最后不管做减法的结果如何,都会更新减法之后的结果赋值到state锁资源值;

3.6、tryAcquireShared(int)

1、源码:
    // ReentrantReadWriteLock 的静态内部类 Sync 的 tryAcquireShared 方法,读锁获取锁的核心方法
    protected final int tryAcquireShared(int unused) {
        /*
         * Walkthrough:
         * 1. If write lock held by another thread, fail.
         * 2. Otherwise, this thread is eligible for
         *    lock wrt state, so ask if it should block
         *    because of queue policy. If not, try
         *    to grant by CASing state and updating count.
         *    Note that step does not check for reentrant
         *    acquires, which is postponed to full version
         *    to avoid having to check hold count in
         *    the more typical non-reentrant case.
         * 3. If step 2 fails either because thread
         *    apparently not eligible or CAS fails or count
         *    saturated, chain to version with full retry loop.
         */
        Thread current = Thread.currentThread(); // 获取当前线程对象
        int c = getState(); // 获取内存中最新的锁资源值
        if (exclusiveCount(c) != 0 && // 如果有线程持有写锁
            getExclusiveOwnerThread() != current) // 并且持有写锁的线程不是当前线程
            return -1; // 那么则返回-1表明获取读锁失败,应该乖乖进入CLH阻塞队列
        int r = sharedCount(c); // 获取读锁共享计数
        
        // readerShouldBlock()在公平策略中,当有阻塞队列时则返回true需要阻塞,无阻塞队列时返回false不需要阻塞;
        // readerShouldBlock()在非公平策略中,阻塞队列中的第一个结点是不是独占式结点,如果是则返回true表明读锁需要阻塞,否则返回false不需要阻塞;
        if (!readerShouldBlock() && // 如果读锁不需要阻塞处理
            r < MAX_COUNT && // 如果读锁计数值没有超过最大限制值
            compareAndSetState(c, c + SHARED_UNIT)) { // 并且通过CAS尝试获取读锁资源
            // 能执行到if里面来,说明当前线程已经成功的突破了重重包围,准备看看如何接下来的处理;
            if (r == 0) { // 如果成功获取到读锁资源前,发现之前还没有任何线程持有读锁
                firstReader = current; // 则给firstReader对象赋值为第一个获取读锁的线程对象
                firstReaderHoldCount = 1; // 并且firstReaderHoldCount第一个线程持有读锁次数初始化次数为1
            } else if (firstReader == current) { // 能执行这个判断,说明读锁计数值肯定不为零,当首次获取读锁的线程正好是当前线程的话
                firstReaderHoldCount++; // 那么firstReaderHoldCount又加1,这里又可以认为重入锁的概念,但是这里重入的是读锁
            } else { // 如果执行到这里,说明当前持有读锁的线程不是当前线程
                HoldCounter rh = cachedHoldCounter; // 获取最近的一个成功获取读锁的线程的计数对象
                if (rh == null || rh.tid != getThreadId(current)) // 如果rh为空或者rh的线程id不是当前线程的话,
                    cachedHoldCounter = rh = readHolds.get(); // 那么则将readHolds的计数对象取出来赋值给cachedHoldCounter
                    // 意思就是说,readHolds里面有最近一次获取读锁的线程的一些简单的计数信息
                else if (rh.count == 0) // 当最近一个的那个计数对象count=0,则说明HoldCounter还刚刚被创立出来
                    readHolds.set(rh); // 那么将rh这个对象直接赋值到readHolds中去
                rh.count++; // 并且次数累加一次
            }
            return 1; // 返回1说明当前已经成功的获取到了读锁,并且也成功的修改了state这么一个和锁资源密切相关的字段
        }
        
        // 执行到此,有3种情况:
        // 1、当读锁需要阻塞处理的话,则会执行到此;
        // 2、当读锁不需要阻塞处理,但是读锁的计数值超过了最大限制值MAX_COUNT=65535,那么也会执行到此;
        // 3、当读锁不需要阻塞处理,读锁计数值也没有超过最大限制值,但是通过CAS尝试占有读锁资源时失败了,也会执行到此
        // 总之一句话,没有顺利获取到读锁资源的线程,都会执行到这里来;
        return fullTryAcquireShared(current); // 获取读锁失败,则回炉重造通过自旋方式重试
    }

2、通过readLock.lock()最终调用的是Sync的tryAcquireShared尝试获取锁方式,从而可以得出几个结论:
    • 已持有写锁的线程,其他线程是不能持有读锁的;
    • 已持有写锁的线程可以再持有读锁,这里我们称之为锁降级;
    • 已持有读锁的线程可以再持有读锁,这和ReentrantLock的重入锁概念是一致的;
    • 已持有读锁的线程,其他线程也可以持有读锁的;

3、至于回炉重造的重试机制和tryAcquireShared操作方式以及代码非常类似,这里就不再详讲了;

3.7、tryReleaseShared(int)

1、源码:
    // ReentrantReadWriteLock 的静态内部类 Sync 的 tryReleaseShared 方法,读锁释放锁的核心方法
    protected final boolean tryReleaseShared(int unused) {
        Thread current = Thread.currentThread(); // 获取当前线程对象
        if (firstReader == current) { // 如果首次获取读锁的线程为当前线程的话
            // assert firstReaderHoldCount > 0;
            if (firstReaderHoldCount == 1) // 如果此刻firstReaderHoldCount次数正好为1的话,说明该线程的读锁没有重入
                firstReader = null; // 则直接将首次获取读锁的线程置为空即可
            else
                firstReaderHoldCount--; // 若firstReaderHoldCount不为1,则肯定是读锁重入了,则需要自减1操作;
        } else {
            // 执行到此,说明当前要释放读锁的线程不是那个首次获取到读锁的线程
            HoldCounter rh = cachedHoldCounter;
            if (rh == null || rh.tid != getThreadId(current)) // 获取最近的那个线程对象,如果不是当前线程的话
                rh = readHolds.get(); // 那么则通过readHolds获取最近的那个计数对象
            int count = rh.count; // 取出count值
            if (count <= 1) { // 若小于等于1,那么自减1就没了,所以减都没减了,直接移除掉,简单干脆
                readHolds.remove(); // 直接移除
                if (count <= 0) // 
                    throw unmatchedUnlockException();
            }
            --rh.count; // 如果大于1的话,则还有的减,那么就自减1操作
        }
        for (;;) { // 自旋的死循环操作方式
            int c = getState(); // 获取最新的锁资源值
            int nextc = c - SHARED_UNIT; // 通过计算高位减1处理
            if (compareAndSetState(c, nextc)) // 通过尝试CAS正好设置成功的话,那么则返回nextc与0的比较
                // Releasing the read lock has no effect on readers,
                // but it may allow waiting writers to proceed if
                // both read and write locks are now free.
                return nextc == 0; // 不管如何,只要CAS成功,则表明读锁次数值已经被降低释放了一次了
                
            // 执行到此,说明由于并发的原因导致CAS失败,所以需要继续循环再次操作释放读锁次数操作
        }
    }

2、该方法主要讲解了读锁如何进行释放资源,如果首次释放失败的话,则会通过自旋的方式继续尝试释放资源,直到成功为止;

四、总结

1、这里有许多其它更底层的没有分析,因为都是AQS内部的基类方法了,而这些基类方法都在之前介绍过了,如果大家不记得的就去翻前面的篇章( 原理剖析(第 005 篇)AQS工作原理分析 );

2、经过上面的一系列分析之后,在这里我再来总结一下ReentrantReadWriteLock的流程的一些特性;
    // ReentrantReadWriteLock.WriteLock.lock()特性:
    • 已持有读锁的线程不能再持有写锁;
    • 已持有写锁的线程可以再持有写锁,这和ReentrantLock的重入锁概念是一致的;
    • 已持有读锁的线程,其他线程是不能持有写锁的;
    • 已持有写锁的线程,其他线程是不能持有写锁的;

    // ReentrantReadWriteLock.ReadLock.lock()特性:
    • 已持有写锁的线程,其他线程是不能持有读锁的;
    • 已持有写锁的线程可以再持有读锁,这里我们称之为锁降级;
    • 已持有读锁的线程可以再持有读锁,这和ReentrantLock的重入锁概念是一致的;
    • 已持有读锁的线程,其他线程也可以持有读锁的;
    
3、然而将上面的WriteLock\ReadLock特性进行合并为:
    • 已持有写锁的线程可以再持有写锁,这和ReentrantLock的重入锁概念是一致的;
    • 已持有写锁的线程,其他线程是不能持有读锁、写锁的;
    • 已持有写锁的线程可以再持有读锁,这里我们称之为锁降级;
    
    • 已持有读锁的线程可以再持有读锁,这和ReentrantLock的重入锁概念是一致的;
    • 已持有读锁的线程,其他线程也可以持有读锁的;    
    • 已持有读锁的线程,其他线程(同时也包括已持有读锁的线程)是不能持有写锁的;

4、排除可重入的特性,再精炼合并特性为:
    • 写锁会排斥读锁、写锁,但是读锁会阻塞写锁;
    • 写锁可以降级为读锁,但读锁不能升级为写锁; 

五、下载地址

https://gitee.com/ylimhhmily/SpringCloudTutorial.git

SpringCloudTutorial交流QQ群: 235322432

SpringCloudTutorial交流微信群: 微信沟通群二维码图片链接

欢迎关注,您的肯定是对我最大的支持!!!

<上一篇        首页        下一篇>

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

推荐阅读更多精彩内容