源碼分析:同步基礎框架——AbstractQueuedSynchronizer(AQS)

簡介

AQS 全稱是 AbstractQueuedSynchronizer,位于java.util.concurrent.locks 包下面,AQS 提供了一個基于FIFO的隊列和維護了一個狀態state變量賴表示狀態,可以作為構建鎖或者其他相關同步裝置的基礎框架。AQS 支持兩種模式:共享模式 和 排他模式,當它被定義為一個排他模式時,其他線程對其的獲取就被阻止,而共享模式對于多個線程獲取都可以成功。之所以說它是一個同步基礎框架是因為很多同步類里面都用到了AQS,比如 ReentrantLock 中的內部類同步器Sync繼承至AQS,ReentrantReadWriteLock中的同步器也是繼承至AQS,還有 Semaphore 、CountDownLatch等都是基于AQS來實現的。

核心源碼

類結構

AQS 繼承了 AbstractOwnableSynchronizer, AbstractOwnableSynchronizer 這個類比較簡單,就一個屬性 private transient Thread exclusiveOwnerThread ,用來標識當前獨占鎖的持有者線程,通俗的說就是哪個線程拿到了獨占鎖,就調用AbstractOwnableSynchronizer 的方法把這個線程保存起來。源碼如下:

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
    ...
}

public abstract class AbstractOwnableSynchronizer implements java.io.Serializable {
  private transient Thread exclusiveOwnerThread;
  // 構造方法,get set 方法省略。。。
}

后面的分析中,會有大量的同步器在獲得鎖之后會調用setExclusiveOwnerThread(Thread) 方法來保存鎖的持有者線程;

重要內部類Node

static final class Node {
    volatile int waitStatus;
    volatile Node prev;
    volatile Node next;
    volatile Thread thread;
    Node nextWaiter;
}

以上五個成員變量主要負責保存該節點的線程引用,同步隊列的前驅和后繼節點,同時也包括了同步狀態。

屬性解釋:

waitStatus:表示節點的狀態。其中包含的狀態有:

  1. CANCELLED,值為1,表示當前的線程被取消;
  2. SIGNAL,值為-1,表示當前節點的后繼節點包含的線程需要運行,也就是unpark;
  3. CONDITION,值為-2,表示當前節點在等待condition,也就是在condition隊列中;
  4. PROPAGATE,值為-3,表示當前場景下后續的acquireShared能夠得以執行;
  5. 值為0,表示當前節點在sync隊列中,等待著獲取鎖。

prev:前驅節點,比如當前節點被取消,那就需要前驅節點和后繼節點來完成連接

next:后繼節點

thread:入隊列時的當前線程

nextWaiter:存儲condition隊列中的后繼節點

重要屬性:同步隊列和同步狀態

節點成為同步隊列和 condition 條件隊列構建的基礎,同步器擁有三個成員變量:頭結點head、尾節點tail和同步狀態state

private transient volatile Node head;
private transient volatile Node tail;
private volatile int state;

對于新的獲取鎖請求,形成Node節點,掛載到隊列的尾部;對于鎖資源的釋放都是從隊列的頭部進行操作的。

      +------+  prev +-----+       +-----+
 head |      | <---- |     | <---- |     |  tail
      +------+       +-----+       +-----+

可以重寫的API

實現自定義同步器時,需要使用同步器提供的getState()、setState()和compareAndSetState()方法來控制同步狀態。

方法1:protected boolean tryAcquire(int arg)

描述:已排它模式獲取同步狀態。這個方法的實現需要查詢前狀態是否允許獲取,然后再進compareAndSetState()修改狀態,修改成功代表成功獲得鎖。

方法2:protected boolean tryRelease(int arg)

描述:釋放鎖,也就是釋放同步狀態state的值到初始狀態,一般是0。

方法3:protected int tryAcquireShared(int arg)

描述:共享模式下獲取同步狀態,一般可以用來做共享鎖,或者用作限制資源最多同時被訪問多少次。

方法4:protected boolean tryReleaseShared(int arg)

描述:共享模式下釋放同步狀態。

方法5:protected boolean isHeldExclusively()

描述:在排它模式下,返回同步狀態是否被占用,比如我們可以實現返回邏輯為 getState() == 1,為true的話說明資源已經被占用了。

其他代碼我們通過自己實現簡單的排他鎖案例來進行具體的詳細分析

基于AQS實現的排他鎖

一、定義一個MyAQSLock類

public class MyAQSLock{
}

二、定義一個內部類Sync做為同步器,繼承自AbstractQueuedSynchronizer

public class MyAQSLock{
    class Sync extends AbstractQueuedSynchronizer{
    }
}

三、重寫同步器部分API

因為我們要實現的是排它鎖的功能,意思就是同一時刻只能有一個線程獲得鎖,所以只需要重寫tryAcquire、tryReleaseisHeldExclusively方法即可。

class Sync extends AbstractQueuedSynchronizer{
        @Override
        protected boolean **tryAcquire**(int acquires){
            // 入參只能為1
            **assert acquires == 1;
            // 使用CAS的方式修改state值,修改成功代表成功獲得鎖
            if(compareAndSetState(0,1)){
                // 修改鎖的持有者為當前線程
                setExclusiveOwnerThread(Thread.currentThread());
                // 返回true,表示成功獲得鎖
                return true;
            }
            // 返回false,沒有獲得鎖
            return false;
        }

        @Override
        protected boolean **tryRelease**(int releases){
            assert releases == 1;
            if (getState() == 0){
                // 已經被釋放了
                throw new IllegalMonitorStateException();
            }
            // lock() 和 unlock() 一般都是成對出現的,所以這里不需要同步語句,可以直接修改state值為0
            setState(0);
            return true;
        }
        @Override
        protected boolean **isHeldExclusively**() {
            // 返回true,說明已經有其他線程獲得鎖
            return getState() == 1;
        }

    }

三、定義鎖和解鎖方法

public class MyAQSLock{

    private final Sync sync;
    MyAQSLock(){
        sync = new Sync();
    }
        class Sync extends AbstractQueuedSynchronizer{
        ...
        }
    public void lock(){
        // 調用同步器,獲得鎖
        sync.acquire(1);
    }

    public boolean tryLock(){
              // 嘗試獲得鎖,如果沒有獲取到鎖,則立即返回false
        return sync.tryAcquire(1);
    }
 
    public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException{
        // 嘗試獲得鎖,如果沒有獲取到鎖,允許等待一段時間
        return sync.tryAcquireNanos(1,unit.toNanos(timeout));
    }
 
    public void unLock(){
        // 解鎖
        sync.release(1);
    } 

    public boolean isLocked(){
        // 判斷鎖是否已經被占用
        return sync.isHeldExclusively();
    }
}

四、測試我們的鎖

static int count = 0;
public static void main(String[] args) throws InterruptedException{
    MyAQSLock myAQSLock = new MyAQSLock();
    CountDownLatch countDownLatch = new CountDownLatch(1000);
    IntStream.range(0,1000).forEach(i->new Thread(()->{
        myAQSLock.lock();
        try{
            IntStream.range(0,10000).forEach(j->{
                count++;
            });
        }finally{
            myAQSLock.unLock();
        }
        countDownLatch.countDown();
    }).start());
    countDownLatch.await();
    System.out.println(count);
}

最后正確輸出10000000,說明我們實現的鎖是有效的。但是要注意我們自己寫的這個鎖是不支持重入的。

代碼實現分析

獲得鎖:public void lock()

lock()方法會調用sync.acquire(int)方法,acquire在AQS里面,方法被final修飾,作為基礎框架邏輯部分,不允許被繼承,源碼展示:

public final void acquire(int arg) {
    // tryAcquire 是我們自己實現的方法,具體實現看上面
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        // acquireQueued 返回true表示線程被中斷了,中斷當前線程
        selfInterrupt();
}

上面acquire()主要的邏輯有:

  1. 嘗試獲得鎖,調用tryAcquire(arg)方法,該方法的邏輯在我們自定義的MyAQSLock類中,我們利用了compareAndSetState來保證state字段的原子性。

  2. 如果tryAcquire返回true的話,if分支會直接退出,表示成功獲得鎖,繼續執行調用lock() 方法后面的邏輯;

  3. 如果tryAcquire返回false的話,表示沒有獲得鎖,會繼續執行 && 后面的邏輯;

  4. 首先會調用addWaiter(Node.EXCLUSIVE)方法為當前線程創建排隊節點,并加入到隊列,Node.EXCLUSIVE代表這個節點是獨占排他鎖的意思,具體源碼如下:

    private Node addWaiter(Node mode) {
        // 為當前線程創建一個節點,最后會返回出去這個節點
        Node node = new Node(Thread.currentThread(), mode);
        // 隊列不為空時,快速嘗試在同步隊列尾部添加當前節點,如果失敗了會進入enq方法自旋入隊
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        // 上面入隊失敗了,或者是pred為空(第一個排隊的線程進來),繼續自旋入隊
        enq(node);
        return node;
    }
    
    private Node enq(final Node node) {
        // 空的for循環,自旋操作,直到成功把節點加入到同步隊列
        for (;;) {
            // 同步隊列尾巴
            Node t = tail;
            if (t == null) { // Must initialize
                 // 尾巴是空的,還沒有初始化, 第一個排隊的線程進來的話,隊頭隊尾都是同一個節點
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                // 進入到這里,說明同步隊列已經有線程在排隊了
                // 當前節點前驅直接指向同步隊里的尾節點
                node.prev = t;
                // CAS 修改尾節點為當前節點
                if (compareAndSetTail(t, node)) {
                    // t還是老的尾節點,修改新的尾節點后老的尾節點的下一個節點就是當前節點,建立他們的聯系
                    t.next = node;
                    // 成功把當前節點加入到了同步隊列,返回當前節點,退出自旋
                    return t;
                }
            }
        }
    }
    

    addWaiter()方法總結:首先會快速嘗試一次在隊列的尾部添加當前線程節點,如果失敗的話(在這個時候,可能有新的線程也沒有獲得鎖,并且跑在當前的前面加入到同步隊列了),會調用enq邏輯進行自旋加入隊尾,直到成功加入隊列為止。

  5. 再次嘗試從同步隊列獲得鎖acquireQueued(node,arg)

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            // 自旋操作
            for (;;) {
                // 當前節點的上一個節點
                final Node p = node.predecessor(); //**①**
                // 如果前驅節點是頭結點,然后去嘗試獲得鎖,tryAcquire是我們自己實現的獲得鎖邏輯
                **if (p == head && tryAcquire(arg)) { //②**
                    // 當前線程成功獲得鎖,當前節點設置為頭結點
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    // 返回false,表示沒有被中斷
                    return interrupted;
                }
                // 到這里說明p != head 或者 **tryAcquire** 返回了false,還是沒獲得鎖,這時候就需要阻塞線程了
                // shouldParkAfterFailedAcquire 如果線程應阻塞,則返回true
                // parkAndCheckInterrupt  阻塞當前線程 
                if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            // 節點被取消了
            if (failed)
                cancelAcquire(node);
        }
    }
    
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
         // SIGNAL值為-1,表示pred節點的后繼節點包含的線程需要運行,也就是unpark
        if (ws == Node.SIGNAL)
            return true;
        if (ws > 0) {
            // 大于0的值只有1, 1表示線程被取消
            // 進入到這里說明 pred 節點被取消了,需要從同步隊列上刪掉它
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            // 一般初始時為0,設置成-1
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        // 返回false,外面會一直自旋操作
        return false;
    }
    
    private final boolean parkAndCheckInterrupt() {
        // 調用底層的Unsafe一直阻塞線程
        LockSupport.park(this);
        // 被unpark喚醒之后,會繼續回去自旋獲得鎖,并返回線程在此期間是否有被中斷
        return Thread.interrupted();
    }
    

    lock方法總結:

    1. 嘗試獲得鎖,方法tryAcquire
      1. 成功獲得鎖,直接退出;沒有獲得鎖,繼續執行;
    2. 新建排隊節點,并加入到同步隊列,方法addWaiter
      1. 隊列不為空時,嘗試一次快速直接把節點加入到隊列尾巴上;如果隊尾為空或者快速添加失敗,繼續執行下面邏輯
      2. 自旋,直到成功把新建的節點加入到同步隊列;方法enq
        為什么要自旋呢?是因為在調這個方法的時候,可能有其他想要獲得鎖線程沒有獲得鎖,并且已經修改了尾節點;
    3. 再次嘗試從同步隊列獲得鎖,方法acquireQueued
      1. 上面已經把當前線程的節點加入到隊列中了,理論上排隊的線程很多的話,它是馬上獲取不到鎖的
      2. 所以它會自旋判斷是否到了自己可以獲取鎖和CAS嘗試獲取鎖,關鍵代碼if (p == head && tryAcquire(arg))
        理論上當前線程進入到了隊列排隊,只要隊列中還有更早的線程在它前面排隊,當前線程都不會比更早的線程先獲得鎖,所以在這一塊對于公平鎖和非公平鎖肯定都是公平的。
      3. 沒有資格去獲取鎖或沒有成功獲得鎖,就阻塞自己,方法parkAndCheckInterrupt
      4. 阻塞線程被喚醒,自旋成功獲得鎖,排隊期間被中斷的線程也會獲得鎖,之后退出自旋循環,返回線程的中斷狀態;
    4. 線程如果被中斷了,中斷當前線程,被中斷的線程還是會繼續執行后面邏輯
    • 以上過程涉及到的技術點有:CAS,自旋,隊列入隊,隊列刪除節點(被取消的節點),阻塞線程(LockSupport.park(this))

釋放鎖:public final boolean release(int arg)

unlock方法調用的是sync.release(1),而release是AQS 方法中的方法,表示將同步狀態設置回初始狀態,將鎖釋放。

public final boolean release(int arg) {
    // tryRelease 是我們自己的實現,就是把state字段設置成0,如果是可重入的,只能慢慢減到初始狀態
      if (tryRelease(arg)) {
        // 進入到這里說明CAS 設置成功,也就代表鎖成功釋放了,需要喚醒隊列中的第一個排隊的節點線程
          Node h = head;
        // head 表示的是當前獲得鎖的節點
          if (h != null && h.waitStatus != 0)
            // 喚醒頭結點的下一個節點
              unparkSuccessor(h);
          return true;
      }
      return false;
}
private void unparkSuccessor(Node node) {
    // 這里的node 是當前持有鎖的節點
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);     
    // 找到頭結點的后繼節點
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
         // 大于0的狀態只有1,表示被取消了,如果被取消了,就繼續取下一個節點喚醒
        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);**
}

釋放鎖的邏輯比加鎖邏輯要簡單很多,主要邏輯有:

  1. 修改同步狀態為初始值,方法tryRelease(arg)

    這里我們自己實現的會直接將同步狀態設置為0,如果是支持可重入,就需要慢慢減了;

  2. 釋放鎖儲層,喚醒頭結點(當前獲得鎖的節點)的后繼節點:unparkSuccessor(h);
    找到頭結點的后繼節點中第一個沒有被取消的節點,并喚醒該節點所處線程

基于AQS實現自己的共享鎖

設計一個同步器,在同一時刻,只允許最多兩個線程能夠并行訪問,超過限制的其他線程將進入阻塞狀態。

這個功能和 Semaphore 的功能很相似,這個學會了,以后看 Semaphore 的源碼也就很簡單了。

實現思路:

可以利用AQS 的API tryAcquireShared 實現獲得共享鎖,定義一個狀態,允許的范圍為【0,1,2】,狀態為2代表新的線程進入的時候需要阻塞等待

public class MyAqsSharedLock{

    // 定義最大共享值
    private final int maxSharedValue = 2;
    // 同步器
    private final Sync sync;

    MyAqsSharedLock(){
        // 構造方法初始化同步器
        sync = new Sync();
    }
    // 基于AQS實現的同步器
    class Sync extends AbstractQueuedSynchronizer{
        @Override
        protected int tryAcquireShared(int arg){
            // 為什么要自旋呢?因為可能滿足state的條件,但是CAS修改失敗
            while(true){
                int state = getState();
                // 檢查同步狀態是否達到最大值
                if(state >= maxSharedValue){
                    // 返回-1 表示沒有獲得鎖
                    return -1;
                }
                // CAS 修改同步狀態
                if(compareAndSetState(state,state + arg)){
                    // 修改成功,表示獲得了鎖,大于等于0表示獲得了鎖
                    return getState();
                }
            }
        }

        @Override
        protected boolean tryReleaseShared(int arg){
            // 為什么要自旋呢?因為可能滿足state的條件,但是CAS修改失敗
            while(true){
                int state = getState();
                // CAS 修改同步狀態,修改成功返回true,失敗繼續自旋 
                if(compareAndSetState(state,state - arg)){
                    return true;
                }
            }
        }
    }
    /** 加鎖 */
    public void lock(){
        sync.acquireShared(1);
    }
    /** 解鎖 */
    public void unLock(){
        sync.releaseShared(1);
    }
}

測試方法:

5個線程循環打印輸出線程名和當前時間

public static void main(String[] args){
        MyAqsSharedLock lock = new MyAqsSharedLock();
        IntStream.range(0,5).forEach(i -> new Thread(new Runnable(){
            @SneakyThrows
            @Override
            public void run(){
                while(true){
                    lock.lock();
                    try{
                        System.out.println(Thread.currentThread().getName()+":執行。。。時間:"+ LocalDateTime.now());
                        TimeUnit.SECONDS.sleep(2);
                    }finally{
                        lock.unLock();
                        TimeUnit.SECONDS.sleep(1);
                    }

                }
            }
        },"T"+i).start());
    }

輸出結果示例:

T0:執行。。。時間:2020-10-30T17:45:45.117
T1:執行。。。時間:2020-10-30T17:45:45.117
T3:執行。。。時間:2020-10-30T17:45:47.118
T2:執行。。。時間:2020-10-30T17:45:47.118
T1:執行。。。時間:2020-10-30T17:45:49.119
T4:執行。。。時間:2020-10-30T17:45:49.119

會發現幾乎在同一時間最多只有2個線程在打印輸出,滿足我們的要求。

代碼實現分析

獲得共享鎖:public void lock()

共享鎖的 lock() 調用的是sync.acquireShared(1)acquireShared也在AQS里面,同樣被final修飾作為基礎框架邏輯部分,不允許被繼承,源碼展示:

public final void acquireShared(int arg) {
    // tryAcquireShared 是我們自己實現的邏輯,返回-1,表示沒有獲得鎖
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}
// 沒有獲得共享,再次嘗試獲得鎖,和排他模式的acquireQueued方法非常相似
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) {
                // 前驅節點是頭結點,說明輪到咱獲得鎖了
                // 繼續調用我們自己的邏輯,CAS 獲得鎖
                int r = tryAcquireShared(arg);
                // 這里再次印證了,我們的tryAcquireShared返回值定義,負值是沒有獲得鎖,>=0 表示成功獲得鎖
                if (r >= 0) {
                    // 設置新的頭結點,如果后面的排隊節點是共享模式的節點,直接喚醒它
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    if (interrupted)
                        // 中斷當前線程
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            // 到這了,說明要么沒有排隊到當前線程,要么CAS獲取鎖失敗,那就只有阻塞線程了
            // shouldParkAfterFailedAcquire 如果線程應阻塞,則返回true
            // parkAndCheckInterrupt  阻塞當前線程admol, 具體實現分析可以看上面lock的分析
            if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        // 線程被取消,摘掉節點
        if (failed)
            cancelAcquire(node);
    }
}

獲取共享鎖總結:

  1. 嘗試獲得鎖,方法:tryAcquireShared
    1. 獲得鎖成功,直接返回;獲得鎖失敗,繼續執行下面邏輯;
  2. 再次嘗試獲得鎖,方法:doAcquireShared
    1. 新建排隊節點,并加入到同步隊列,方法:addWaiter,邏輯和獲得排它鎖的一致
    2. 自旋(嘗試獲得鎖,阻塞線程,等待被喚醒),直到成功獲得鎖

釋放共享鎖:public void unLock()

unlock方法調用的是sync.releaseShared(1),releaseShared也是AQS 方法中的方法,不允許被繼承,表示將同步狀態設置回初始狀態,將鎖釋放。

public final boolean releaseShared(int arg) {
    // tryReleaseShared 我們自己實現的邏輯
    if (tryReleaseShared(arg)) {
        // 釋放鎖失敗,繼續釋放,自旋直到釋放成功
        doReleaseShared();
        return true;
    }
    return false;
}

private void doReleaseShared() {
    // 自旋
    for (;;) {
        Node h = head;
        // 同步等待的隊列不為空
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            // 檢查狀態是否要喚醒下一個節點的線程
            if (ws == Node.SIGNAL) {
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                // 加入h節點是持有鎖的節點,會喚醒它的下一個節點線程
                unparkSuccessor(h);
            } else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        // 理論上喚醒一個就會退出
        if (h == head)                   // loop if head changed
            break;
    }
}

總結

  1. AQS是Java中可以實現同步器功能的一個基礎框架,我們自己也可以基于AQS實現想要的同步功能
  2. AQS 中用Node節點維護了一個雙向鏈表,用來保存排隊獲取鎖的線程,已經用來喚醒線程
  3. AQS 中為了一個state的同步狀態變量,可以基于這個變量實現很多功能
  4. 實現AQS的幾個重要API,就可以實現一個簡單同步器的功能,其他像自旋,排隊,阻塞,喚醒,AQS都已經幫我們做好了

AQS 條件鎖分析請看這里:源碼分析:②ReentrantLock之條件鎖Condition

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

推薦閱讀更多精彩內容