并發(fā)系列四:基于兩種案例來認(rèn)識(shí)ReentrantLock源碼加鎖過程(公平鎖)

前言

  • 上篇文章咱們證明了synchronized關(guān)鍵字的特性:無鎖、偏向鎖、輕量鎖、重(chong)偏向、重(chong)輕量、重量鎖。可以說synchronized是jvm層面實(shí)現(xiàn)同步的方式。在jdk中,存在一個(gè)叫java.util.concurrent的包,簡(jiǎn)稱JUC,它是一個(gè)jdk層面的并發(fā)包,里面存在了大量與并發(fā)編程相關(guān)的api,其中最代表意義的就是atomic和lock兩種類別,前者是基于樂觀鎖CAS(Compare And Swap)的實(shí)現(xiàn),后者是基于AQS(Abstract Queued Synchronizer)實(shí)現(xiàn)。本文將詳細(xì)講解下AQS原理以及根據(jù)兩個(gè)案例來解讀ReentrantLock源碼。
  • 兩個(gè)案例:

    1.線程A單獨(dú)加鎖
    2.線程A正在持有鎖的過程中,線程t1來加鎖

一、AQS原理

  • AQS簡(jiǎn)稱Abstract Queued Synchronizer,它的核心是基于一個(gè)雙向鏈表組成的隊(duì)列(CLH隊(duì)列) + volatile關(guān)鍵字修飾的int類型變量實(shí)現(xiàn)的。(關(guān)于volatile關(guān)鍵字可以參考其他博主的一些總結(jié): 傳送門),大致核心可以以如下圖來呈現(xiàn):

    在這里插入圖片描述

    簡(jiǎn)單總結(jié)就是:內(nèi)部使用雙向鏈表維護(hù)了一個(gè)隊(duì)列,其中Node數(shù)據(jù)結(jié)構(gòu)為此隊(duì)列的基石,內(nèi)部維護(hù)了prev(指向上一個(gè)節(jié)點(diǎn))、next(指向下一個(gè)節(jié)點(diǎn))、waitStatus(當(dāng)前node的狀態(tài))、thread(當(dāng)前維護(hù)的線程)四個(gè)重要的屬性。其中waitStatus分別有如下取值:

    Node中waitStatus具體取值 含義
    CANCELLED(1) 中斷或取消,此狀態(tài)下的節(jié)點(diǎn)會(huì)從隊(duì)列中移除
    SIGNAL(-1) 此狀態(tài)下的節(jié)點(diǎn)一定是在隊(duì)列排隊(duì)中
    CONDITION(-2) 條件阻塞,比如說內(nèi)部因Condition而阻塞的節(jié)點(diǎn)
    PROPAGATE(-3) 表示下一個(gè)acquireShared應(yīng)該無條件傳播
    0 默認(rèn)狀態(tài)

    除此之外,隊(duì)列中還維護(hù)了三個(gè)屬性,head(指向隊(duì)列中的頭節(jié)點(diǎn))、state(鎖的狀態(tài))、tail(指向隊(duì)列中的尾節(jié)點(diǎn))。其中,state的取值有兩種情況,將以如下表展示出來:

    AQS中state具體取值 含義
    0 表示當(dāng)前鎖沒有被線程持有
    1 表示當(dāng)前鎖正在被線程持有
    大于1 表示當(dāng)前鎖被線程重入了(重入鎖),這里要注意:ReentrantLock重入了幾次,就要釋放幾次鎖

二、案例1:線程A單獨(dú)加鎖

  • 代碼如下:

    public class SimpleThreadLock {
    
        static ReentrantLock lock = new ReentrantLock(true);
    
        public static void main(String[] args) throws InterruptedException {
            Thread a = new Thread(() -> {
                try {
                    lock.lock();
                    System.out.println("Get lock");
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }, "線程a");
    
            a.start();
            a.join();
            System.out.println("end");
        }
    }
    

    代碼也比較簡(jiǎn)單,就是在主線程中創(chuàng)建了一個(gè)線程,并且內(nèi)部去使用ReentrantLock加鎖,獲取到鎖后就打印出Get lock這句話,當(dāng)t1線程執(zhí)行完后再繼續(xù)執(zhí)行主線程的邏輯。這里就不一步步演示斷點(diǎn)了,直接上源碼。

  • 這里先說明下ReentrantLock重載的兩個(gè)構(gòu)造方法

    // 默認(rèn)非公平鎖
    public ReentrantLock() {
        sync = new NonfairSync();
    }
    
    // 若傳入true則是公平鎖
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
    

    因?yàn)樵蹅儌魅肓?strong>true進(jìn)去,所以此時(shí),它是一把公平鎖。

  • lock.lock()方法,因?yàn)樵蹅冎付耸褂霉芥i,所以最終會(huì)進(jìn)入ReentrantLock內(nèi)部維護(hù)的FairSync類的lock方法

    // FairSync類下的lock方法
    final void lock() {
        acquire(1);
    }
    

    于是,我們需要找到acquire方法,此方法為AQS(父類AbstractQueuedSynchronizer)的方法,所以最終會(huì)進(jìn)入如下這么一段代碼:

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

    這段代碼,看似很精簡(jiǎn),但是它做的事真的太多了。濃縮的才是精華呀!好了,咱們不偏題,繼續(xù)按照咱們的主題:線程A單獨(dú)加鎖。不過要繼續(xù)往下看,還是要加深下acquire方法的含義,我們必須要tryAcquire方法返回false,才能繼續(xù)走if條件中后面的邏輯,以及if條件內(nèi)部的邏輯。于是,我們直接看tryAcquire方法源碼:

  • tryAcquire方法

    protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }
    

    tryAcquire方法是一個(gè)protected方法,內(nèi)部直接拋出了一個(gè)異常,還記得咱們是從哪個(gè)類掉用到父類AbstractQueuedSynchronizer的acquire方法的?沒錯(cuò),就是FairSync類。那么咱們就直接定位到FairSync類的tryAcquire方法唄。

    protected final boolean tryAcquire(int acquires) {
        // 拿到當(dāng)前線程,也就是線程A
        final Thread current = Thread.currentThread();
    
        // 拿到當(dāng)前aqs的state變量,我們沒有修改過它,
        // 默認(rèn)為0
        int c = getState();
        if (c == 0) {
            // 進(jìn)入此邏輯,此邏輯跟acquire方法有點(diǎn)類似
            // 必須要hasQueuedPredecessors()方法返回false
            // 才能繼續(xù)往下執(zhí)行,于是我們把hasQueuedPredecessors的源碼也貼出來
            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;
    }
    
  • hasQueuedPredecessors方法源碼

    public final boolean hasQueuedPredecessors() {
        // 拿到aqs中的tail
        Node t = tail; 
        // 拿到aqs中的head
        Node h = head;
        Node s;
    
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }
    

    此方法涵蓋的情景比較多,但是就當(dāng)前情景而言,它很容易理解,在當(dāng)前情形中,我們壓根沒操作過tail和head那么h 肯定等于 t,所以此方法返回false,返回false后,我們回到FairSync類的tryAcquire方法,

    protected final boolean tryAcquire(int acquires) {
        // .... 上半部分代碼省略
        if (c == 0) {
            // 在當(dāng)前情景下,hasQueuedPredecessors返回的是false
            // 也就是說會(huì)繼續(xù)走if后面的邏輯,
            // if后面的邏輯就是執(zhí)行CAS操作,
            // 將state屬性從0設(shè)置成1
            // 由于此時(shí)只有一個(gè)線程在執(zhí)行,
            // 這個(gè)cas操作一定是成功的
            // cas成功后就會(huì)執(zhí)行setExclusiveOwnerThread代碼,這段代碼很有用
            // 它是一個(gè)賦值的操作,也就是記錄
            // 當(dāng)前擁有鎖的線程
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        // .... 下半部分else if邏輯也省略了
        return false;
    }
    

    通過上述代碼中的注釋,我們可以發(fā)現(xiàn),線程A加鎖成功后會(huì)返回true,至此,tryAcquire的返回值為true。還記的我們是從哪個(gè)方法進(jìn)來的嗎?是的,是從父類AbstractQueuedSynchronizer的acquire方法進(jìn)來的,上面總結(jié)到了,只有當(dāng)tryAcquire返回false,才會(huì)繼續(xù)往下執(zhí)行。至此,線程A單獨(dú)加鎖的案例就結(jié)束了。通過這么一個(gè)單線程加鎖的案例,如果你認(rèn)為AQS很簡(jiǎn)單的話,那就大錯(cuò)特錯(cuò)了,單線程加鎖的案例中,我們僅使用到了AQS中的state變量,CLH隊(duì)列卻始終沒有涉及到,而且從加鎖到加鎖結(jié)束的整個(gè)過程,我們連一個(gè)Node類型的數(shù)據(jù)結(jié)構(gòu)都沒有看到過。那Node類型的數(shù)據(jù)結(jié)構(gòu)什么時(shí)候會(huì)被用到呢?我們來看下一個(gè)案例線程A正在持有鎖的過程中,線程t1來加鎖

三、案例2:線程A正在持有鎖的過程中,線程t1來加鎖

  • 同樣的,咱們改造下代碼:

    public class TwoThreadLock {
    
        static ReentrantLock lock = new ReentrantLock(true);
    
        public static void main(String[] args) throws InterruptedException {
            new Thread(() -> {
                try {
                    lock.lock();
                    System.out.println("Thread a get lock");
                    TimeUnit.SECONDS.sleep(60);
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }, "線程a").start();
    
            Thread t1 = new Thread(() -> {
                try {
                    lock.lock();
                    System.out.println("Thread t1 get lock");
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }, "線程t1");
    
            t1.start();
            t1.join();
    
            System.out.println("end");
        }
    }
    

    上段代碼,毫無疑問,線程t1在調(diào)用lock.lock()方法時(shí),就阻塞到那里了,要等線程a睡60s后才會(huì)繼續(xù)執(zhí)行,那么這里面到底做了哪些事呢?我們來一起研究下。

  • 同案例1,使用的是公平鎖,最終肯定會(huì)調(diào)用到tryAcquire方法去,咱們這次就一次性的把tryAcquire方法給講清楚

    protected final boolean tryAcquire(int acquires) {
        // 拿到當(dāng)前線程,也就是線程t1
        final Thread current = Thread.currentThread();
    
        // 拿到當(dāng)前aqs的state變量,此時(shí)的c是多少呢?
        // 沒錯(cuò),是1,因?yàn)殒i已經(jīng)被線程A占有了,此時(shí)的
        // state為1。于是它會(huì)走else if邏輯
        int c = getState();
        if (c == 0) {
            // 進(jìn)入此邏輯,此邏輯跟acquire方法有點(diǎn)類似
            // 必須要hasQueuedPredecessors()方法返回false
            // 才能繼續(xù)往下執(zhí)行,于是我們把hasQueuedPredecessors的源碼也貼出來
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        // 走了else if邏輯,它也發(fā)現(xiàn)當(dāng)前持有鎖的線程不是自己呀,于是直接return false
        // 這里順帶解釋下這個(gè)else if的邏輯,這個(gè)else if
        // 就是判斷當(dāng)前調(diào)用lock方法的線程是不是和當(dāng)前持有
        // 鎖的線程一樣,如果是一樣的,則將state + 1并賦值給nextc屬性
        // 這就表示了ReentrantLock支持重入性
        // 那么什么時(shí)候會(huì)出現(xiàn)nextc屬性小于0的情況呢?
        // nextc是一個(gè)int類型,當(dāng)超過了它的存儲(chǔ)返回后
        // 會(huì)出現(xiàn)小于0的情況 ===> 也就是說ReentrantLock
        // 的重入次數(shù)最大為支持int類型最大值
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
    

    通過上述代碼塊中的注釋可知,線程t1的加鎖流程并沒有這么順利,在tryAcquire方法中返回了false,那這代表了什么呢?是的,它代表著線程t1可以繼續(xù)走acquire后面的邏輯了,咱們繼續(xù)把a(bǔ)cquire方法貼出來:

    public final void acquire(int arg) {
        // 在案例2的情況下,tryAcquire方法返回了false
        // 于是會(huì)執(zhí)行后面的acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
        // 當(dāng)acquireQueued(addWaiter(Node.EXCLUSIVE), arg)返回了true才會(huì)執(zhí)行內(nèi)部的selfInterrupt()方法
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
    

    于是,咱們先了解下addWaiter(Node.EXCLUSIVE)方法,它的源碼如下:

    private Node addWaiter(Node mode) {
        // 此時(shí)的mode是由上述代碼塊傳入的,
        // 它的值為Node.EXCLUSIVE ===> 這是一個(gè)空節(jié)點(diǎn),
        // 值為null,
        // 創(chuàng)建了一個(gè)node節(jié)點(diǎn), 內(nèi)部維護(hù)了當(dāng)前線程(線程t1),并且它的next節(jié)點(diǎn)為null(有Node的構(gòu)造方法可知)
        Node node = new Node(Thread.currentThread(), mode);
        // 拿到aqs隊(duì)列中的tail屬性,
        // 此時(shí)肯定為null啊(aqs隊(duì)列都沒初始化,哪來的隊(duì)尾節(jié)點(diǎn))
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        
        // 此時(shí)pred為null,即不會(huì)走上面的if邏輯,于是執(zhí)行enq方法,記住:此時(shí)傳入enq方法時(shí)的形參為新new出來的Node
        // 內(nèi)部維護(hù)的是當(dāng)前線程(線程t1)
        enq(node);
        return node;
    }
    

    上面代碼塊的注釋也說了,最終會(huì)執(zhí)行到enq方法,enq方干啥的呢?猜一下?是的,它就是初始aqs隊(duì)列的。我們來看一下它的源碼:

    /**
     形參node內(nèi)部維護(hù)的線程為t2, 并且它的next屬性指向?yàn)閚ull
     */
    private Node enq(final Node node) {
        // 此處寫了一個(gè)死循環(huán),也就是常說的自旋鎖
        for (;;) {
            // 自旋的過程中
            // 第一次自旋:
            //  拿到隊(duì)尾元素, 此時(shí)隊(duì)列都沒有,肯定為null
            //  發(fā)現(xiàn)隊(duì)列中的tail指向的是null,于是初始化tail節(jié)點(diǎn),并讓aqs中的head指向了tail,
            //  至此,aqs簡(jiǎn)易版本的隊(duì)列就出來啦,
            //  head和tail指向同一個(gè)node,并且此node內(nèi)部
            //  維護(hù)的thread、prev、next、waitStatus全是默認(rèn)值
            // 由于是if else邏輯,所以初始化tail屬性后,就會(huì)進(jìn)行第二次自旋
            // 第二次自旋:
            //  再次拿到tail, 由于第一次自旋把tail給初始化了,所以此時(shí)拿到的tail不為null, 于是走了else邏輯
            //  在else中,主要操作的是形參node, 還記得形參node是什么嗎? ==> 維護(hù)當(dāng)前線程(線程t1)的node節(jié)點(diǎn),
            //  此時(shí)會(huì)將node的上一個(gè)節(jié)點(diǎn)指向t節(jié)點(diǎn)
            //  同時(shí)進(jìn)行cas操作,將node節(jié)點(diǎn)變成tail
            //  當(dāng)cas成功后,再設(shè)置t的next指向node
            //  最終返回這個(gè)t.
            //  此時(shí)此刻這個(gè)t是什么樣的數(shù)據(jù)結(jié)構(gòu)呢?
            //  此時(shí)的這個(gè)t就是隊(duì)列中的head節(jié)點(diǎn)了,
            //  并且它的next為node(維護(hù)線程t1)
            //  所以此時(shí)此刻隊(duì)列中現(xiàn)在有兩個(gè)元素了
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }
    

    代碼中的注釋描述了enq的過程,我專門畫了一個(gè)圖來描述aqs隊(duì)列產(chǎn)生的過程,幫助理解:


    在這里插入圖片描述

    enq初始化aqs隊(duì)列的過程后,就執(zhí)行到了addWaiter方法的出口了

    private Node addWaiter(Node mode) {
        // ....上述代碼省略
        // enq初始化隊(duì)列后,會(huì)將node進(jìn)行返回
        // 這個(gè)node就是維護(hù)線程t1的node,它已經(jīng)是
        // 隊(duì)列中的隊(duì)列了
        enq(node);
        return node;
    }
    

    addWaiter方法執(zhí)行完了之后,將繼續(xù)執(zhí)行acquire方法

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

    此時(shí)應(yīng)該接著執(zhí)行acquireQueued(addWaiter(Node.EXCLUSIVE), arg)了,由于addWaiter方法已經(jīng)執(zhí)行完成,返回的是擁有當(dāng)前線程的node,同時(shí)它也是當(dāng)前隊(duì)列中的隊(duì)尾。我們來查看下acquireQueued的源碼:

    /**
     node形參為維護(hù)當(dāng)前線程(t1)的節(jié)點(diǎn),
     同時(shí)arg為1
     */
    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            // 此處又自旋了
            for (;;) {
                // 獲取到當(dāng)前節(jié)點(diǎn)的上一個(gè)節(jié)點(diǎn),在
                // 當(dāng)前案例下,它是head節(jié)點(diǎn)
                final Node p = node.predecessor();
                // 第一次自旋:
                //   做判斷,發(fā)現(xiàn)上一個(gè)節(jié)點(diǎn)是head節(jié)點(diǎn)
                //   于是繼續(xù)執(zhí)行加鎖方法tryAcquire
                //   因?yàn)樵诋?dāng)前案例下,線程a睡眠了60s
                //   肯定還是加鎖失敗的,加鎖失敗后,
                //   則走下面的邏輯,這里就是為了當(dāng)前
                //   節(jié)點(diǎn)繼續(xù)上鎖、因?yàn)橛锌赡芮懊娴?            //   節(jié)點(diǎn)已經(jīng)釋放鎖了,或者說被park
                //   的線程被unpark了,要繼續(xù)自旋,
                //   嘗試獲取鎖
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                
                // 判斷當(dāng)前這個(gè)節(jié)點(diǎn)是否需要park
                // 什么是park?就是使用unsafe類來阻塞指定的線程,
                // 在shouldParkAfterFailedAcquire方法中
                // 傳入的是當(dāng)前節(jié)點(diǎn)和上一個(gè)節(jié)點(diǎn),
                // 大致邏輯為:
                //   1. 判斷當(dāng)前節(jié)點(diǎn)的上一個(gè)節(jié)點(diǎn)(即p)的waitStatus是不是SIGNAL(-1)狀態(tài),如果是則返回true
                //     SIGNAL代表什么呢?上面的表格中有說到
                //     SIGNAL代表這個(gè)Node是處于排隊(duì)狀態(tài)
                //     因此可以得出一個(gè)結(jié)論:如果上一個(gè)節(jié)點(diǎn)也處于排隊(duì)狀態(tài)
                //     那么我就返回true,進(jìn)而執(zhí)行parkAndCheckInterrupt方法,parkAndCheckInterrupt方法就是讓park當(dāng)前線程,讓當(dāng)前線程進(jìn)入阻塞狀態(tài),自旋再此暫停
                //   2. 如果p節(jié)點(diǎn)的waitStatus為負(fù)數(shù),即不是中斷或者取消狀態(tài)
                //      那么它會(huì)將p的waitStatus置為-1.并返回false
                //      進(jìn)而進(jìn)入第二次自旋,當(dāng)進(jìn)入第二次自旋時(shí),若上面還未獲取鎖成功,那么當(dāng)前線程就會(huì)被park
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
    

    所以,當(dāng)線程t2在執(zhí)行到此方法時(shí),發(fā)現(xiàn)head即線程a對(duì)應(yīng)的node的waitStatus為0,于是會(huì)自旋一次將head的waitStatus置為-1,然后再繼續(xù)自旋,此時(shí)自己嘗試加鎖又失敗了,此時(shí)就會(huì)進(jìn)入park狀態(tài)。所以就在acquireQueued方法處阻塞了,等待線程a釋放鎖后喚醒線程t1。至此案例2的加鎖過程也結(jié)束了

四、總結(jié)

  • 本次只是基于兩個(gè)簡(jiǎn)單的案例來認(rèn)識(shí)ReentrantLock加鎖流程的源碼,其中還有很多其他的case沒有涉及到。這兩種案例算是認(rèn)識(shí)ReentrantLock加鎖源碼的入門吧。下篇博客將介紹下基于這兩種案例的解鎖過程。
  • ReentrantLock加鎖流程涉及到每個(gè)方法的詳細(xì)步驟可查看在github中的總結(jié):傳送門
  • I am a slow walker, but I never walk backwards.
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,837評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,196評(píng)論 3 414
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,688評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,654評(píng)論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,456評(píng)論 6 406
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,955評(píng)論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,044評(píng)論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,195評(píng)論 0 287
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,725評(píng)論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,608評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,802評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,318評(píng)論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,048評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,422評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,673評(píng)論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,424評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,762評(píng)論 2 372

推薦閱讀更多精彩內(nèi)容