四七咧、通勤路上搞定 Java 多線程面試(1)

漫談面試系列

前言

談到多線程跃惫,一般都會(huì)聯(lián)想到高并發(fā),但是實(shí)際上兩者并不是一個(gè)概念艾栋,高并發(fā)一般指的是從業(yè)務(wù)方面的描述系統(tǒng)的并發(fā)負(fù)載能力爆存,而多線程只不過是如何使CPU的利用率達(dá)到最大化。因此一般問到高并發(fā)蝗砾,都會(huì)從你的項(xiàng)目業(yè)務(wù)角度出發(fā)先较,偏向于實(shí)戰(zhàn)方面,而多線程一般是問底層的一些編程技術(shù)方面的問題悼粮。
當(dāng)然闲勺,如果沒有掌握多線程的技術(shù),那就不用談所謂的高并發(fā)場(chǎng)景了扣猫。因此我們接下來先了解 Java 當(dāng)中多線程的一些基本知識(shí)點(diǎn)以及相應(yīng)的面試題菜循,而 JVM 層面的多線程面試題即JMM已經(jīng)在《漫談面試系列》——JVM(2)中談及,如果讀者沒有了解這方面的內(nèi)容苞笨,可以先前往該章節(jié)了解一下為什么我們需要關(guān)注線程安全問題债朵。

下面我們通過幾道常問的面試題進(jìn)行相關(guān)知識(shí)點(diǎn)的學(xué)習(xí):

  1. 實(shí)現(xiàn)多線程的方式有哪些,ThreadLocal使用中需要注意什么瀑凝?
  2. CAS 是什么序芦?CAS 有什么缺陷,如何解決粤咪?
  3. AQS 是什么谚中,它有什么作用?

1. 多線程的實(shí)現(xiàn)方式以及ThreadLocal注意點(diǎn)

這里主要介紹一下 Future/Callable 的多線程實(shí)現(xiàn)方式寥枝,其余兩種應(yīng)該就算是初學(xué)者都懂的了宪塔。首先 Callable 算是 Runnable 接口的一個(gè)補(bǔ)充,因?yàn)?Runnable 的 run() 方法是不帶返回值的囊拜,但是 Callable 的 call() 方法是帶返回值某筐,而為了不破壞原有的代碼和風(fēng)格,這個(gè) call() 方法會(huì)在 run() 方法里面調(diào)用冠跷,代碼如下:

//該run方法由FutureTask類實(shí)現(xiàn)
public void run() {
        if (state != NEW ||!UNSAFE.compareAndSwapObject(this, runnerOffset,null, Thread.currentThread()))
            return;
        try {
            Callable<V> c = callable;
            if (c != null && state == NEW) {
                V result;
                boolean ran;
                try {
                  //調(diào)用call方法
                    result = c.call();
                    ran = true;
                } catch (Throwable ex) {
                    result = null;
                    ran = false;
                    setException(ex);
                }
                if (ran)
                    set(result);
            }
        } finally {
            runner = null;
            int s = state;
            if (s >= INTERRUPTING)
                handlePossibleCancellationInterrupt(s);
        }
    }

Callable 接口提供了帶返回值的線程方法南誊,但是具體如何操作 Callable 接口則由 Future 接口的實(shí)現(xiàn)類進(jìn)行,并且其最終實(shí)現(xiàn)類有且唯有 FutureTask 類蜜托。


Future類的繼承關(guān)系

從上圖可以看出抄囚,F(xiàn)utureTask 實(shí)現(xiàn)了 Future 接口,那么它便獲得了針對(duì) Callable 實(shí)現(xiàn)類的一些操作橄务,例如獲取返回值(阻塞)幔托、判斷任務(wù)是否完成、判斷任務(wù)是否被取消蜂挪、取消任務(wù)等重挑;
同時(shí)它也實(shí)現(xiàn)了 Runnable 接口,那么它可以被 Thread 類使用棠涮。下面我們通過代碼看看 Future 接口有哪些關(guān)乎 Callable 操作的方法:

public interface Future<V> {
    //取消任務(wù)
    boolean cancel(boolean mayInterruptIfRunning);
    //任務(wù)是否結(jié)束攒驰,無論是完成導(dǎo)致的結(jié)束還是異常導(dǎo)致的結(jié)束
    boolean isCancelled();
     //任務(wù)是否完成
    boolean isDone();
    //阻塞式獲取,只要 call() 方法沒有接受故爵,就一直阻塞
    V get() throws InterruptedException, ExecutionException;
    //阻塞式獲取玻粪,但是帶有時(shí)間參數(shù),超過時(shí)間則直接返回
    V get(long timeout, TimeUnit unit)
            throws InterruptedException, ExecutionException, TimeoutException;
}

總的來說诬垂,我們需要定義 Callable 的 call() 方法劲室,這個(gè)方法可以類比為平時(shí)我們 Runnable 的 run() 方法,然后將 Callable 對(duì)象傳入給 FutureTask 類结窘,在 FutureTask 的 run() 方法中調(diào)用這個(gè) call() 方法很洋,除此之外,如果我們想得知目前該線程的執(zhí)行狀態(tài)和返回結(jié)果隧枫,也可以通過 FutureTask 類的一些方法如 isCancelled()喉磁、isDone()谓苟、get()等方法。接下來我們通過一個(gè)圖以及簡單的代碼demo了解下如何使用 FutureTask 實(shí)現(xiàn)多線程协怒。


FutureTask
public void future(){
        //這里通過lambda表達(dá)式直接生成匿名的Callable類
        FutureTask futureTask = new FutureTask(() -> {
            System.out.println("futureTask demo");
            return "demo";
        });
        Thread thread = new Thread(futureTask);
        thread.start();
        //判斷是否在運(yùn)行
        System.out.println(futureTask.isDone());
        //判斷是否以及取消
        System.out.println(futureTask.isCancelled());
        try {
            //獲取返回值
            System.out.println(futureTask.get());
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }

這里不過多闡述 FutureTask 的內(nèi)部細(xì)節(jié)涝焙,只為了讓讀者可以理清這種線程方式的邏輯,當(dāng)然 FutureTask 的核心還是在于它提供的異步操作的方法孕暇,這些內(nèi)容讀者可以參考相關(guān)文章仑撞。


很多業(yè)務(wù),我們需要在多線程環(huán)境下進(jìn)行一些變量的傳遞妖滔,而如果我們使用共享變量隧哮,又會(huì)由于多線程環(huán)境下一致性的問題導(dǎo)致業(yè)務(wù)出錯(cuò),針對(duì)這種共享變量的一致性座舍,我們只能上鎖進(jìn)行操作沮翔,這種行為相對(duì)來說非常損耗資源;

而如果我們使用局部變量曲秉,又會(huì)在方法的調(diào)用上難以傳參鉴竭,如果我的方法棧很深的話,便會(huì)有大量的方法的形參都得加上這個(gè)局部變量岸浑。

因此搏存,java 提供了一個(gè)方便我們線程隔離的局部變量工具類 ThreadLocal ,假如我們業(yè)務(wù)上不需要共享變量只需要局部變量的話矢洲,可以通過該工具類方便的進(jìn)行方法的調(diào)用傳參璧眠,保證每個(gè)線程獲取到的數(shù)據(jù)都是屬于各自線程的。

ThreadLocal 本質(zhì)上并不存儲(chǔ)任何東西读虏,最終存儲(chǔ)的內(nèi)容都是存放到 Thread 類的一個(gè)內(nèi)部 Map 類當(dāng)中责静,既然說到 Map,那肯定有 key 也有 value盖桥,而 ThreadLocal 對(duì)象便是這個(gè) Map 的 key灾螃,我們只要看看 ThreadLocal 的一個(gè) get()/set() 方法源碼,相信讀者就理解這個(gè)類到底是做什么用了揩徊。

public T get() {
    //獲取當(dāng)前線程
    Thread t = Thread.currentThread();
    //然后在這個(gè)線程里面獲取上面提到的 map
    ThreadLocalMap map = getMap(t);
     if (map != null) {
        //通過this來獲取對(duì)應(yīng)的value腰鬼,這個(gè)this便是ThreadLocal對(duì)象
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
     }
     return setInitialValue();
}
public void set(T value) {
      //這個(gè)類的方法都會(huì)通過這個(gè)語句獲取當(dāng)前的線程
      Thread t = Thread.currentThread();
      //然后在這個(gè)線程里面獲取上面提到的 map
      ThreadLocalMap map = getMap(t);
      if (map != null)
          //set操作是將 this 作為 key,至于這個(gè) key 便是 ThreadLocal 對(duì)象了
          map.set(this, value);
      else
          createMap(t, value);
  }
//獲取線程的map對(duì)象
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

基本上這兩個(gè)方法已經(jīng)表明了 ThreadLocal 到底是如何實(shí)現(xiàn)線程隔離的塑荒,其實(shí)數(shù)據(jù)本身就是存到對(duì)應(yīng)的 Thread 對(duì)象當(dāng)中熄赡,肯定是線程隔離的,只不過我們通過這個(gè) ThreadLocal 作為 key 齿税,方便我們獲取其中的數(shù)據(jù)而已彼硫,同時(shí),如果你想一個(gè)線程擁有多個(gè)局部變量,那么你就得申明多個(gè) ThreadLocal 對(duì)象進(jìn)行使用拧篮。

介紹完ThreadLocal 的基本知識(shí)词渤,接下來我們回到問題,我們使用 ThreadLocal 的時(shí)候需要注意什么串绩。

  1. 首先缺虐,我們的數(shù)據(jù)最終都是存儲(chǔ)在線程 Thread 類自身,因此如果我們使用線程池的時(shí)候赏参,由于存在線程的重復(fù)利用志笼,那么就有可能出現(xiàn)這個(gè)線程里面的 map 存有了不同業(yè)務(wù)環(huán)境的變量沿盅,最終可能導(dǎo)致業(yè)務(wù)方面的數(shù)據(jù)問題把篓,因此如果我們使用了線程池,務(wù)必在使用了 get() 方法后并且保證后續(xù)不需要這個(gè)變量了腰涧,在 finally 塊當(dāng)中調(diào)用 remove() 方法韧掩。
  2. 還有一點(diǎn)便是,由于 Thread 類中的 ThreadLocalMap 并不知道對(duì)應(yīng)的 key 什么時(shí)候會(huì)沒有引用指向窖铡,因此它無法判斷這個(gè) key 到底還有沒有用疗锐,于是它通過一個(gè)弱引用的形式來存儲(chǔ)這個(gè) ThreadLocal 對(duì)象作為 key ,只要外界沒有引用指向這個(gè) ThreadLocal 對(duì)象费彼,那么GC的時(shí)候便會(huì)自動(dòng)清理掉這個(gè) ThreadLocal 對(duì)象滑臊,不過這個(gè)帶來的問題便是,會(huì)導(dǎo)致 map 里面出現(xiàn)一對(duì) <null,Value> 的情況箍铲,假如出現(xiàn)這種情況后雇卷,線程一直沒有銷毀,也沒執(zhí)行 get\set\remove 方法的話颠猴,那么這個(gè) Value 便會(huì)一直存在关划,最終導(dǎo)致內(nèi)存泄漏因此每次使用完 ThreadLocal 對(duì)象翘瓮,盡量在調(diào)用一次 get\set\remove 方法贮折,因?yàn)檫@些方法會(huì)將 map 中 key 為 null 的鍵值對(duì)刪除

2. CAS是什么资盅?CAS 有什么缺陷调榄,如何解決?

CAS 全稱是 Compare And Swap 呵扛,即比較與交換振峻,它由一條指令完成,是原子性的择份,如果多個(gè)線程對(duì)同一個(gè)共享變量執(zhí)行 CAS 操作扣孟,最終只會(huì)有一個(gè)線程可以操作成功,操作成功返回 true荣赶,反之返回 false凤价。
在 Java 的 sun.misc.Unsafe 類下鸽斟,就有許多可以使用的 CAS 方法,這些方法都屬于 native 方法利诺,即最終實(shí)現(xiàn)由底層 C/C++ 完成富蓄,在 JUC 里面用的最多的應(yīng)該就是 compareAndSwapInt 方法。

//第一個(gè)參數(shù)是需要修改的對(duì)象
//第二個(gè)參數(shù)是這個(gè)對(duì)象需要修改的字段的內(nèi)存地址偏移量慢逾,用于直接找到相應(yīng)的內(nèi)存位置
//第三個(gè)參數(shù)是需要比較的值立倍,如果當(dāng)前內(nèi)存的值與這個(gè)值相同,才會(huì)進(jìn)行數(shù)據(jù)寫入
//第四個(gè)參數(shù)是需要寫入的值
public final native boolean compareAndSwapInt(Object var1, long offset, int expect, int update);

如果業(yè)務(wù)屬于CPU密集類型侣滩,一般我們會(huì)一直循環(huán)調(diào)用CAS來實(shí)現(xiàn)非阻塞資源競(jìng)爭口注,不需要通過鎖的形式來修改某個(gè)共享變量,從而減少線程切換導(dǎo)致的性能損耗君珠,但是這種做法可能會(huì)導(dǎo)致CPU的資源損耗寝志,因?yàn)樗姥h(huán)會(huì)一直占用 CPU 資源。

當(dāng)然策添,如果 CAS 能解決所有共享變量的線程安全問題材部,為什么我們還需要用鎖的形式呢?接下來我們來看看 CAS 的一些問題以及相應(yīng)的解決辦法:

  1. 自旋的CAS開銷大唯竹,如果線程一直競(jìng)爭資源失敗乐导,則會(huì)導(dǎo)致cpu一直自旋,造成不必要的開銷浸颓,如果業(yè)務(wù)允許放棄資源物臂,則可以通過設(shè)置超時(shí)時(shí)間解決.
  2. CAS只能保證一個(gè)共享變量的原子操作,如果我們想對(duì)多個(gè)共享變量進(jìn)行原子操作猾愿,只能通過鎖的形式解決.
  3. ABA問題鹦聪,如果ABA并沒有對(duì)業(yè)務(wù)有實(shí)質(zhì)上的影響的話,這個(gè)并不算問題蒂秘,當(dāng)如果業(yè)務(wù)想要了解該變量當(dāng)前的語義泽本,就會(huì)出現(xiàn)問題,例如棧的問題姻僧,存在棧A-B-C规丽,A是棧頂,有線程X想要將B改為棧頂撇贺,線程X獲取A的值時(shí)線程被掛起赌莺,同時(shí)線程Y介入,將棧A,B出棧松嘶,同時(shí)將A入棧艘狭,這個(gè)時(shí)候棧結(jié)構(gòu)為A-C,線程X喚醒,此時(shí)他對(duì)棧進(jìn)行出棧操作巢音,并將B作為棧頂遵倦,但由于B已經(jīng)出棧,B.next為null官撼,這就導(dǎo)致了C的丟失梧躺,最終棧結(jié)構(gòu)變?yōu)锽而不是最初的想法B-C,這個(gè)問題可以通過加入時(shí)間戳來實(shí)現(xiàn)預(yù)期值變化的感知.

3. AQS 是什么傲绣,它有什么作用

在傳統(tǒng)的多線程資源競(jìng)爭當(dāng)中掠哥,這些線程并沒有進(jìn)行統(tǒng)一的管理,可能導(dǎo)致的結(jié)果便是部分線程永遠(yuǎn)搶占不到鎖秃诵,即饑餓問題续搀。
為了對(duì)多線程并發(fā)進(jìn)行一個(gè)統(tǒng)一管理,AQS 即 AbstractQueuedSynchronizer 誕生了顷链。

AQS 的本質(zhì)是通過一個(gè)雙向鏈表和一個(gè)狀態(tài)值的結(jié)構(gòu)來管理多個(gè)線程目代,鏈表里面存儲(chǔ)的是一個(gè)個(gè)封裝好的線程對(duì)象節(jié)點(diǎn)屈梁,而狀態(tài)值代表了當(dāng)前鎖的狀態(tài)嗤练,其中 AQS 會(huì)使用 CAS 進(jìn)行狀態(tài)值的修改,從而保證當(dāng)前鎖的狀態(tài)值無誤在讶。

接下來我們通過一系列的圖片煞抬,來了解下 AQS 的基本原理。

首先我們來看看 AQS 的結(jié)構(gòu)是怎么樣的:


AQS結(jié)構(gòu).png

這里有個(gè)關(guān)鍵點(diǎn)构哺,即 head 節(jié)點(diǎn)本質(zhì)上不算是阻塞隊(duì)列的一員革答,因?yàn)樵摴?jié)點(diǎn)的線程是持有鎖的,后續(xù)節(jié)點(diǎn)的喚醒都需要通過 head 節(jié)點(diǎn)來實(shí)現(xiàn)曙强,而這個(gè) head 節(jié)點(diǎn)里面封裝的線程具體是什么也不重要

AQS的結(jié)構(gòu)非常簡單残拐,接著我們借助 ReentrantLock 來了解下 AQS 如何管理線程:
AQS-start.png

以下操作中,線程顏色對(duì)應(yīng)文字顏色碟嘴,即流程文字對(duì)應(yīng)相應(yīng)的線程操作

AQS-T1.png
AQS-T2.png
AQS-T2.png

為了預(yù)防當(dāng)前節(jié)點(diǎn)的前驅(qū)節(jié)點(diǎn)被中斷取消導(dǎo)致無法喚醒當(dāng)前節(jié)點(diǎn)溪食,在 shouldParkAfterFailedAcquire() 方法中會(huì)通過遍歷的形式尋找一個(gè)"有效的前驅(qū)節(jié)點(diǎn)"進(jìn)行關(guān)聯(lián)并設(shè)置這個(gè)前驅(qū)節(jié)點(diǎn)的 waitStatus 值

可以看出,AQS 入隊(duì)操作主要分為 3 個(gè)步驟娜扇,分別是:

  1. 競(jìng)爭鎖错沃,競(jìng)爭成功直接持有鎖,失敗則進(jìn)入步驟 2 雀瓢;如果當(dāng)前是公平鎖并且阻塞隊(duì)列的 tail 不為空枢析,則直接進(jìn)入步驟 2 而不進(jìn)行競(jìng)爭
  2. 將當(dāng)前線程包裝為一個(gè) Node 節(jié)點(diǎn),如果隊(duì)列沒初始化則先進(jìn)行初始化操作刃麸,等到阻塞隊(duì)列的 tail 不為空醒叁,則將 tail 賦值為當(dāng)前的 Node 節(jié)點(diǎn),并與上任 tail 節(jié)點(diǎn)進(jìn)行關(guān)聯(lián)
  3. 如果當(dāng)前節(jié)點(diǎn)的前驅(qū)節(jié)點(diǎn)為 head ,則嘗試競(jìng)爭鎖把沼,競(jìng)爭失敗則將當(dāng)前節(jié)點(diǎn)的有效前驅(qū)節(jié)點(diǎn)的waitStatus設(shè)置為-1断傲,最后進(jìn)入阻塞狀態(tài),等待前驅(qū)節(jié)點(diǎn)的喚醒

接下來我們?cè)诳焖龠^一遍入隊(duì)過程:
AQS-T3.png

AQS-T3.png

AQS-7.png

看完了 AQS 的入隊(duì)操作智政,接下來我們看看 AQS 的出隊(duì)操作是如何進(jìn)行:
AQS-unlock-T1.png
AQS-unlock-T1.png

這里為什么要從尾部往前遍歷呢? 主要原因是防止中間有節(jié)點(diǎn)已經(jīng)處于取消狀態(tài)并且與它的后續(xù)節(jié)點(diǎn)斷開關(guān)聯(lián)導(dǎo)致無法喚醒阻塞隊(duì)列其他的等待節(jié)點(diǎn)认罩,因此從后往前遍歷一個(gè)最接近 head 節(jié)點(diǎn)的阻塞節(jié)點(diǎn)進(jìn)行喚醒

AQS-unlock-T2.png

AQS 的出隊(duì)以及喚醒操作主要分為 3 個(gè)步驟:

  1. 持有鎖的線程修改 state 字段以及 exclusiveOwnerThread 字段(不考慮重入)
  2. 如果當(dāng)前線程節(jié)點(diǎn)的 waitStatus 不為0,從后往前遍歷尋找最接近 head 的節(jié)點(diǎn)并將該節(jié)點(diǎn)的線程喚醒
  3. 被喚醒的線程先將前驅(qū)節(jié)點(diǎn)設(shè)置為 head (如果已經(jīng)是 head 則跳過該步驟)续捂,然后進(jìn)行鎖的競(jìng)爭(非公平鎖的話有可能競(jìng)爭失敗然后進(jìn)入阻塞)垦垂,競(jìng)爭成功后修改 state 字段以及 exclusiveOwnerThread 字段,并將 head 賦值為當(dāng)前節(jié)點(diǎn)

AQS 使用了模板設(shè)計(jì)模式牙瓢,將阻塞隊(duì)列的隊(duì)列操作自行實(shí)現(xiàn)劫拗,然后給子類留下了 tryAcquire(競(jìng)爭鎖)/tryRelease(釋放鎖)等模板方法,例如自定義公平鎖或非公平鎖的實(shí)現(xiàn)矾克,子類通過繼承 AQS 來實(shí)現(xiàn)具體的上鎖\釋放鎖的方法页慷,因此 AQS 的作用是為同步工具類提供基于阻塞隊(duì)列的組件。

同時(shí)胁附,為了保證 AQS 在多線程進(jìn)行入隊(duì)-出隊(duì)操作時(shí)的線程安全操作酒繁, AQS 內(nèi)部使用了大量的自旋 CAS 操作保證線程安全,并利用了 LockSupport 類的 park() 和 unpark() 方法進(jìn)行線程的阻塞和釋放控妻。

AQS 典型的實(shí)現(xiàn)類有 ReentrantLock 州袒,它通過一個(gè)內(nèi)部類 FairSync/NonfairSync 繼承 AQS 定義了公平鎖/非公平鎖競(jìng)爭鎖的具體邏輯。搞懂了 AQS 弓候,在看 JUC 包內(nèi)的同步工具類便會(huì)變得很簡單郎哭,因?yàn)槟阒恍枰P(guān)注他們不同的上鎖/釋放鎖的邏輯即可,至于線程的管理菇存,幾乎都是一樣夸研。

總結(jié)

在該篇章中,我首先介紹了帶有返回值的多線程實(shí)現(xiàn)方法依鸥,其次介紹了在同步容器中經(jīng)常出現(xiàn)的 CAS 亥至,最后介紹了同步容器的核心類 AQS ,其中 AQS 的內(nèi)容可能偏多毕籽,建議讀者在閱覽 AQS 流程圖解的時(shí)候可以關(guān)聯(lián)下具體代碼的實(shí)現(xiàn)抬闯,可以有效幫助理解 AQS 的運(yùn)行流程和作用。

接下來关筒,我們回到最初的三個(gè)問題

  1. 實(shí)現(xiàn)多線程的方式有哪些溶握,ThreadLocal使用中需要注意什么?
  2. CAS 是什么蒸播?CAS 有什么缺陷睡榆,如何解決萍肆?
  3. AQS 是什么,它有什么作用胀屿?

如果你們可以很流暢的回答這些問題塘揣,那么恭喜你,該章節(jié)的內(nèi)容已經(jīng)全部掌握宿崭,如果不行亲铡,希望可以回到對(duì)應(yīng)問題講解的地方,或者對(duì)某個(gè)不了解的點(diǎn)進(jìn)行額外的知識(shí)搜索葡兑,盡量用自己組織的語言回答這些問題奖蔓。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市讹堤,隨后出現(xiàn)的幾起案子吆鹤,更是在濱河造成了極大的恐慌,老刑警劉巖洲守,帶你破解...
    沈念sama閱讀 217,734評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件疑务,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡梗醇,警方通過查閱死者的電腦和手機(jī)知允,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,931評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來婴削,“玉大人廊镜,你說我怎么就攤上這事牙肝“λ祝” “怎么了?”我有些...
    開封第一講書人閱讀 164,133評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵配椭,是天一觀的道長虫溜。 經(jīng)常有香客問我,道長股缸,這世上最難降的妖魔是什么衡楞? 我笑而不...
    開封第一講書人閱讀 58,532評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮敦姻,結(jié)果婚禮上瘾境,老公的妹妹穿的比我還像新娘。我一直安慰自己镰惦,他們只是感情好迷守,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,585評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著旺入,像睡著了一般兑凿。 火紅的嫁衣襯著肌膚如雪凯力。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,462評(píng)論 1 302
  • 那天礼华,我揣著相機(jī)與錄音咐鹤,去河邊找鬼。 笑死圣絮,一個(gè)胖子當(dāng)著我的面吹牛祈惶,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播扮匠,決...
    沈念sama閱讀 40,262評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼行瑞,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了餐禁?” 一聲冷哼從身側(cè)響起血久,我...
    開封第一講書人閱讀 39,153評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎帮非,沒想到半個(gè)月后氧吐,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,587評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡末盔,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,792評(píng)論 3 336
  • 正文 我和宋清朗相戀三年筑舅,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片陨舱。...
    茶點(diǎn)故事閱讀 39,919評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡翠拣,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出游盲,到底是詐尸還是另有隱情误墓,我是刑警寧澤,帶...
    沈念sama閱讀 35,635評(píng)論 5 345
  • 正文 年R本政府宣布益缎,位于F島的核電站谜慌,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏莺奔。R本人自食惡果不足惜欣范,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,237評(píng)論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望令哟。 院中可真熱鬧恼琼,春花似錦、人聲如沸屏富。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,855評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽役听。三九已至颓鲜,卻和暖如春表窘,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背甜滨。 一陣腳步聲響...
    開封第一講書人閱讀 32,983評(píng)論 1 269
  • 我被黑心中介騙來泰國打工乐严, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人衣摩。 一個(gè)月前我還...
    沈念sama閱讀 48,048評(píng)論 3 370
  • 正文 我出身青樓昂验,卻偏偏與公主長得像,于是被迫代替她去往敵國和親艾扮。 傳聞我的和親對(duì)象是個(gè)殘疾皇子既琴,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,864評(píng)論 2 354