源碼|并發一枝花之ReentrantLock與AQS(1):lock、unlock

顯示鎖ReentrantLock的內部同步依賴于AQS(AbstractQueuedSynchronizer),因此,分析ReentrantLock必然涉及AQS。

本文假設讀者已熟練掌握AQS的基本原理(參考AQS的基本原理),通過分析ReentrantLock#lock()與ReentrantLock#unlock()的實現原理,用實例幫助讀者理解AQS的等待隊列模型。

JDK版本:oracle java 1.8.0_102

接口聲明

public interface Lock {
    void lock();

    void lockInterruptibly() throws InterruptedException;

    boolean tryLock();

    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

    void unlock();

    Condition newCondition();
}

ReentrantLock對標內置鎖,實現了Lock接口。忽略Condition相關,主要提供lock、unlock兩種語義,和兩種語義的衍生品。

實現原理

繼承AQS

本文中的“繼承”指“擴展extend”。

AQS復習

AQS并提供了多個未實現的protected方法,留給作者覆寫以開發不同的同步器:

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    ...
    protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }
    protected boolean tryRelease(int arg) {
        throw new UnsupportedOperationException();
    }
    protected int tryAcquireShared(int arg) {
        throw new UnsupportedOperationException();
    }
    protected boolean tryReleaseShared(int arg) {
        throw new UnsupportedOperationException();
    }
    protected boolean tryReleaseShared(int arg) {
        throw new UnsupportedOperationException();
    }
    ...
}

而其他非私有方法則使用final修飾,禁止子類覆寫。

繼承

ReentrantLock支持公平、非公平兩種策略,并通過繼承AQS實現了對應兩種策略的同步器NonfairSync與FairSync。ReentrantLock默認使用非公平策略,即NonfairSync:

    public ReentrantLock() {
        sync = new NonfairSync();
    }
    ...
    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        final void lock() {
            ...
        }

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }
    abstract static class Sync extends AbstractQueuedSynchronizer {
        ...
        abstract void lock();

        final boolean nonfairTryAcquire(int acquires) {
            ...
        }

        protected final boolean tryRelease(int releases) {
            ...
        }

        final ConditionObject newCondition() {
            return new ConditionObject();
        }
        ...
    }
    ...

先不追究細節。下面以默認的非公平策略為例,講解lock和unlock的實現。

lock

    public void lock() {
        sync.lock();
    }

非公平策略下,sync指向一個NonfairSync實例。

    static final class NonfairSync extends Sync {
        ...
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
        ...
    }

ReentrantLock用state表示“所有者線程已經重復獲取該鎖的次數”。當state等于0時,表示當前沒有線程持有該鎖,因此,將state CAS設置為1,并記錄排他的所有者線程ownerThread(ownerThread只會在0->1及1->0兩次狀態轉換中修改);否則,state必然大于0,則嘗試再獲取一次鎖。ownerThread將在state大于0時,用于判斷重入性。

  • 排他性:如果線程T1已經持有鎖L,則不允許除T1外的任何線程T持有該鎖L
  • 重入性:如果線程T1已經持有鎖L,則允許線程T1多次獲取鎖L,更確切的說,獲取一次后,可多次進入鎖。

二者結合,描述了ReentrantLock的一個性質:允許ownerThread重入,不允許其他線程進入或重入。

acquire

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    ...
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
    static void selfInterrupt() {
        Thread.currentThread().interrupt();
    }
    ...
}

改寫:

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    ...
    public final void acquire(int arg) {
        if (tryAcquire(arg)) {
            return;
        }
        Node newNode = addWaiter(Node.EXCLUSIVE);
        boolean interrupted = acquireQueued(newNode, arg);
        if (interrupted) {
            selfInterrupt();
        }
    }
    ...
}

首先,通過tryAcquire()嘗試獲取鎖。按照AQS的約定,tryAcquire()返回true表示獲取成功,可直接返回;否則獲取失敗。如果獲取失敗,則向等待隊列中添加一個獨占模式的節點,并通過acquireQueued()阻塞的等待該節點被調用(即當前線程被喚醒)。如果是因為被中斷而喚醒的,則復現中斷信號。

tryAcquire

NonfairSync覆寫了AQS#tryAcquire():

    static final class NonfairSync extends Sync {
        ...
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
        ...
    }
    abstract static class Sync extends AbstractQueuedSynchronizer {
        ...
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                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;
        }
        ...
    }

12-17行重復了NonfairSync#lock()中state=0時的狀態轉換。18行進行排他性判斷,如果當前線程等于ownerThread,則直接返回false。否則,19-22行進行重入,state加1(acquires=1),表示所有者線程重復獲取該鎖的次數增加1。

實際上,NonfairSync#lock()不需要特殊處理state=0時的狀態轉換。可通過NonfairSync#tryAcquire()、Sync#nonfairTryAcquire()完成。

為什么19-22行不需要同步

注意,如果18行判斷當前線程等于ownerThread,則根據程序順序規則,19-22行不需要同步。因為同一線程中,第二次調用NonfairSync#tryAcquire()時(會進入19-22行),第一次調用鎖寫入的state、ownerThread一定是可見的。

為什么要用state表示重入次數

如果沒有記錄重入次數,則第一次釋放鎖時,會一次性把ownerThread多次重入的鎖都釋放掉,而此時“鎖中的代碼”還沒有執行完成,造成混亂。

addWaiter

如果tryAcquire()獲取失敗,則要通過AQS#addWaiter()向等待隊列中添加一個獨占模式的節點,并返回該節點:

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    ...
    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }
    private Node enq(final Node node) {
        for (;;) {
            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;
                }
            }
        }
    }
    ...
}

AQS#enq()中重復了9-15行的邏輯,直接看enq()。

如果尾指針為null,則頭指針也一定為null,表示等待隊列未初始化,就CAS初始化隊列(常見于無阻塞隊列的設計中,如源碼|并發一枝花之BlockingQueue),然后繼續循環。如果尾指針非null,則隊列已初始化,就CAS嘗試在尾節點后插入新的節點node。

在插入過程中,會出現“node.prev指向舊的尾節點,但舊的尾節點.next為null未指向node(盡管,尾指針指向node)”的狀態,即“隊列在prev方向一致,next方向不一致”。記住該狀態,分析ReentrantLock#unlock()時會用到。

最后,enq()返回舊的尾節點。但外層的AQS#addWaiter()仍然返回新節點node。

隊列剛完成初始化時,存在一個dummy node。插入節點時,tail后移指向新節點,head不變仍然指向dummy node。直到調用AQS#acquireQueued()時,head才會后移,消除了dummy node,后面分析。

acquireQueued

插入新節點node后,通過AQS#acquireQueued()阻塞的等待該節點被調用(即當前線程被喚醒):

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    ...
    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
    ...
}

該方法是lock過程的核心難點,需要結合AQS#addWaiter()理解AQS內部基于等待隊列的同步模型。

AQS的核心是狀態依賴,可概括為兩條規則:

  • 當狀態還沒有滿足的時候,節點會進入等待隊列。
  • 特別的,獲取成功的節點成為隊列的頭結點。

首先,AQS#addWaiter()會將新節點node加入隊尾(維護規則“當狀態還沒有滿足的時候,節點會進入等待隊列”),然后,AQS#acquireQueued()檢查node的前繼節點是否是頭節點。如果是,則嘗試獲取鎖;如果不是,或獲取所失敗,都會嘗試阻塞等待。

如果11行獲取鎖成功,則更新頭節點(維護規則“獲取成功的節點成為隊列的頭結點”),修改failed標志,并返回interrupted標志。interrupted初始化為false,可能在17-19行被修改。

初始化隊列后的第一次更新頭結點,直接setHead消除了dummy node。消除之后,實際節點代替了dummy node的作用,但與dummy node不同的是,該節點是持有鎖的。

如果11行判斷前繼節點不是頭節點或獲取鎖失敗,則進入17-19行。AQS.shouldParkAfterFailedAcquire()判斷是否需要阻塞等待,如果需要,則通過AQS#parkAndCheckInterrupt()阻塞等待,直到被喚醒或被中斷。

shouldParkAfterFailedAcquire

AQS.shouldParkAfterFailedAcquire()根據pred.waitStatus判斷新節點node是否應該被阻塞:

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    ...
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            return true;
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }
    ...
}

AQS#addWaiter()構造新節點時,pred.waitStatus使用了默認值0。此時,進入14-16行,CAS設置pred.waitStatus為SIGNAL==-1。最后返回false。

回到AQS#acquireQueued()中后,由于AQS#parkAndCheckInterrupt()返回false,循環會繼續進行。假設node的前繼節點pred仍然不是頭結點或鎖獲取失敗,則會再次進入AQS#parkAndCheckInterrupt()。上一輪循環中,已經將pred.waitStatus設置為SIGNAL==-1,則這次會進入7-8行,直接返回true,表示應該阻塞。

什么時候會遇到ws > 0的case呢?當pred所維護的獲取請求被取消時,pred.waitStatus會被設置為CANCELLED==1,從而進入9-14行。改寫:

if (ws > 0) {
    do {
        pred = pred.prev;
        node.prev = pred;
    } while (pred.waitStatus > 0);
    pred.next = node;
}

邏輯很簡單,循環移除所有被取消的前繼節點pred,直到找到未被取消的pred。移除所有被取消的前繼節點后,直接返回false。

注意,在執行6行之前,隊列處于“node.prev指向最新的前繼節點,但pred.next指向已經移除的后繼節點”的狀態,即“隊列在prev方向一致,next方向不一致”。記住該狀態,分析ReentrantLock#unlock()時會用到。

此處不需要檢查前繼節點是否為null。因為等待隊列的頭結點要么是dummy node,滿足dummy.waitStatus==0;要么是剛替換的real node,滿足real.waitStatus==0;要么是后繼節點已經阻塞的節點,滿足real.waitStatus==SIGNAL==-1。則最晚遍歷到頭結點時,一定會退出循環,不會出現pred為null的情況。

回到AQS#acquireQueued()后,重新檢查前繼節點是否為頭節點,并作出相應處理。

經過多次循環執行AQS.shouldParkAfterFailedAcquire()后,等待隊列趨于穩定。最終的穩定狀態為:

  • 除了頭節點,剩余節點都會返回true,表示需要阻塞等待。
  • 除了尾節點,剩余節點都滿足waitStatus==SIGNAL,表示釋放后需要喚醒后繼節點
parkAndCheckInterrupt
public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
...
    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }
...
}

AQS#parkAndCheckInterrupt()借助LockSupport.park()實現阻塞等待。最后調用Thread.interrupted()檢查是否被中斷,并清除中斷狀態,并返回中斷標志。

如果是被中斷的,則需要在外層AQS#acquireQueued()中重新設置中斷標志interrupted,并在下一次循環中返回。然后在更外層的AQS#acquire()中調用AQS.selfInterrupt()重放中斷。

為什么不能直接在AQS#parkAndCheckInterrupt()返回后中斷?因為返回中轉標志能提供更大的靈活性,外界可以自行決定是即時重放、稍后重放還是壓根不重放。Condition在得知AQS#acquireQueued()是被中斷的之后,便沒有直接復現中斷,而是根據REINTERRUPT配置決定是否重放。

cancelAcquire

最后,如果在執行AQS#acquire()的過程中拋出任何異常,則取消任務:

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    ...
    private void cancelAcquire(Node node) {
        ...
        node.waitStatus = Node.CANCELLED;
        ...
    }
    ...
}

因此,如果指考慮ReentrantLock#lock()方法的話,那么被標記為CACELLED狀態的節點一定在獲取鎖時拋出了異常,AQS.shouldParkAfterFailedAcquire()中清理了這部分CACELLED節點。

超時版ReentrantLock#tryLock()中,還可以由于超時觸發取消。

lock小結

ReentrantLock#lock()收斂后,AQS內部的等待隊列如圖:

image.png

unlock

ReentrantLock#unlcok()與ReentrantLock#lcok()是對偶的。

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

獲取鎖以單位1進行,釋放鎖時也以單位1進行。

release

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    ...
    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
    ...
}

首先,通過tryRelease()嘗試釋放鎖。按照AQS的約定,tryRelease()返回true表示已完全釋放,可喚醒所有阻塞線程;否則沒有完全釋放,不需要喚醒。如果已完全釋放,則只需要喚醒頭結點的后繼節點,該節點的ownerThread必然與頭結點不同(如果相同,則之前lock時能夠重入,不需要排隊);否則沒有完全釋放,不需要喚醒任何節點。

對于獨占鎖,“完全釋放”表示ownerThread的所有重入操作均已結束。

解釋8行的判斷

如果h == null,則隊列還未初始化(回憶AQS#enq())。如果h.waitStatus == 0,則要么剛剛初始化隊列,只有一個dummy node,沒有后繼節點(回憶AQS#enq());要么后繼節點還沒被阻塞,不需要喚醒(回憶等待隊列的穩定狀態)。

tryRelease

對照tryAcquire()分析tryRelease():

    abstract static class Sync extends AbstractQueuedSynchronizer {
        ...
        protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }
        ...
    }

5-6行很重要。如果存在某線程持有鎖,則可以檢查unlock是否被ownerThread觸發;如果不存在線程持有鎖,則ownerThread==null,可以檢查是否在未lock的情況下進行unlock,或者重復執行了unlock。

因此,使用ReentrantLock時,try-finally要這么寫:

Lock lock = new ReentrantLock();
lock.lock();
try {
 // do sth
} finally {
 lock.unlcok();
}

確保在調用lock()成功之后,才能調用unlock()。

接下來,8行判斷是否將要進行1->0的狀態轉換,如果是,則可以完全釋放鎖,將ownerThread置為null。然后設置state。

最后,返回是否可完全釋放的標志free。

可見性問題

為了抓住核心功能,前面一直忽略了一個很重要的問題——可見性。忽略可見性問題的話,閱讀源碼基本沒有影響,但自己實現同步器時將帶來噩夢。

以此處為例,是應該先執行8-11行,還是先執行12行呢?或者是無所謂呢?

為保障可見性,必須先執行8-11行,再執行12行。因為exclusiveOwnerThread的可見性要借助于(piggyback)于volatile變量state

    ...
    private transient Thread exclusiveOwnerThread;
    ...
    private volatile int state;
    ...

配套的,也必須先讀state,再讀exclusiveOwnerThread:

    abstract static class Sync extends AbstractQueuedSynchronizer {
        ...
        final boolean nonfairTryAcquire(int acquires) {
            ...
            int c = getState(); // 先讀state
            if (c == 0) {
                ...
            }
            else if (current == getExclusiveOwnerThread()) {    // 再讀exclusiveOwnerThread
                ...
            }
            return false;
        }
        ...
    }

核心是三條Happens-Before規則:

  • 程序順序規則:如果程序中操作A在操作B之前,那么在線程中操作A將在操作B之前執行。
  • 傳遞性:如果操作A在操作B之前執行,并且操作B在操作C之前執行,那么操作A必須在操作C之前執行。
  • volatile變量規則:對volatile變量的寫入操作必須在對該變量的讀操作之前執行。

具體來說,“先寫exclusiveOwnerThread再寫state;先讀state再讀exclusiveOwnerThread”的方案,保證了“在讀state之后,發生在寫state之前的寫exclusiveOwnerThread操作發生在讀state之后的讀exclusiveOwnerThread操作一定是可見的”。

程序順序規則、傳遞性兩條基本規則,經常與監視器鎖規則、volatile變量規則顯示的搭配,一定要掌握。

相對的,線程啟動規則、線程結束規則、中斷規則、終結器規則則通常被隱式的使用。

unparkSuccessor

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    ...
    private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        Node s = node.next;
        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);
    }
    ...
}

對lock過程的分析中,我們得知,隊列中所有節點的waitStatus要么為0,要么為SIGNAL==-1。當node.waitStatus==SIGNAL時,表示node的后繼節點s已被阻塞或正在被阻塞?,F在需要喚醒s,則7-8行將node.waitStatus置0。

注意,node.waitStatus一定不為CANCELLED==1,因為如果lock()方法沒有執行成功,就無法通過unlock()方法調用AQS#unparkSuccessor()。

接下來,10-18行從尾節點向前遍歷,找到node后最靠近node的未取消的節點,如果存在該節點s(s!=null),就喚醒s.thread以競爭鎖。

一致性問題

為什么要從尾節點向前遍歷,而不能從node向后遍歷?這是因為,AQS中的等待隊列基于一個弱一致性雙向鏈表實現,允許某些時刻下,隊列在prev方向一致,next方向不一致。

理想情況下,隊列每時每刻都處于一致的狀態(強一致性模型),從node向后遍歷找第一個未取消節點是更高效的做法。然而,維護一致性通常需要犧牲部分性能,為了進一步的提升性能,腦洞大開的神牛們想出了各種高性能的弱一致性模型。盡管模型允許了更多弱一致狀態,但所有弱一致狀態都在控制之下,不會出現一致性問題。

回憶lock過程的分析,有兩個地方出現了這個弱一致狀態:

  • AQS#enq()插入新節點(包括AQS#addWaiter())的過程中,舊的尾節點next為null未指向新節點。對應條件s == null。如圖:
image.png
  • AQS.shouldParkAfterFailedAcquire()移除CACELLED節點的過程中,中間節點指向已被移除的CACELLED節點。對應條件s.waitStatus > 0。如圖:
image.png

因此,從node開始,沿著next方向向后遍歷是行不通的。只能從tail開始,沿著prev方向向前遍歷,直到找到未取消的節點(s != null),或遍歷完node的所有后繼子孫(s == null。

當然,s == null也可能表示node恰好是尾節點,該狀態是強一致的,但仍然可以復用該段代碼。

unlock小結

ReentrantLock#unlock()收斂后,AQS內部的等待隊列如圖:

image.png

總結

本文的目標是借助對ReentrantLock#lock()與ReentrantLock#unlock()的分析,理解AQS的等待隊列模型。

實際上,ReentrantLock還有一些重要的特性和API,如ReentrantLock#lockInterruptibly()、ReentrantLock#newCondition()。后面分先后分析這兩個API,加深對AQS的理解。


本文鏈接:源碼|并發一枝花之ReentrantLock與AQS(1):lock、unlock
作者:猴子007
出處:https://monkeysayhi.github.io
本文基于 知識共享署名-相同方式共享 4.0 國際許可協議發布,歡迎轉載,演繹或用于商業目的,但是必須保留本文的署名及鏈接。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,406評論 6 538
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,034評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 177,413評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,449評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,165評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,559評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,606評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,781評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,327評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,084評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,278評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,849評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,495評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,927評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,172評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,010評論 3 396
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,241評論 2 375

推薦閱讀更多精彩內容