Java并发之JUC-AQS(3)

1、简介

JUC指的是java.util.concurrent包。

AQS是AbstractQueuedSynchronizerAbstractQueuedLongSynchronizer 这两个类的缩写。
AbstractQueuedSynchonizerAbstractQueuedLongSynchronizer的唯一区别就是其内部state变量的类型:

  • AbstractQueuedSynchonizer类里的state变量是int类型
  • AbstractQueuedLongSynchronizer 里的state变量是long类型

AQS在Java里的地位极其重要,它是Java并发包的基础提供类,是实现CountDownLatchReentrantLockReentrantReadWriteLock里两个内部类,读锁和写锁SemaphoreFutureTask等类的基础!是实现各类同步器的基础组件!

AQS解决了在实现同步器时涉及的大量细节问题,例如等待线程采用FIFO队列操作的顺序。在不同的同步器中还可以定义一些灵活的标准来判断某个线程是应该通过还是等待。

我们直接从AbstractQueuedSynchronizer类开始说起,然后我们再以阅读AQS的拓展组件源码为切入点,了解各组件是怎么使用AQS满足自己功能需求的。相信我,这两章跟下来,你会收获满满~

本章节应该会很长,但是我会力求精简、易懂、力求我能讲明白,读者能读懂!我们一起加油~

2、AQS内部属性、内部类及方法

AQS-UML.jpg

上面是AQS的UML图,方便大家了解来龙去脉。

AbstractOwnableSynchronizer类,可由线程独占的同步器。这个类为创建锁和相关同步器提供了基础,这可能需要一个所有权的概念。
这个类本身不管理或使用此信息。但是,子类和工具可以使用,适当维护有助于控制和监视访问并提供诊断。

AQS是一个抽象类,不允许被实例化,设计者的初衷可能就是为了让子类继承来实现功能多样性的。

2.1、AQS内部有五个非常重要的属性:

  • head 头结点,也就是当前持有锁的线程节点
  • tail CLH队列的尾部节点,每一个新节点进入,都插入到伪链表的尾部
  • state 最为重要的属性,这个整数可以用于表示任意状态。
    • ReentrantLock用它来表示所有者线程已经重复获取该锁的次数
    • Semaphore用它来表示剩余的许可数量
    • FutureTask用它来表示任务的状态(尚未开始、正在运行、已完成、已取消)
  • spinForTimeoutThreshold 自旋超时阀值,粗略的估计足以在非常短的超时情况下提高响应能力
    这个属性定义了自旋的阀值,在doAcquireSharedNanos()等方法中有使用到。
    如果用户定义的等待时间超过这个阀值,那么线程将阻塞,在阻塞期间如果能够等到唤醒的机会并tryAcquireShared成功则返回true,否则返回false,超时也返回false。
    如果用户定于的等待时间小于等于这个阀值,则会无限循环线程不阻塞,直到有线程释放同步状态或者超时,然后返回对应的结果。
  • exclusiveOwnerThread 这是通过继承AbstractOwnableSynchronizer类,获得的属性,表示独占模式下的同步器持有者

2.2、AQS 未实现的方法,也就是说这些方法交由子类去实现

  • boolean tryAcquire(int) 尝试获取操作
  • boolean tryRelease(int) 尝试释放同步状态
  • int tryAcquireShared(int) 共享的方式尝试获取操作
  • boolean tryReleaseShared(int) 共享的方式尝试释放
  • boolean isHeldExclusively()判断当前是否为独占锁

可以选择其中一部分进行覆写,但是要保持实现逻辑完整,不能穿插实现。根据实现方式不同,分为独占锁和共享锁:

  • 独占锁 ReentrantLock、ReentrantReadWriteLock.WriteLock
  • 共享锁 ReentrantReadWriteLock.ReadLock、CountDownLatch、CyclicBarrier、Semaphore

实现策略为:

  • 独占锁,实现 tryAcquire(int)tryRelease(int)isHeldExclusively()
  • 共享锁,实现tryAcquireShared(int)tryReleaseShared(int)

2.3、AQS内部类 ConditionObject

它实现了Condition接口可用于线程间的通信协作,上一章节我们有提到过。

2.4、AQS内部CLH队列

AQS在其内部还维护了一个FIFO队列,AQS的同步机制就是依靠这个CLH队列完成的。CLH是FIFO的双端双向队列,如下图:

AQS-Node.jpg

Node定义以下常量:

    // 类别1
    static final Node SHARED = new Node();
    static final Node EXCLUSIVE = null;

    // 类别2
    static final int CANCELLED = 1;  // 当前线程因为超时或者中断被取消。这是一个终结态,也就是状态到此为止。
    static final int SIGNAL = -1;    // 表示当前线程的后继线程被阻塞或即将被阻塞,当前线程释放锁或者取消后需要唤醒后继线程。这个状态一般都是后继节点设置前驱节点的
    static final int CONDITION = -2; // 表示当前线程在Condition队列中
    static final int PROPAGATE = -3; // 用于将唤醒后继线程传递下去,这个状态的引入是为了完善和增强共享锁的唤醒机制。在一个节点成为头节点之前,是不会跃迁为此状态的
    0 // 无状态

上面的这些常量是为了设置下面这些属性:

// 当前等待状态
volatile int waitStatus;
Node nextWaiter;

SHAREDEXCLUSIVE用于设置nextWaiter,用于表示当前节点是共享的还是独占的。分别用于独占锁和共享锁。
CANCELLEDSIGNALCONDITIONPROPAGATE用于设置waitStatus

2.5 总结

  • 通过上面的介绍,我们应该猜能测出,AQS同步器其实就是主要做了三件事:

    • 管理同步状态
    • 线程的阻塞与唤醒
    • 同步队列的维护
  • AQS内部维护的headtail两个Node引用

    其中head在逻辑上代表当前持有锁的线程,且head节点是不存储thread线程信息和前驱节点信息的,当唤醒后继节点后,后继节点线程会将后继节点设置为head,并将对节点内的prevthread属性设置为null。
    这两个属性是延迟初始化的,也就是说在第一次且第一个线程持有锁时,第二个线程因为获取失败,进入同步队列时会对headtail进行初始化,也就是说在所有线程都能获取到锁时,其内部的headtail都为null,一旦headtail被初始化后,即使没有线程持有锁,其内部的headtail 依然保留最后一个持有锁的线程节点!(headtail都指向一个内存地址)

  • 当一个线程获取锁失败而被加入到同步队列时,会用CAS来设置尾节点tail为当前线程对应的Node节点。

  • AQS内部的cas操作,都是依赖Unsafe类的,自Java9之后的版本,Unsafe类被移除,取而代之的是VarHandle类。

3、思路

AQS中获取操作和释放操作的标准形式:

  boolean acquire() throws InterruptedException {
    while(当前状态不允许获取操作) {
      if(需要阻塞获取请求) {
        如果当前线程不在队列中,则将气插入队列
        阻塞当前线程
      }
      else
        返回失败
    }
    可能更新同步器的状态
    如果线程位于队列中,则将其移出队列
    返回成功
  }

---------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------

  void release() {
    更新同步器的状态
    if (新的状态允许某个被阻塞的线程获取成功)
      接触队列中一个或多个线程的阻塞状态
  }

3.1 独占锁实现思路

获取锁:

代码:

    /**
     * 获取独占锁,忽略中断。
     * 首先尝试获取锁,如果成功,则返回true;否则会把当前线程包装成Node插入到队列中,在队列中会检测是否为head的直接后继,并尝试获取锁,
     * 如果获取失败,则会通过LockSupport阻塞当前线程,直至被释放锁的线程唤醒或者被中断,随后再次尝试获取锁,如此反复。被唤醒后继续之前的代码执行
     */
    public final void acquire(int arg) {
        if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
    ---------------------------------------------------------------------------------------
    其中tryAcquire()方法需要由子类实现,ReentrantLock通过覆写这个方法实现了公平锁和非公平锁
    ---------------------------------------------------------------------------------------

    /**
     * 在同步队列中插入节点。
     */
    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        Node pred = tail;
        // 判断尾节点是否为null
        if (pred != null) {
            node.prev = pred;
            // 通过CAS在队尾插入当前节点
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        // 为null 将节点插入队列,必要时进行初始化
        enq(node);
        return node;
    }
    
    /**
     * 通过无限循环和CAS操作在队列中插入一个节点成功后返回。
     * 将节点插入队列,必要时进行初始化
     */
    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            // 初始化head和tail
            if (t == null) {
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                /* CAS设置tail为node
                 * 表面上看是把老tail的next连接到node。
                 * 如果同步队列head节点和tail节点刚刚被这个线程初始化,实际上也把head的next也连接到了node,而老tail属性被node覆盖了。
                 * 反之则是,把老tail的next连接到node,head并没有与node产生连接,这样就形成了链表
                 */
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

    /**
     * 在队列中的节点通过此方法获取锁,忽略中断。
     * 这个方法很重要,如果上述没有获取到锁,将线程包装成Node节点加入到同步队列的尾节点,然后看代码里的注释
     */
    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                /*
                 * 检测当前节点前驱是否head,这是试获取锁。
                 * 如果是的话,则调用tryAcquire尝试获取锁,
                 * 成功,则将head置为当前节点。原head节点的next被置为null等待垃圾回收
                 */
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                /*
                 * 如果未成功获取锁则根据前驱节点判断是否要阻塞。
                 * 如果阻塞过程中被中断,则置interrupted标志位为true。
                 * shouldParkAfterFailedAcquire方法在前驱状态不为SIGNAL的情况下都会循环重试获取锁。
                 */
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

    /**
     * 根据前驱节点中的waitStatus来判断是否需要阻塞当前线程。
     */
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            /*
             * 前驱节点设置为SIGNAL状态,在释放锁的时候会唤醒后继节点,
             * 所以后继节点(也就是当前节点)现在可以阻塞自己。
             */
            return true;
        if (ws > 0) {
            /*
             * 前驱节点状态为取消,向前遍历,更新当前节点的前驱为往前第一个非取消节点。
             * 当前线程会之后会再次回到循环并尝试获取锁。
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
             /**
              * 等待状态为0或者PROPAGATE(-3),设置前驱的等待状态为SIGNAL,
              * 并且之后会回到循环再次重试获取锁。
              */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }
    
    /**
     * 该方法实现某个node取消获取锁。
     */
    private void cancelAcquire(Node node) {
       if (node == null)
           return;
    
       node.thread = null;
    
       // 遍历并更新节点前驱,把node的prev指向前部第一个非取消节点。
       Node pred = node.prev;
       while (pred.waitStatus > 0)
           node.prev = pred = pred.prev;
    
       // 记录pred节点的后继为predNext,后续CAS会用到。
       Node predNext = pred.next;
    
       // 直接把当前节点的等待状态置为取消,后继节点即便也在cancel可以跨越node节点。
       node.waitStatus = Node.CANCELLED;
    
       /*
        * 如果CAS将tail从node置为pred节点了
        * 则剩下要做的事情就是尝试用CAS将pred节点的next更新为null以彻底切断pred和node的联系。
        * 这样一来就断开了pred与pred的所有后继节点,这些节点由于变得不可达,最终会被回收掉。
        * 由于node没有后继节点,所以这种情况到这里整个cancel就算是处理完毕了。
        *
        * 这里的CAS更新pred的next即使失败了也没关系,说明有其它新入队线程或者其它取消线程更新掉了。
        */
       if (node == tail && compareAndSetTail(node, pred)) {
           compareAndSetNext(pred, predNext, null);
       } else {
           // 如果node还有后继节点,这种情况要做的事情是把pred和后继非取消节点拼起来。
           int ws;
           if (pred != head &&
               ((ws = pred.waitStatus) == Node.SIGNAL ||
                (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
               pred.thread != null) {
               Node next = node.next;
               /* 
                * 如果node的后继节点next非取消状态的话,则用CAS尝试把pred的后继置为node的后继节点
                * 这里if条件为false或者CAS失败都没关系,这说明可能有多个线程在取消,总归会有一个能成功的。
                */
               if (next != null && next.waitStatus <= 0)
                   compareAndSetNext(pred, predNext, next);
           } else {
               /*
                * 这时说明pred == head或者pred状态取消或者pred.thread == null
                * 在这些情况下为了保证队列的活跃性,需要去唤醒一次后继线程。
                * 举例来说pred == head完全有可能实际上目前已经没有线程持有锁了,
                * 自然就不会有释放锁唤醒后继的动作。如果不唤醒后继,队列就挂掉了。
                * 
                * 这种情况下看似由于没有更新pred的next的操作,队列中可能会留有一大把的取消节点。
                * 实际上不要紧,因为后继线程唤醒之后会走一次试获取锁的过程,
                * 失败的话会走到shouldParkAfterFailedAcquire的逻辑。
                * 那里面的if中有处理前驱节点如果为取消则维护pred/next,踢掉这些取消节点的逻辑。
                */
               unparkSuccessor(node);
           }
           
           /*
            * 取消节点的next之所以设置为自己本身而不是null,
            * 是为了方便AQS中Condition部分的isOnSyncQueue方法,
            * 判断一个原先属于条件队列的节点是否转移到了同步队列。
            *
            * 因为同步队列中会用到节点的next域,取消节点的next也有值的话,
            * 可以断言next域有值的节点一定在同步队列上。
            *
            * 在GC层面,和设置为null具有相同的效果。
            */
           node.next = node; 
       }
    }
    
    /**
     * 唤醒后继线程。
     */
    private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
        // 尝试将node的等待状态置为0,这样的话,后继争用线程可以有机会再尝试获取一次锁。
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
    
        Node s = node.next;
        /*
         * 这里的逻辑就是如果node.next存在并且状态不为取消,则直接唤醒s即可
         * 否则需要从tail开始向前找到node之后最近的非取消节点。
         *
         * 这里为什么要从tail开始向前查找也是值得琢磨的:
         * 如果读到s == null,不代表node就为tail,参考addWaiter以及enq函数中的我的注释。
         * 不妨考虑到如下场景:
         * 1. node某时刻为tail
         * 2. 有新线程通过addWaiter中的if分支或者enq方法添加自己
         * 3. compareAndSetTail成功
         * 4. 此时这里的Node s = node.next读出来s == null,但事实上node已经不是tail,它有后继了!
         */
        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);
    }
释放锁:

对于释放独占锁,会调用tryRelaes(int)方法,该方法由子类实现,在完全释放掉锁后,释放掉锁的线程会将后继线程唤醒,后继线程进行锁争用。

代码:

    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            /*
            * 此时的head节点可能有3种情况:
            * 1. null (AQS的head延迟初始化+无竞争的情况)
            * 2. 当前线程在获取锁时new出来的节点通过setHead设置的
            * 3. 由于通过tryRelease已经完全释放掉了独占锁,有新的节点在acquireQueued中获取到了独占锁,并设置了head
            * 第三种情况可以再分为两种情况:
            * (一)时刻1:线程A通过acquireQueued,持锁成功,set了head
            *      时刻2:线程B通过tryAcquire试图获取独占锁失败失败,进入acquiredQueued
            *      时刻3:线程A通过tryRelease释放了独占锁
            *      时刻4:线程B通过acquireQueued中的tryAcquire获取到了独占锁并调用setHead
            *      时刻5:线程A读到了此时的head实际上是线程B对应的node
            * (二)时刻1:线程A通过tryAcquire直接持锁成功,head为null
            *      时刻2:线程B通过tryAcquire试图获取独占锁失败失败,入队过程中初始化了head,进入acquiredQueued
            *      时刻3:线程A通过tryRelease释放了独占锁,此时线程B还未开始tryAcquire
            *      时刻4:线程A读到了此时的head实际上是线程B初始化出来的傀儡head
            * 以上这段是从网上某位大神那里摘抄的。甚是经典,这AQS的源码真真的是精妙无比啊!
            */
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

3.2 获取共享锁实现思路

获取锁:

与获取独占锁不同,关键在于,共享锁可以被多个线程持有。
如果需要AQS实现共享锁,在实现tryAcquireShared()方法时,返回负数,表示获取失败;返回0,表示获取成功,但是后继争用线程不会成功;返回正数,表示获取成功,表示后继争用线程也可能成功。

代码:

    public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }
    
    private void doAcquireShared(int arg) {
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    // 一旦共享获取成功,设置新的头结点,并且唤醒后继线程
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
    
    /**
     * 这个函数做的事情有两件:
     * 1. 在获取共享锁成功后,设置head节点
     * 2. 根据调用tryAcquireShared返回的状态以及节点本身的等待状态来判断是否要需要唤醒后继线程。
     */
    private void setHeadAndPropagate(Node node, int propagate) {
        // 把当前的head封闭在方法栈上,用以下面的条件检查。
        Node h = head;
        setHead(node);
        /*
         * propagate是tryAcquireShared的返回值,这是决定是否传播唤醒的依据之一。
         * h.waitStatus为SIGNAL或者PROPAGATE时也根据node的下一个节点共享来决定是否传播唤醒,
         */
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }
    
    /**
     * 这是共享锁中的核心唤醒函数,主要做的事情就是唤醒下一个线程或者设置传播状态。
     * 后继线程被唤醒后,会尝试获取共享锁,如果成功之后,则又会调用setHeadAndPropagate,将唤醒传播下去。
     * 这个函数的作用是保障在acquire和release存在竞争的情况下,保证队列中处于等待状态的节点能够有办法被唤醒。
     */
    private void doReleaseShared() {
        /*
         * 以下的循环做的事情就是,在队列存在后继线程的情况下,唤醒后继线程;
         * 或者由于多线程同时释放共享锁由于处在中间过程,读到head节点等待状态为0的情况下,
         * 虽然不能unparkSuccessor,但为了保证唤醒能够正确稳固传递下去,设置节点状态为PROPAGATE。
         * 这样的话获取锁的线程在执行setHeadAndPropagate时可以读到PROPAGATE,从而由获取锁的线程去释放后继等待线程。
         */
        for (;;) {
            Node h = head;
            // 如果队列中存在后继线程。
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;
                    unparkSuccessor(h);
                }
                // 如果h节点的状态为0,需要设置为PROPAGATE用以保证唤醒的传播。
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;
            }
            // 检查h是否仍然是head,如果不是的话需要再进行循环。
            if (h == head)
                break;
        }
    }
释放锁:

释放共享锁与获取共享锁的代码都使用了doReleaseShared(int)
代码:

    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            // doReleaseShared的实现上面获取共享锁已经介绍
            doReleaseShared();
            return true;
        }
        return false;
    }

我一直都在试图应该怎样去阐释AQS,想了很多,最后想到,我们可以把AQS想象为发动机(假设全世界所有车辆都使用这个AQS发动机),AQS提供动力,而各个品牌各类型的车辆,需要针对自己的定位,去定制化对发动机的使用。

比如:大牛,需要提高发动机的排量,一脚油门下去,要有推背感,不考虑油耗;而居家的卡罗拉,排量最好2.0以下,一脚油门下去.....经济性排在第一位。不知道这样比喻恰不恰当...

这一章节,我会随着自己不断对AQS学习的理解,会随时进行补充和更正的。

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

推荐阅读更多精彩内容