ReentrantLock源码解析

首先来看ReentrantLock的公平锁实现源码

    Lock lock = new ReentrantLock(true);
    lock.lock();
    
    public void lock() {
        sync.lock();
    }
    
    /**
    *  lock方法调用acquire
    **/
    final void lock() {
        acquire(1);
    }

   /**
    *  acquire方法实现
    **/
    public final void acquire(int arg) {
        //tryAcquire(arg)尝试加锁,如果加锁失败则会调用acquireQueued方法加入队列去排队,如果加锁成功则不会调用
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

\color{red}{公平锁首先会调用tryAcquire去尝试加锁,当然这里的尝试加锁并不是直接加锁}

  • 第一步便是判断锁是不是自由状态,如果是则判断直接是否需要排队(hasQueuedPredecessors方法判断队列是否被初始化(如果没有初始化显然不需要排队,队列如果被初始化了,则自己有可能需要排队));如果hasQueuedPredecessors返回false,由于取反了故而不需要排队则进行Cas操作去上锁,如果需要排队则不会进入if分支当中,会直接返回false表示加锁失败
  • 第二步如果不是自由状态再判断是不是重入,如果不是重入则直接返回false加锁失败,如果是重入则把计数器+1

下面来一步步看源码

锁对象:其实就是ReentrantLock的实例对象,上述代码第一行中的lock对象就是所谓的锁
自由状态:自由状态表示锁对象没有被别的线程持有,计数器为0
计数器:再lock对象中有一个字段state用来记录上锁次数,比如lock对象是自由状态则state为0,如果大于零则表示被线程持有了,当然也有重入那么state则>1
waitStatus:仅仅是一个状态而已;ws是一个过渡状态,在不同方法里面判断ws的状态做不同的处理,所以ws=0有其存在的必要性
tail:队列的队尾
head:队列的对首
ts:第二个给lock加锁的线程
tf:第一个给lock加锁的线程
tc:当前给线程加锁的线程
tl:最后一个加锁的线程
tn:随便某个线程 当然这些线程有可能重复,比如第一次加锁的时候tf==tc==tl==tn
节点:就是上面的Node类的对象,里面封装了线程,所以某种意义上node就等于一个线程

tryAcquire(1)

        protected final boolean tryAcquire(int acquires) {
            //获取当前线程
            final Thread current = Thread.currentThread();
            //获取lock对象的上锁状态,如果锁是自由状态则=0,如果被上锁则为1,大于1表示重入
            int c = getState();
            if (c == 0) { //当前锁是自由状态,没有人占用
                //hasQueuedPredecessors,判断自己是否需要排队,不需要排队则返回false,取反则是true
                //不需要排队则进行cas尝试枷锁,如果加锁成功则把当前线程设置为拥有锁的线程
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            //如果C不等于0,而且当前线程不等于拥有锁的线程则不会进else if 直接返回false,加锁失败
            //如果C不等于0,但是当前线程等于拥有锁的线程则表示这是一次重入,那么直接把状态+1表示重入次数+1
            //那么这里也侧面说明了reentrantlock是可以重入的,因为如果是重入也返回true,也能lock成功
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

    protected final void setExclusiveOwnerThread(Thread thread) {
        exclusiveOwnerThread = thread;
    }

下面看hasQueuedPredecessors()方法

    public final boolean hasQueuedPredecessors() {
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }
排队结构图.png

可以想到有以下这么几种情况

第一种情况

队列没有初始化,不需要排队,直接去cas加锁,但是可能会失败
假设两个线程同时来lock,都看到队列没有初始化,都认为不需要排队,都去进行CAS修改计数器;但是某个线程t1先拿到锁,那么另外一个t2则会CAS失败,这个时候他就会初始化队列,并排队

第二种情况

队列被初始化了,但是thread过来加锁,发觉队列当中第一个排队的就是自己(比如重入)这个时候他也有可能不需要排队;为什么不需要排队?
因为队列当中第一个排队的线程他会去尝试获取一下锁,因为有可能这个时候持有锁锁的那个线程可能释放了锁,如果释放了就直接获取锁执行。但是如果没有释放他就会去排队。

  • h != t 判断首不等于尾这里要分三种情况

1、队列没有初始化,也就是第一个线程thread来加锁的时候那么这个时候队列没有初始化,h和t都是null,那么这个时候不等于不成立(false),直接返回。

2、队列被初始化了,后面我们会分析队列初始化的流程,如果队列被初始化那么h!=t则成立(假设队列里的数据大于1);h != t 返回true。
Node h = head; h.next赋值给s;s表示头的下一个,则表示他是队列当中参与排队的线程而且是排在最前面的;为什么是s最前面不是h嘛?诚然h是队列里面的第一个,但是不是排队的第一个;因为h是持有锁的,但是不参与排队(比如买火车票,第一个人已经在买票了,他后面才算排队)

因为h要么是虚拟出来的节点,要么是持有锁的节点
初始化的时候,会虚拟一个空的节点

然后判断s是否等于空,其实就是判断队列里面是否只有一个数据;假设队列大于1个,那么肯定不成立(s==null---->false),因为大于一个h.next肯定不为空;
由于是||运算如果返回false,还要判断s.thread != Thread.currentThread();这里又分为两种情况

  • 2.1 s.thread != Thread.currentThread() 返回true,就是当前线程不等于在排队的第一个线程s;
    方法最终返回true,那么则需要去排队。队列不为空,有人在排队,而且第一个排队的人和现在来参与竞争的人不是同一个,那么你就乖乖去排队

  • 2.2 s.thread != Thread.currentThread() 返回false 表示当前来参与竞争锁的线程和第一个排队的线程是同一个线程
    方法最终返回false,那么去则不需要去排队
    不需要排队则调用 compareAndSetState(0, acquires)去改变计数器尝试上锁;这里又分为两种情况

    • 2.2.1 第一种情况加锁成功
      假如这个时候h也就是持有锁的那个线程执行完了,释放锁了,那么加锁成功;成功则执行 setExclusiveOwnerThread(current); 然后返回true
    • 2.2.2 第二种情况加锁失败
      假如这个时候h也就是持有锁的那个线程没执行完,没释放锁,那么加锁失败
总结第二种情况

如果队列被初始化了,而且至少有一个人在排队那么自己也去排队;但是有个插曲;他会去看看那个第一个排队的人是不是自己,如果是自己那么他就去尝试加锁;尝试看看锁有没有释放

第三种情况

队列被初始化了,但是里面只有一个数据;什么情况下才会出现这种情况呢?
注意不是ts加锁的时候里面就只有一个数据,因为队列初始化的时候会虚拟一个h作为头结点,当前线程作为第一个排队的节点。
因为aqs认为h永远是不排队的,假设你不虚拟节点出来那么ts就是h,而ts其实需要排队的,因为这个时候tf可能没有执行完,ts得不到锁,故而他需要排队;
假设原先队列里面有5个人在排队,当前面4个都执行完了,轮到第五个线程得到锁的时候;他会把自己设置成为头部,而尾部又没有,故而队列当中只有一个h就是第五个,因为这个时候第五个线程已经不排队了,他拿到锁了,所以他不参与排队,故而需要设置成为h;即头部;所以这个时间内,队列当中只有一个节点,这个时候队列已经初始化了,但是只有一个数据,并且这个数据所代表的线程是持有锁
h != t false返回false

第三种情况总结:

如果队列当中只有一个节点,而这种情况我们分析了,这个节点就是当前持有锁的那个节点,故而我不需要排队,进行cas;
如果持有锁的线程释放了锁,那么我能成功上锁

加锁过程总结

如果是第一个线程tf,那么和队列无关,线程直接持有锁。并且也不会初始化队列,如果接下来的线程都是交替执行,那么永远和AQS队列无关,都是直接线程持有锁,如果发生了竞争,比如tf持有锁的过程中T2来lock,那么这个时候就会初始化AQS,初始化AQS的时候会在队列的头部虚拟一个Thread为NULL的Node,因为队列当中的head永远是持有锁的那个node(除了第一次会虚拟一个,其他时候都是持有锁的那个线程锁封装的node),现在第一次的时候持有锁的是tf而tf不在队列当中所以虚拟了一个node节点,队列当中的除了head之外的所有的node都在park,当tf释放锁之后unpark某个(基本是队列当中的第二个,为什么是第二个呢?前面说过head永远是持有锁的那个node,当有时候也不会是第二个,比如第二个被cancel之后,至于为什么会被cancel,不在我们讨论范围之内,cancel的条件很苛刻,基本不会发生)node之后,node被唤醒,假设node是t2,那么这个时候会首先把t2变成head(sethead),在sethead方法里面会把t2代表的node设置为head,并且把node的Thread设置为null,为什么需要设置null?其实原因很简单,现在t2已经拿到锁了,node就不要排队了,那么node对Thread的引用就没有意义了。所以队列的head里面的Thread永远为null。

我们再来看看acquire方法方法
    public final void acquire(int arg) {
        //tryAcquire(arg)尝试加锁,如果加锁失败则会调用acquireQueued方法加入队列去排队,如果加锁成功则不会调用
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

下面解析下acquireQueued(addWaiter(Node.EXCLUSIVE), arg)方法
如果代码能执行到这里,说明当前线程tc需要排队

  • 需要排队有两种情况---换言之代码能够执行到这里有两种情况:
    1、tf持有了锁,并没有释放,所以tc来加锁的时候需要排队,但这个时候队列并没有初始化
    2、tn(无所谓哪个线程,反正就是一个线程)持有了锁,那么由于加锁tn!=tf(tf是属于第一种情况,我们现在不考虑tf了),所以队列是一定被初始化了的,tc来加锁,那么队列当中有人在排队,故而他也去排队

先看addWaiter(Node.EXCLUSIVE)方法

//node的结构
public class Node{
    volatile Node prev;
    volatile Node next;
    volatile Thread thread;
}

Node.EXCLUSIVE = null;

    private Node addWaiter(Node mode) {
    //由于AQS队列当中的元素类型为Node,故而需要把当前线程tc封装成为一个Node对象,下文我们叫做nc
    Node node = new Node(Thread.currentThread(), mode);
    //tail为对尾,赋值给pred 
    Node pred = tail;
    //判断pred是否为空,其实就是判断对尾是否有节点,其实只要队列被初始化了队尾肯定不为空,假设队列里面只有一个元素,那么对尾和对首都是这个元素
    //换言之就是判断队列有没有初始化
    //上面我们说过代码执行到这里有两种情况,1、队列没有初始化和2、队列已经初始化了
    //pred不等于空表示第二种情况,队列被初始化了,如果是第二种情况那比较简单
   //直接把当前线程封装的nc的上一个节点设置成为pred即原来的对尾
   //继而把pred的下一个节点设置为当nc,这个nc自己成为对尾了
    if (pred != null) {
        //直接把当前线程封装的nc的上一个节点设置成为pred即原来的对尾,对应 10行的注释
        node.prev = pred;
        //这里需要cas,因为防止多个线程加锁,确保nc入队的时候是原子操作
        if (compareAndSetTail(pred, node)) {
            //继而把pred的下一个节点设置为当nc,这个nc自己成为对尾了 对应第11行注释
            pred.next = node;
            //然后把nc返回出去
            return node;
        }
    }
    //如果上面的if不成了就会执行到这里,表示第一种情况队列并没有初始化---下面32行解析这个方法
    enq(node);
    //返回nc
    return node;
}



private Node enq(final Node node) {//这里的node就是当前线程封装的node也就是nc
    //死循环
    for (;;) {
        //对尾复制给t,上面已经说过队列没有初始化,故而第一次循环t==null(因为是死循环,因此强调第一次,后面可能还有第二次、第三次,每次t的情况肯定不同)
        Node t = tail;
        //第一次循环成了成立
        if (t == null) { // Must initialize
            //new Node就是实例化一个Node对象下文我们称为nn,调用无参构造方法实例化出来的Node里面三个属性都为null
            //入队操作--compareAndSetHead继而把这个nn设置成为队列当中的头部,cas防止多线程、确保原子操作;记住这个时候队列当中只有一个,即nn
            if (compareAndSetHead(new Node()))
                //这个时候AQS队列当中只有一个元素,即头部=nn,所以为了确保队列的完整,设置头部等于尾部,即nn即是头也是尾
                //然后第一次循环结束
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

//为了方便 第二次循环我再贴一次代码来对第二遍循环解释
private Node enq(final Node node) {//这里的node就是当前线程封装的node也就是nc
    //死循环
    for (;;) {
        //对尾复制给t,由于第二次循环,故而tail==nn,即new出来的那个node
        Node t = tail;
        //第二次循环不成立
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            //不成立故而进入else
            //首先把nc,当前线程所代表的的node的上一个节点改变为nn,因为这个时候nc需要入队,入队的时候需要把关系维护好
            //所谓的维护关系就是形成链表,nc的上一个节点只能为nn,这个很好理解
            node.prev = t;
            //入队操作--把nc设置为对尾,对首是nn,
            if (compareAndSetTail(t, node)) {
                //上面我们说了为了维护关系把nc的上一个节点设置为nn
                //这里同样为了维护关系,把nn的下一个节点设置为nc
                t.next = node;
                //然后返回t,即nn,死循环结束,也就是24行放回了,这个返回其实就是为了终止循环,返回出去的t,没有意义,你可以查看24行代码。
                return t;
            }
        }
    }
}

  //这个方法已经解释完成了
  enq(node);
  //返回nc,不管哪种情况都会返回nc;到此addWaiter方法解释完成
  return node;


总结

addWaiter方法就是让nc入队-并且维护队列的链表关系,但是由于情况复杂做了不同处理
主要针对队列是否有初始化,没有初始化则new一个新的Node nn作为对首,nn里面的线程为null

acquireQueued(addWaiter(Node.exclusive),arg))经过上面的解析之后可以理解成为
acquireQueued(nc,arg))

final boolean acquireQueued(final Node node, int arg) {//这里的node 就是当前线程封装的那个node 下文叫做nc
    //记住标志很重要
    boolean failed = true;
    try {
        //同样是一个标志
        boolean interrupted = false;
        //死循环
        for (;;) {
            //获取nc的上一个节点,有两种情况;1、上一个节点为头部;2上一个节点不为头部
            final Node p = node.predecessor();
            //如果nc的上一个节点为头部,则表示nc为队列当中的第二个元素,为队列当中的第一个排队人;
            //如果nc为队列当中的第二个元素,第一个排队的则调用tryAcquire去尝试假设
            //只有nc为第二个元素;第一个排队的情况下才会尝试加锁,其他情况直接去park了,因为第一个排队的执行到这里的时候需要看看持有有锁的线程有没有释放锁,释放了就轮到我了,就不park了
            //有人会疑惑说开始调用tryAcquire加锁失败了(需要排队),这里为什么还要进行tryAcquire不是重复了吗?
            //其实不然,因为第一次tryAcquire判断是否需要排队,如果需要排队,那么我就入队;当我入队之后我发觉前面那个人就是第一个,那么我不死心,再次问问前面那个人搞完没有
            //如果搞完了,我就不park,接着他搞;如果他没有搞完,那么我则在队列当中去park,等待别人叫我
            //但是如果我去排队,发觉前面那个人在睡觉,前面那个人都在睡觉,那么我也睡觉把
            if (p == head && tryAcquire(arg)) {
                //能够执行到这里表示我来加锁的时候,锁被持有了,我去排队,进到队列当中的时候发觉我前面那个人没有park,前面那个人就是当前持有锁的那个人,那么我问问他搞完没有
                //能够进到这个里面就表示前面那个人搞完了;所以这里能执行到的几率比较小;但是在高并发的世界中这种情况真的需要考虑
                //如果我前面那个人搞完了,我nc得到锁了,那么前面那个人直接出队列,我自己则是对首;这行代码就是设置自己为对首
                setHead(node);
                //这里的P代表的就是刚刚搞完事的那个人,由于他的事情搞完了,要出队;怎么出队?把链表关系删除
                p.next = null; // help GC
                //设置表示---记住记加锁成功的时候为false
                failed = false;
                //返回false;为什么返回false 为了不调用50行---acquire方法当中的selfInterrupt方法;为什么不调用?下次解释比较复杂
                return interrupted;
            }
            //进到这里分为两种情况
            //1、nc的上一个节点不是头部,说白了,就是我去排队了,但是我上一个人不是队列第一个
            //2、第二种情况,我去排队了,发觉上一个节点是第一个,但是他还在搞事没有释放锁
            //不管哪种情况这个时候我都需要park,park之前我需要把上一个节点的状态改成park状态
            //这里比较难以理解为什么我需要去改变上一个节点的park状态呢?每个node都有一个状态,默认为0,表示无状态
            //-1表示在park;但是不能自己把自己改成-1状态?为什么呢?因为你得确定你自己park了才是能改为-1;不然你自己改成自己为-1;但是改完之后你没有park那不就骗人?(原子性)
            //所以只能先park;在改状态;但是问题你自己都park了;完全释放CPU资源了,故而没有办法执行任何代码了,所以只能别人来改;故而可以看到每次都是自己的后一个节点把自己改成-1状态
            if (shouldParkAfterFailedAcquire(p, node) &&
                //改上一个节点的状态成功之后;自己park;到此加锁过程说完了
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}



    //cas将上一个节点的ws改为-1
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        //上一个节点,肯定为0
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)//-1
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;
        if (ws > 0) {
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             * waitStatus must be 0 or PROPAGATE.  Indicate that we
             * need a signal, but don't park yet.  Caller will need to
             * retry to make sure it cannot acquire before parking.
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }



public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}



    static void selfInterrupt() {
        Thread.currentThread().interrupt();
    }

公平锁lock方法到此结束,下面看看unlock方法

    public void unlock() {
        sync.release(1);
    }


    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            //释放锁,
            Node h = head;
            //如果后面还有线程等待,则h.waitStatus = -1
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }


       
        protected final boolean tryRelease(int releases) {
            //因为是重入锁,因此需要减一
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                //当持有锁数量为0时,tc才是真正释放了锁
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }



    private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        Node s = node.next;
        ////若后续节点为空或已被cancel,则从尾部开始找到队列中第一个waitStatus<=0,即未被cancel的节点
        if (s == null || s.waitStatus > 0) {
            s = null;
            //队尾不为空并且队尾不等于队头
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            LockSupport.unpark(s.thread);
    }

下面看看非公平锁的源码和公平锁的区别

        //直接cas操作否则acquire(1), 公平锁直接执行方法acquire(1)
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }


    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

这里看到公平锁和非公平锁的都会入队,排队,非公平锁会首先cas尝试加锁,在进行该操作,两者还有一个区别就是acquire(arg)方法

        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                //少了一步 !hasQueuedPredecessors()  判断自己是否需要排队
                //公平锁需要先判断自己是否需要排队,不需要排队才会cas操作尝试能否加锁成功
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }



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

推荐阅读更多精彩内容