[Java源码][并发J.U.C]---解析ReentrantReadWriteLock

前言

本文将分析读写锁ReentrantReadWriteLock的源码,也会在分析中穿插一些例子来加深对源码的理解. 本文会如以下顺序介绍.
1. 整体架构
2. 读写状态的设计
3. 写锁的源码分析并以例子加深理解
4. 读锁的源码分析并以例子加深理解
5. 锁降级
本文源码下载

整体架构

读写锁在读线程获得锁时可以允许多个读线程同时访问,但是在写线程获得锁时,所有的读线程和其他写线程均被阻塞.

ReentrantReadWriteLock.png

从上图可以看到

1. ReentrantReadWriteLock主要是实现了接口ReadWriteLock来实现读写锁,并且真正返回的读锁是ReadLock的一个实例,写锁是WriteLock的一个实例.
2. ReentrantReadWriteLock中有一个Sync的成员变量sync(SyncReentrantReadWriteLock内部类), 并且ReadLockWriteLock使用了该成员变量sync来实现它们从接口Lock继承的抽象方法.
3. Sync的一个实例sync可以是一个FairSync或者NonfairSync, 并且Sync类继承了AbstractQueuedSynchronizer,由此可知SyncReentrantReadWriteLock类的核心,并且实现了读写锁的具体逻辑.

接下来分析的内容都是跟Sync类息息相关.

读写状态的设计

先了解一下读写状态的设计. 我们知道AQS中有一个状态值, 比如在ReentrantLock中表示持有锁的线程重入了多少次. 但是在ReentrantReadWriteLock中有读锁和写锁因此需要划分,所以高16位代表读锁的状态,低16位代表写锁的状态.
如图所示,一个线程已经获取了写锁,并且重进入了两次,同时也连续获取了两次读锁.(有人可能会疑惑为什么在获得写锁的同时还可以获得读锁呢, 在锁降级的时候你会得到答案.)

来自Java并发编程的艺术.png

从读写锁的作用可知读锁是一个共享锁, 写锁是一个互斥锁. 因此sharedCount(int c)是为了获取读锁的状态值, exclusiveCount(int c)是为了获取写锁的状态值.

abstract static class Sync extends AbstractQueuedSynchronizer {
        static final int SHARED_SHIFT   = 16;
        static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
        static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
        static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

        /** 返回c的高16位  读状态*/
        static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
        /** 返回c的低16位  写状态*/
        static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
}

写锁的获取和释放

先分析写锁是因为写锁与之前分析的锁在获取和释放的过程基本类似,而读锁相较于写锁会稍微复杂一点点.

写锁的获取

源码如下
作用: 当前线程尝试获取写锁, 获取成功返回true,获取失败返回false.

/**
         * 作用: 写锁的获取
         *
         * @param acquires 获取的个数
         * @return true表示获取锁, false表示未获取锁
         */
        protected final boolean tryAcquire(int acquires) {
            Thread current = Thread.currentThread();
            int c = getState();   // 整体状态
            int w = exclusiveCount(c); // 写状态的个数
            /**
             *  整体状态如果等于0 表明读锁和写锁目前都没有线程获取到 则可以去获取写锁
             *  如果不等于0
             *  1. 存在读锁或者当前线程不是已经获取写锁的线程,则直接返回
             *  2. 如果写锁的数量没有超过最高值则获得写锁
             */

            if (c != 0) {
                // (Note: if c != 0 and w == 0 then shared count != 0)
                // 存在读锁或者当前线程不是已经获取写锁的线程
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                // 重入式获取
                setState(c + acquires);
                return true;
            }
            /**
             *  表示整体状态为0
             *  如果writeShouldBlock需要阻塞或者CAS操作不成功则返回false
             */
            if (writerShouldBlock() ||
                    !compareAndSetState(c, c + acquires))
                return false;
            /**
             * 请注意setExclusiveOwnerThread该方法设置的是写锁
             */
            setExclusiveOwnerThread(current); // 设置当前线程是获得写锁的线程
            return true;
        }

其实获取写锁的逻辑比较简单. 具体细节可以参考上面的注解.
1. 如果不存在读锁或写锁(状态为0),则成功获取锁并设置setExclusiveOwnerThread后返回true.
2. 如果存在读锁,则直接返回false.
3. 如果存在写锁, 分以下两种情况:

  • 3.1 如果写锁的线程不是当前线程,则返回false.
  • 3.2 如果写锁的重入数已经超过了最大值,则返回false.
  • 3.3 设置写锁的重入数(加1), 返回true.

流程图如下:


tryAcquire.png
关于writeShouldBlock()readShouldBlock()

这两个方法是Sync的抽象方法, 由子类实现, 可以看一下公平锁和非公平锁的具体实现.

static final class NonfairSync extends Sync {
        private static final long serialVersionUID = -8159625535654395037L;
        // 写锁永远不需要阻塞
        final boolean writerShouldBlock() {
            return false; // writers can always barge
        }
        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();
        }
    }

    static final class FairSync extends Sync {
        private static final long serialVersionUID = -2274990926593161451L;
        final boolean writerShouldBlock() {
            return hasQueuedPredecessors();
        }
        final boolean readerShouldBlock() {
            return hasQueuedPredecessors();
        }
        // 如果前面有节点 返回true 说明需要阻塞
    }

写锁的释放

作用: 写锁的释放

/**
         *
         * 作用: 写锁的释放
         * @param releases 释放的个数
         * @return 写锁是否完全释放 true 完全释放
         */
        protected final boolean tryRelease(int releases) {
            // 如果当前线程不是已经获取写锁的线程,则直接抛出异常
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            int nextc = getState() - releases;
            boolean free = exclusiveCount(nextc) == 0;
            // 判断写锁(重入锁)是否已经全部释放完
            if (free)
                setExclusiveOwnerThread(null);
            setState(nextc); // 设置状态
            return free;
        }

例子: 测试写锁的获取和释放

工具类SleepUnitCache

package com.sourcecode.reentrantreadwritelock;

import java.util.HashMap;
import java.util.Map;

public class Cache {
    static Map<String, Object> map = new HashMap<String, Object>();
    static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    static Lock r = rwl.readLock();
    static Lock w = rwl.writeLock();
    // 获取一个key对应的value
    public static final Object get(String key) {
        r.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " gets lock.");
            SleepUnit.sleep(5);
            return map.get(key);
        } finally {
            System.out.println(Thread.currentThread().getName() + " releases lock.");
            r.unlock();
        }
    }
    // 设置key对应的value,并返回旧的value
    public static final Object put(String key, Object value) {
        w.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " gets lock.");
            SleepUnit.sleep(10);
            return map.put(key, value);
        } finally {
            System.out.println(Thread.currentThread().getName() + " releases lock.");
            w.unlock();
        }
    }
    // 清空所有的内容
    public static final void clear() {
        w.lock();
        try {
            map.clear();
        } finally {
            w.unlock();
        }
    }
}
public class SleepUnit {
    public static void sleep(int time) {
        try {
            TimeUnit.SECONDS.sleep(time);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

测试类. 启动五个线程去获取写锁,通过打印出AQS中的等待队列来观察情况.

public class TestCache {
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(new Runner(), "thread-" + i).start();
        }
        for (int i = 0; i < 10; i++) {
            SleepUnit.sleep(5);
            Cache.rwl.printWaitingNode();
        }
    }

    static class Runner implements Runnable {
        public void run() {
            Cache.put("k0", "k0");
        }
    }
}

结果输出: 结果并没有什么意外, 当所有线程尝试去获取写锁时只有一个线程可以拿到锁.

thread-0 gets lock.
[NULL,-1]->[thread-3,独占,-1]->[thread-2,独占,-1]->[thread-1,独占,-1]->[thread-4,独占,0]->
[NULL,-1]->[thread-3,独占,-1]->[thread-2,独占,-1]->[thread-1,独占,-1]->[thread-4,独占,0]->
thread-3 gets lock.
[NULL,-1]->[thread-2,独占,-1]->[thread-1,独占,-1]->[thread-4,独占,0]->
[NULL,0]->thread-2 gets lock.
[thread-2,独占,-1]->[thread-1,独占,-1]->[thread-4,独占,0]->
[NULL,-1]->[thread-1,独占,-1]->[thread-4,独占,0]->
thread-1 gets lock.
[NULL,-1]->[thread-4,独占,0]->
[NULL,-1]->[thread-4,独占,0]->
thread-4 gets lock.
[NULL,0]->
[NULL,0]->
[NULL,0]->

读锁的获取和释放

读锁相对于写锁来说比较复杂点,因为读锁的时候是共享的,意味着每个线程都可以获得线程, 又因为读锁是可重入的,所以每个获得读锁的线程都有一个对应的可重入的数量.说到这里很容易就会想到用ThreadLocal类来实现的. 关于ThreadLocal可以参考我的另外一篇博客[Java源码][并发J.U.C]---解析ThreadLocal, 因此接下来先介绍一下读锁用到的一些变量及其作用.

读锁使用到的变量介绍

/**
         * 一个ThreadLocal的子类,value值是HoldCounter类的对象并重写了initValue()方法
         */
        static final class ThreadLocalHoldCounter
                extends ThreadLocal<HoldCounter> {
            public HoldCounter initialValue() {
                return new HoldCounter();
            }
        }

        /**
         * 一个thredlocal实例保存线程对应的HoldCount
         * 在构造函数或者readObject中完成初始化
         * 当读锁线程的重入数变为0时,会被removed.
         */
        private transient ThreadLocalHoldCounter readHolds;

        /**
         * 成功获取读锁的最后一个线程的HoldCounter对象.
         * 为了避免总是去readHolds中查找
         */
        private transient HoldCounter cachedHoldCounter;

        /**
         * firstReader是第一个获得读锁定的线程, 
         * 严格意义上是第一个使得读锁状态值从0变为1的线程
         * firstReaderHoldCount是其对应的重入数
         *
         */
        private transient Thread firstReader = null;
        private transient int firstReaderHoldCount;

        /**
         *  构造函数, 初始化readHolds并设置状态
         */
        Sync() {
            readHolds = new ThreadLocalHoldCounter();
            setState(getState()); // ensures visibility of readHolds
        }

在读锁中主要使用了三个变量来保持读锁的获取和释放.
1. firstReader 存储着第一个把整体状态从0变为1的线程.
2. cachedHoldCounter 保存着最后一个获取读锁线程的HoldCounter对象.
3. readHolds保存每个线程和其对应的HoldCounter对象, 不包括firstReader, 包括最后一个获取读锁线程.

读锁的获取

tryAcquireShared方法.

/**
         * @param unused 释放
         * @return  返回一个数值如果大于等于0,表明获得锁.
         *          返回一个数值如果小于0,表明没有获得锁.
         */
        protected final int tryAcquireShared(int unused) {
            Thread current = Thread.currentThread();
            int c = getState(); //获取当前状态
            if (exclusiveCount(c) != 0 &&
                    getExclusiveOwnerThread() != current)  //如果写锁存在并且写锁持有者不是当前线程
                return -1;
            // 说明 1.写锁不存在 或者 2.写锁存在但是写锁持有者是当前线程
            int r = sharedCount(c);  // 获取读锁的个数

            /**
             *   1. 读锁不需要阻塞.
             *   2. 读锁的总个数没有超过最大数.
             *   3. 通过CAS设置c的状态 因为高16位是读锁的个数 所以需要加上1<<16.
             */
            if (!readerShouldBlock() &&
                    r < MAX_COUNT &&
                    compareAndSetState(c, c + SHARED_UNIT)) {
                /**
                 *  上面三个条件都满足的情况下会进入这里继续执行
                 *  1.  r == 0 意味着当前线程是第一个获得读锁的线程(之前没有获得过).
                 *  2.  firstReader == current 意味当前线程是那个之前第一个获得读锁的线程 可以重入
                 *  3.  如果都不是就说明当前线程不是第一个获得读锁的线程,因此当前线程最起码是第二个获得读锁的线程,
                 *      a.  先去cachedHoldCounter看一下是不是最后一次获得读锁的线程,如果不是就把当前线程缓存起来
                 *          (因为此时该线程是目前最后一个获得读锁的线程)
                 *      b.  如果是的话如果rh.count==0,就需要把从readHolds中添加进去
                 *            (这是因为在对应的release中rh.count==0的时候readHolds做了清除操作)
                 *      rh.count++
                 *  返回1,代表成功获得锁.
                 */
                if (r == 0) {
                    firstReader = current;
                    firstReaderHoldCount = 1;
                } else if (firstReader == current) {
                    firstReaderHoldCount++;
                } else {
                    HoldCounter rh = cachedHoldCounter;
                    if (rh == null || rh.tid != getThreadId(current))
                        cachedHoldCounter = rh = readHolds.get();
                    else if (rh.count == 0)
                        readHolds.set(rh);
                    rh.count++;
                }
                return 1;
            }
            return fullTryAcquireShared(current);
        }

作用: 尝试获取读锁, 返回大于等于0的数表明获得锁, 返回小于0的数表明未获得锁.
逻辑如下:
1. 如果另一个线程持有写锁定,则失败.
2. 如果进入到这里表明两种情况写锁不存在或者写锁存在但是写锁持有者就是当前线程
3. 如果同时满足3个条件分别是a.读锁不需要阻塞b.读锁的总个数没有超过最大数c.CAS设置状态成功, 则当前线程成功获得读锁. 否则进入fullTryAcquireShared方法(放到后面分析).
4. 满足了上面3个条件后需要更新读锁的相关变量.基本逻辑如下:

  • 4.1 如果读锁状态值为0,表明该线程是第一个获得读锁的线程,设置firstReaderfirstReadHoldCount变量.
  • 4.2 如果不是4.1则表明读锁已经被线程获取过了,那么如果当前线程就是那个第一次获得读锁的线程,则设置其重入数firstReaderHoldCount即可.
  • 4.3 如果不是4.2则表明读锁已经被获取过了并且当前线程并不是那个第一次获得读锁的线程,此时就可以去缓存cachedHoldCounter中看看是不是当前线程,如果不是的话就从readHolds中获取并将其缓存在cachedHoldCounter中. 最后rh.count++设置一下重入数.
    注意 当某个HoldCountercount为0的时候,readHolds是会将其清除掉的.

流程图如下:

tryAcquireShared.png

fullTryAcquireShared方法

/**
         * 作用: 获取锁, 返回值大于等于0表示
         * 用于处理tryAcquireShared方法中未能满足的3个条件
         */
        final int fullTryAcquireShared(Thread current) {
            HoldCounter rh = null;
            for (;;) {
                int c = getState();                 // 获取当前状态
                if (exclusiveCount(c) != 0) {       // 如果写锁不为0 表明存在写锁
                    // 如果写锁不是当前线程(说明此刻已经有别的线程获得写锁了),则需要阻塞当前线程所以返回-1.
                    if (getExclusiveOwnerThread() != current)
                        return -1;
                    // else we hold the exclusive lock; blocking here
                    // would cause deadlock.
                    /**
                     *  这一段话的意思是如果当前线程如果在这里block了,那会形成死锁,
                     *  因为当前线程已经在持有写锁的情况来请求读锁的,那么该锁在没有释放锁的情况下block了
                     *  就会形成死锁了
                     */
                } else if (readerShouldBlock()) {  // 不存在写锁并且需要阻塞
                    // Make sure we're not acquiring read lock reentrantly
                    /**
                     * 确认一下当前线程有没有在之前获得锁,也就是在阻塞前确认一下不是重入读锁的线程
                     * 如果是重入锁的话就让他操作CAS 如果不是的话就需要阻塞
                     * 至于为什么,我个人理解如下:
                     *    对公平锁来说,readShouldBlock()返回true,表明AQS队列中有等待写锁的线程,
                     *    那么如果重入读锁也返回-1让其阻塞的话那就会形成死锁,因为该重入读锁由于阻塞无法释放读锁,
                     *    AQS等待队列中的写锁又因为读锁的存在而无法获得写锁从而形成死锁了.
                     */
                    
                    if (firstReader == current) {  // 当前线程已经获得过锁则
                        // assert firstReaderHoldCount > 0;
                    } else {
                        if (rh == null) {
                            rh = cachedHoldCounter;
                            if (rh == null || rh.tid != getThreadId(current)) {
                                rh = readHolds.get();
                                if (rh.count == 0)  // 计数为0, 需要从readHolds中删除
                                    readHolds.remove();
                            }
                        }
                        if (rh.count == 0) //说明当前线程之前没有获得锁
                            return -1;
                    }
                }
                // 如果读锁的个数达到最大值抛出error
                if (sharedCount(c) == MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                // CAS操作 逻辑跟tryAcquireShared方法里面的类似.
                if (compareAndSetState(c, c + SHARED_UNIT)) {
                    if (sharedCount(c) == 0) {
                        firstReader = current;
                        firstReaderHoldCount = 1;
                    } else if (firstReader == current) {
                        firstReaderHoldCount++;
                    } else {
                        if (rh == null)
                            rh = cachedHoldCounter;
                        if (rh == null || rh.tid != getThreadId(current))
                            rh = readHolds.get();
                        else if (rh.count == 0)
                            readHolds.set(rh);
                        rh.count++;
                        cachedHoldCounter = rh; // cache for release
                    }
                    return 1;
                }
            }
        }

fullTryAcquireShared是为了处理tryAcquireShared中无法同时满足那三个条件而进行的处理方法, for循环操作.
1. 如果存在写锁并且写锁不是当前线程则需要阻塞当前线程返回-1.
2. 如果存在写锁并且写锁就是当前线程则不需要管readerShouldBlock()方法进行CAS操作.
3. 如果不存在写锁,如果当前线程之前获得过读锁则进行CAS操作,否则返回-1,阻塞当前线程.
4. 如果读锁的个数达到最大值则抛出Error.
5. 进行CAS操作.

对于第3点的个人理解

对公平锁来说,readShouldBlock()返回true,表明AQS队列中有等待写锁的线程,那么如果重入读锁也返回-1让其阻塞的话那就会形成死锁,因为该重入读锁由于阻塞无法释放读锁,AQS等待队列中的写锁又因为读锁的存在而无法获得写锁从而形成死锁了.

对应流程图如下:

fullTryAcquireShared.png

读锁的释放

/**
         *  作用: 释放读锁
         * @param unused
         * @return
         */
        protected final boolean tryReleaseShared(int unused) {
            Thread current = Thread.currentThread(); // 获取当前线程

            /**
             *  1. firstReader == current 表明当前线程是那个第一个获得读锁的线程,可以直接操作firstReaderHolderCount就可以了
             *  2. 如果不是则看是不是最后一次获得读锁的线程,
             *      a. 如果不是则取出当前线程对应的holdcount,保存到rh中
             *      b. 如果是直接保存到rh
             *
             *      如果rh的count为1,表明当前线程获得读锁后没有重入过,既然是释放锁,这个时候就需要从threadlocal中删除掉
             *
             *      rh.count--
             *
             */
            if (firstReader == current) {
                // assert firstReaderHoldCount > 0;
                if (firstReaderHoldCount == 1)
                    firstReader = null;
                else
                    firstReaderHoldCount--;
            } else {
                HoldCounter rh = cachedHoldCounter;
                // 不是最后一个获得读锁的线程,需要从threadlocal中也就是readHolds中取出当前线程的HoldCount
                if (rh == null || rh.tid != getThreadId(current))
                    rh = readHolds.get();
                int count = rh.count;
                // count <= 1 需要从readHolds中删除, 进一步如果count<=0表明错误
                if (count <= 1) {
                    readHolds.remove();
                    if (count <= 0)
                        throw unmatchedUnlockException();
                }
                // 无论怎么样 rh已经是当前线程对应的HoldCount, 释放一个就是减少一个
                --rh.count;
            }
            // 循环操作更新状态, 如果读锁的个数为0,则表明所有读锁都释放完毕这个时候返回true.
            // 不然其他情况都是返回false.
            for (;;) {
                int c = getState();
                int nextc = c - SHARED_UNIT;
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }

作用: 释放读锁,返回true如果所有的读锁释放完,否则返回false.
逻辑很简单,可以直接看代码注解.

锁降级

锁降级: 锁降级指的是写锁降级成为读锁。如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程不能称之为锁降级.锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程.

其实读锁这部分已经分析到了锁降级的内容了, 在上面的分析中我们已经看到在当前线程获取读锁的过程中如果存在写锁并且该写锁就是当前线程的时候可以去获得读锁.

必要性: 主要是为了保证数据的可见性,如果当前线程不获取读锁而是直接释放写锁(接着不加锁直接读取数据),假设此刻另一个线程(记作线程T)获取了写锁并修改了数据,那么当前线程无法感知线程T的数据更新. 如果当前线程获取读锁,即遵循锁降级的步骤,则线程T将会被阻塞,直到当前线程使用数据并释放读锁之后,线程T才能获取写锁进行数据更新。

Sync类中一些其余的方法

/**
         * 尝试获取写锁,该方法给tryLock调用,返回false该线程也不会阻塞
         */
        final boolean tryWriteLock() {
            Thread current = Thread.currentThread();
            int c = getState();
            if (c != 0) {
                int w = exclusiveCount(c);
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                if (w == MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
            }
            if (!compareAndSetState(c, c + 1))
                return false;
            setExclusiveOwnerThread(current);
            return true;
        }

        /**
         * 尝试获取读锁,该方法给tryLock调用,返回false该线程也不会阻塞
         */
        final boolean tryReadLock() {
            Thread current = Thread.currentThread();
            for (;;) {
                int c = getState();
                if (exclusiveCount(c) != 0 &&
                        getExclusiveOwnerThread() != current)
                    return false;
                int r = sharedCount(c);
                if (r == MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                if (compareAndSetState(c, c + SHARED_UNIT)) {
                    if (r == 0) {
                        firstReader = current;
                        firstReaderHoldCount = 1;
                    } else if (firstReader == current) {
                        firstReaderHoldCount++;
                    } else {
                        HoldCounter rh = cachedHoldCounter;
                        if (rh == null || rh.tid != getThreadId(current))
                            cachedHoldCounter = rh = readHolds.get();
                        else if (rh.count == 0)
                            readHolds.set(rh);
                        rh.count++;
                    }
                    return true;
                }
            }
        }

ReadLock 和 WriteLock

代码就不贴了,因为都是调用的Sync类的方法.

参考

1. Java并发编程的艺术
2. https://www.jianshu.com/p/6923c126e762
3. https://www.jianshu.com/p/f81925c8835a

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

推荐阅读更多精彩内容