ThreadLocal 由淺入深畦韭,深入解讀

ThreadLocal 介紹

Java官方文檔中的描述:ThreadLocal 類用來提供線程內部的局部變量。這種變量在多線程環(huán)境下訪問(通過get和set方法訪問)時能保證各個線程的變量相對獨立于其它線程內的變量。ThreadLocal實例通常來說都是 private static 類型的桨踪,用于關聯(lián)線程和線程上下文讥巡。

我們知道 ThreadLocal 的作用是:提供線程內的局部變量掀亩,不同線程之間不會相互干擾,這種變量在線程的生命周期內起作用欢顷,減少同一個線程內多個函數(shù)或組件之間一些公共變量傳遞的復雜度槽棍。

總結:
1.線程并發(fā):在多線程并發(fā)的場景下
2.傳遞數(shù)據(jù):我們可以通過ThreadLocal在同一線程,不同組件中傳遞公共變量
3.線程隔離:每個線程的變量都是獨立的抬驴,不會相互影響

基本使用

常用方法

方法聲明 描述
ThreadLocal() 創(chuàng)建ThreadLocal對象
public void set(T value) 設置當前線程綁定的局部變量
public T get() 獲取當前線程綁定的局部變量
public void remove() 移除當前線程綁定的局部變量

使用案例

我們先看下多線程并發(fā)時共享變量的錯誤案例:

package com.wangcp.test;

/**
 * 需求:線程隔離
 *      在多線程并發(fā)場景下炼七,每個線程中的變量都是相互獨立的
 *      現(xiàn)場A:設置(變量1)   獲取(變量1)
 *      線程B:設置(變量2)   獲炔汲帧(變量2)
 * */
public class Demo01 {

    // 設置變量
    private String content;

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public static void main(String[] args) {
        Demo01 demo = new Demo01();
        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(()->{
                demo.setContent(Thread.currentThread().getName() + "的數(shù)據(jù)");
                System.out.println("----------------------------");
                System.out.println(Thread.currentThread().getName() + "-->" + demo.getContent());
            });
            thread.setName("線程"+ i);
            thread.start();
        }
    }
}

運行結果如下:

image-20211107182851943.png

我們知道Java的調度是一個搶占式調度豌拙,通過運行結果我們看出,多線程之間數(shù)據(jù)隔離的問題题暖,線程A取出了線程B的數(shù)據(jù)按傅。

接著我們加入ThreadLocal后再進行測試:

package com.wangcp.test;
/**
 * 需求:線程隔離
 *      在多線程并發(fā)場景下,每個線程中的變量都是相互獨立的
 *      現(xiàn)場A:設置(變量1)   獲入事薄(變量1)
 *      線程B:設置(變量2)   獲任ㄉ堋(變量2)
 *
 *      ThreadLocal:
 *          1.set() : 將變量綁定到當前線程中
 *          2.get() : 獲取當前線程綁定的變量
 * */
public class Demo01 {
    ThreadLocal<String> t1 = new ThreadLocal<>();
    // 設置變量
    private String content;

    public String getContent() {
//        return content;
        return t1.get();
    }

    public void setContent(String content) {
//        this.content = content;
        t1.set(content);
    }

    public static void main(String[] args) {
        Demo01 demo = new Demo01();
        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(()->{
                /**
                 * 每個線程:存一個變量,過一會后取出這個變量
                 */
                demo.setContent(Thread.currentThread().getName() + "的數(shù)據(jù)");
                System.out.println("----------------------------");
                System.out.println(Thread.currentThread().getName() + "-->" + demo.getContent());
            });
            thread.setName("線程"+ i);
            thread.start();
        }
    }
}

運行結果如下:

image-20211107190121826.png

在我們加入ThreadLocal后枝誊,實現(xiàn)了線程間數(shù)據(jù)的隔離况芒,線程取到的數(shù)據(jù)都是自身所設置的數(shù)據(jù)。

ThreadLocal類與synchronized關鍵字

synchronized 同步方式

可能有的朋友會覺得在上述的例子中我們完全可以通過加鎖來實現(xiàn)這個功能叶撒,我們來看下用synchronized代碼塊實現(xiàn)的效果:

package com.wangcp.test;
/**
 * 需求:線程隔離
 *      在多線程并發(fā)場景下绝骚,每個線程中的變量都是相互獨立的
 *      現(xiàn)場A:設置(變量1)   獲取(變量1)
 *      線程B:設置(變量2)   獲热(變量2)
 * */
public class Demo02 {
    // 設置變量
    private String content;

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public static void main(String[] args) {
        Demo02 demo = new Demo02();
        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(()->{
                /**
                 * 每個線程:存一個變量皮壁,過一會后取出這個變量
                 */
                synchronized (Demo02.class){
                    demo.setContent(Thread.currentThread().getName() + "的數(shù)據(jù)");
                    System.out.println("----------------------------");
                    System.out.println(Thread.currentThread().getName() + "-->" + demo.getContent());
                }
            });
            thread.setName("線程"+ i);
            thread.start();
        }
    }
}

運行結果如下:

image-20211107192337902.png

從運行結果看出,加鎖確實可以解決這個問題哪审,但是在這里我們強調的是線程數(shù)據(jù)隔離問題蛾魄,并不是多線程共享數(shù)據(jù)的問題,在這個案例中使用synchronized關鍵字是不合適的。

ThreadLocal與synchronized的區(qū)別

    雖然 ThreadLocal 模式與 synchronized 關鍵字都用于處理多線程并發(fā)訪問變量的問題滴须,不過兩者處理問題的角度和思路不同舌狗。
synchronized ThreadLocal
原理 同步機制采用“以時間換空間”的方式,只提供了一份變量扔水,讓不同的線程排隊訪問 ThreadLocal采用“以空間換時間”的方式痛侍,為每一個線程都提供了一份變量的副本,從而實現(xiàn)同時訪問而不想干擾
側重點 多個線程之間訪問資源的同步 多線程中讓每個線程之間的數(shù)據(jù)相互隔離
總結:在上面的案例中魔市,雖然使用ThreadLocal和synchronized都能解決問題主届,但是使用ThreadLocal更為合適,因為這樣可以使程序擁有更高的并發(fā)性待德。

ThreadLocal 的內部結構

通過以上的學習君丁,我們對ThreadLocal的作用有了一定的認識。現(xiàn)在我們一起來看一下ThreadLocal的內部結構将宪,探究它能夠實現(xiàn)線程數(shù)據(jù)隔離的原理绘闷。

常見的誤解
如果我們不去看源代碼的話,可能會猜測ThreadLocal是這樣子設計的:每個ThreadLocal都創(chuàng)建一個Map较坛,然后用線程作為Map的key印蔗,要存儲的局部變量作為Map的value,這樣就能達到各個線程的局部變量隔離的效果丑勤。這是最簡單的設計方法华嘹,JDK最早期的ThreadLocal 確實是這樣設計的,但現(xiàn)在早已不是了确封。

img

現(xiàn)在的設計
但是除呵,JDK后面優(yōu)化了設計方案,在JDK8中 ThreadLocal的設計是:每個Thread維護一個ThreadLocalMap爪喘,這個Map的key是ThreadLocal實例本身颜曾,value才是真正要存儲的值Object。

具體的過程是這樣的:

  • 每個Thread線程內部都有一個Map (ThreadLocalMap)

  • Map里面存儲ThreadLocal對象(key)和線程的變量副本(value)

  • Thread內部的Map是由ThreadLocal維護的秉剑,由ThreadLocal負責向map獲取和設置線程的變量值泛豪。

  • 對于不同的線程,每次獲取副本值時侦鹏,別的線程并不能獲取到當前線程的副本值诡曙,形成了副本的隔離,互不干擾略水。

img

這樣設計的好處
這個設計與我們一開始說的設計剛好相反价卤,這樣設計有如下兩個優(yōu)勢:

這樣設計之后每個Map存儲的Entry數(shù)量就會變少。因為之前的存儲數(shù)量由Thread的數(shù)量決定渊涝,現(xiàn)在是由ThreadLocal的數(shù)量決定慎璧。在實際運用當中床嫌,往往ThreadLocal的數(shù)量要少于Thread的數(shù)量。
當Thread銷毀之后胸私,對應的ThreadLocalMap也會隨之銷毀厌处,能減少內存的使用。

ThreadLocal 的核心方法源碼

基于ThreadLocal的內部結構岁疼,我們繼續(xù)分析它的核心方法源碼阔涉,更深入的了解其操作原理。

除了構造方法之外捷绒, ThreadLocal對外暴露的方法有以下4個:

方法聲明 描述
protected T initialValue() 返回當前線程局部變量的初始值
public void set( T value) 設置當前線程綁定的局部變量
public T get() 獲取當前線程綁定的局部變量
public void remove() 移除當前線程綁定的局部變量

以下是這4個方法的詳細源碼分析(為了保證思路清晰, ThreadLocalMap部分暫時不展開,下一個知識點詳解)

set方法

源碼和對應的中文注釋

/**
  * 設置當前線程對應的ThreadLocal的值
  * @param value 將要保存在當前線程對應的ThreadLocal的值
  */
   public void set(T value) {
       // 獲取當前線程對象
       Thread t = Thread.currentThread();
       // 獲取此線程對象中維護的ThreadLocalMap對象
       ThreadLocalMap map = getMap(t);
       // 判斷map是否存在
       if (map != null)
           // 存在則調用map.set設置此實體entry
           map.set(this, value);
       else
           // 1)當前線程Thread 不存在ThreadLocalMap對象
           // 2)則調用createMap進行ThreadLocalMap對象的初始化
           // 3)并將 t(當前線程)和value(t對應的值)作為第一個entry存放至ThreadLocalMap中
           createMap(t, value);
   }

 /**
     * 獲取當前線程Thread對應維護的ThreadLocalMap 
     * 
     * @param  t the current thread 當前線程
     * @return the map 對應維護的ThreadLocalMap 
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    /**
     *創(chuàng)建當前線程Thread對應維護的ThreadLocalMap 
     *
     * @param t 當前線程
     * @param firstValue 存放到map中第一個entry的值
     */
    void createMap(Thread t, T firstValue) {
        //這里的this是調用此方法的threadLocal
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

代碼執(zhí)行流程

A. 首先獲取當前線程瑰排,并根據(jù)當前線程獲取一個Map

B. 如果獲取的Map不為空,則將參數(shù)設置到Map中(當前ThreadLocal的引用作為key)

C. 如果Map為空暖侨,則給該線程創(chuàng)建 Map凶伙,并設置初始值

get方法

源碼和對應的中文注釋

/**
 * 返回當前線程中保存ThreadLocal的值
 * 如果當前線程沒有此ThreadLocal變量,
 * 則它會通過調用{@link #initialValue} 方法進行初始化值
 *
 * @return 返回當前線程對應此ThreadLocal的值
 */
public T get() {
    // 獲取當前線程對象
    Thread t = Thread.currentThread();
    // 獲取此線程對象中維護的ThreadLocalMap對象
    ThreadLocalMap map = getMap(t);
    // 如果此map存在
    if (map != null) {
        // 以當前的ThreadLocal 為 key它碎,調用getEntry獲取對應的存儲實體e
        ThreadLocalMap.Entry e = map.getEntry(this);
        // 對e進行判空 
        if (e != null) {
            @SuppressWarnings("unchecked")
            // 獲取存儲實體 e 對應的 value值
            // 即為我們想要的當前線程對應此ThreadLocal的值
            T result = (T)e.value;
            return result;
        }
    }
    /*
        初始化 : 有兩種情況有執(zhí)行當前代碼
        第一種情況: map不存在,表示此線程沒有維護的ThreadLocalMap對象
        第二種情況: map存在, 但是沒有與當前ThreadLocal關聯(lián)的entry
     */
    return setInitialValue();
}

/**
 * 初始化
 *
 * @return the initial value 初始化后的值
 */
private T setInitialValue() {
    // 調用initialValue獲取初始化的值
    // 此方法可以被子類重寫, 如果不重寫默認返回null
    T value = initialValue();
    // 獲取當前線程對象
    Thread t = Thread.currentThread();
    // 獲取此線程對象中維護的ThreadLocalMap對象
    ThreadLocalMap map = getMap(t);
    // 判斷map是否存在
    if (map != null)
        // 存在則調用map.set設置此實體entry
        map.set(this, value);
    else
        // 1)當前線程Thread 不存在ThreadLocalMap對象
        // 2)則調用createMap進行ThreadLocalMap對象的初始化
        // 3)并將 t(當前線程)和value(t對應的值)作為第一個entry存放至ThreadLocalMap中
        createMap(t, value);
    // 返回設置的值value
    return value;
}

代碼執(zhí)行流程

A. 首先獲取當前線程, 根據(jù)當前線程獲取一個Map

B. 如果獲取的Map不為空显押,則在Map中以ThreadLocal的引用作為key來在Map中獲取對應的Entry e扳肛,否則轉到D

C. 如果e不為null,則返回e.value乘碑,否則轉到D

D. Map為空或者e為空挖息,則通過initialValue函數(shù)獲取初始值value,然后用ThreadLocal的引用和value作為firstKey和firstValue創(chuàng)建一個新的Map

總結: 先獲取當前線程的 ThreadLocalMap 變量兽肤,如果存在則返回值套腹,不存在則創(chuàng)建并返回初始值。

remove方法

源碼和對應的中文注釋

/**
  * 刪除當前線程中保存的ThreadLocal對應的實體entry
  */
   public void remove() {
     // 獲取當前線程對象中維護的ThreadLocalMap對象
     ThreadLocalMap m = getMap(Thread.currentThread());
     // 如果此map存在
     if (m != null)
        // 存在則調用map.remove
        // 以當前ThreadLocal為key刪除對應的實體entry
         m.remove(this);
   }

代碼執(zhí)行流程

A. 首先獲取當前線程资铡,并根據(jù)當前線程獲取一個Map

B. 如果獲取的Map不為空电禀,則移除當前ThreadLocal對象對應的entry

initialValue方法

/**
 * 返回當前線程對應的ThreadLocal的初始值
  * 此方法的第一次調用發(fā)生在,當線程通過get方法訪問此線程的ThreadLocal值時
  * 除非線程先調用了set方法笤休,在這種情況下尖飞,initialValue 才不會被這個線程調用。
  * 通常情況下店雅,每個線程最多調用一次這個方法政基。
  *
  * <p>這個方法僅僅簡單的返回null {@code null};
  * 如果程序員想ThreadLocal線程局部變量有一個除null以外的初始值,
  * 必須通過子類繼承{@code ThreadLocal} 的方式去重寫此方法
  * 通常, 可以通過匿名內部類的方式實現(xiàn)
  *
  * @return 當前ThreadLocal的初始值
  */
  protected T initialValue() {
    return null;
  }

此方法的作用是 返回該線程局部變量的初始值闹啦。

(1) 這個方法是一個延遲調用方法沮明,從上面的代碼我們得知,在set方法還未調用而先調用了get方法時才執(zhí)行窍奋,并且僅執(zhí)行1次荐健。

(2)這個方法缺省實現(xiàn)直接返回一個null酱畅。

(3)如果想要一個除null之外的初始值,可以重寫此方法摧扇。(備注: 該方法是一個protected的方法圣贸,顯然是為了讓子類覆蓋而設計的)

ThreadLocalMap 源碼分析

在分析ThreadLocal方法的時候,我們了解到ThreadLocal的操作實際上是圍繞ThreadLocalMap展開的扛稽。ThreadLocalMap的源碼相對比較復雜, 我們從以下三個方面進行討論吁峻。

基本結構

ThreadLocalMap是ThreadLocal的內部類,沒有實現(xiàn)Map接口在张,用獨立的方式實現(xiàn)了Map的功能用含,其內部的Entry也是獨立實現(xiàn)。

img

成員變量

/**
 * 初始容量 —— 必須是2的整次冪
 */
private static final int INITIAL_CAPACITY = 16;

/**
 * 存放數(shù)據(jù)的table帮匾,Entry類的定義在下面分析
 * 同樣啄骇,數(shù)組長度必須是2的整次冪。
 */
private Entry[] table;

/**
 * 數(shù)組里面entrys的個數(shù)瘟斜,可以用于判斷table當前使用量是否超過閾值缸夹。
 */
private int size = 0;

/**
 * 進行擴容的閾值,表使用量大于它的時候進行擴容螺句。
 */
private int threshold; // Default to 0

跟HashMap類似虽惭,INITIAL_CAPACITY代表這個Map的初始容量;table 是一個Entry 類型的數(shù)組蛇尚,用于存儲數(shù)據(jù)芽唇;size 代表表中的存儲數(shù)目; threshold 代表需要擴容時對應 size 的閾值

存儲結構 - Entry

/*
 * Entry繼承WeakReference取劫,并且用ThreadLocal作為key.
 * 如果key為null(entry.get() == null)匆笤,意味著key不再被引用,
 * 因此這時候entry也可以從table中清除谱邪。
 */
   static class Entry extends WeakReference<ThreadLocal<?>> {
   /** The value associated with this ThreadLocal. */
   Object value;

   Entry(ThreadLocal<?> k, Object v) {
       super(k);
       value = v;
   }
 }

在ThreadLocalMap中炮捧,也是用Entry來保存K-V結構數(shù)據(jù)的。不過Entry中的key只能是ThreadLocal對象惦银,這點在構造方法中已經限定死了寓盗。

另外,Entry繼承WeakReference璧函,也就是key(ThreadLocal)是弱引用傀蚌,其目的是將ThreadLocal對象的生命周期和線程生命周期解綁。

弱引用和內存泄漏

有些程序員在使用ThreadLocal的過程中會發(fā)現(xiàn)有內存泄漏的情況發(fā)生蘸吓,就猜測這個內存泄漏跟Entry中使用了弱引用的key有關系善炫。這個理解其實是不對的。

我們先來回顧這個問題中涉及的幾個名詞概念库继,再來分析問題箩艺。

內存泄漏相關概念

  • Memory overflow:內存溢出窜醉,沒有足夠的內存提供申請者使用。
  • Memory leak: 內存泄漏是指程序中已動態(tài)分配的堆內存由于某種原因程序未釋放或無法釋放艺谆,造成系統(tǒng)內存的浪費榨惰,導致程序運行速度減慢甚至系統(tǒng)崩潰等嚴重后果。內存泄漏的堆積終將導致內存溢出静汤。

弱引用相關概念

Java中的引用有4種類型: 強琅催、軟、弱虫给、虛藤抡。當前這個問題主要涉及到強引用和弱引用:

強引用(“Strong” Reference),就是我們最常見的普通對象引用抹估,只要還有強引用指向一個對象缠黍,就能表明對象還“活著”,垃圾回收器就不會回收這種對象药蜻。

弱引用(WeakReference)瓷式,垃圾回收器一旦發(fā)現(xiàn)了只具有弱引用的對象,不管當前內存空間足夠與否语泽,都會回收它的內存蒿往。

如果key使用強引用

假設ThreadLocalMap中的key使用了強引用,那么會出現(xiàn)內存泄漏嗎湿弦?

此時ThreadLocal的內存圖(實線表示強引用)如下:

img

假設在業(yè)務代碼中使用完ThreadLocal ,threadLocal Ref被回收了腾夯。

但是因為threadLocalMap的Entry強引用了threadLocal颊埃,造成threadLocal無法被回收。

在沒有手動刪除這個Entry以及CurrentThread依然運行的前提下蝶俱,始終有強引用鏈 threadRef->currentThread->threadLocalMap->entry班利,Entry就不會被回收(Entry中包括了ThreadLocal實例和value),導致Entry內存泄漏榨呆。

也就是說罗标,ThreadLocalMap中的key使用了強引用, 是無法完全避免內存泄漏的积蜻。

如果key使用弱引用

那么ThreadLocalMap中的key使用了弱引用闯割,會出現(xiàn)內存泄漏嗎?

此時ThreadLocal的內存圖(實線表示強引用竿拆,虛線表示弱引用)如下:

img

同樣假設在業(yè)務代碼中使用完ThreadLocal 宙拉,threadLocal Ref被回收了。

由于ThreadLocalMap只持有ThreadLocal的弱引用丙笋,沒有任何強引用指向threadlocal實例, 所以threadlocal就可以順利被gc回收谢澈,此時Entry中的key=null煌贴。

但是在沒有手動刪除這個Entry以及CurrentThread依然運行的前提下,也存在有強引用鏈 threadRef->currentThread->threadLocalMap->entry -> value 锥忿,value不會被回收牛郑, 而這塊value永遠不會被訪問到了,導致value內存泄漏敬鬓。

也就是說淹朋,ThreadLocalMap中的key使用了弱引用, 也有可能內存泄漏列林。

出現(xiàn)內存泄漏的真實原因

比較以上兩種情況瑞你,我們就會發(fā)現(xiàn),內存泄漏的發(fā)生跟ThreadLocalMap中的key是否使用弱引用是沒有關系的希痴。那么內存泄漏的的真正原因是什么呢者甲?

細心的同學會發(fā)現(xiàn),在以上兩種內存泄漏的情況中砌创,都有兩個前提:

1.沒有手動刪除這個Entry
2.CurrentThread依然運行

第一點很好理解虏缸,只要在使用完ThreadLocal,調用其remove方法刪除對應的Entry嫩实,就能避免內存泄漏刽辙。

第二點稍微復雜一點, 由于ThreadLocalMap是Thread的一個屬性甲献,被當前線程所引用宰缤,所以它的生命周期跟Thread一樣長。那么在使用完ThreadLocal之后晃洒,如果當前Thread也隨之執(zhí)行結束慨灭,ThreadLocalMap自然也會被gc回收,從根源上避免了內存泄漏球及。

綜上氧骤,ThreadLocal內存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一樣長,如果沒有手動刪除對應key就會導致內存泄漏吃引。

為什么使用弱引用

根據(jù)剛才的分析, 我們知道了: 無論ThreadLocalMap中的key使用哪種類型引用都無法完全避免內存泄漏筹陵,跟使用弱引用沒有關系。

要避免內存泄漏有兩種方式:

使用完ThreadLocal镊尺,調用其remove方法刪除對應的Entry

使用完ThreadLocal朦佩,當前Thread也隨之運行結束

相對第一種方式,第二種方式顯然更不好控制庐氮,特別是使用線程池的時候吕粗,線程結束是不會銷毀的。

也就是說旭愧,只要記得在使用完ThreadLocal及時的調用remove,無論key是強引用還是弱引用都不會有問題。那么為什么key要用弱引用呢庶灿?

事實上先紫,在ThreadLocalMap中的set/getEntry方法中,會對key為null(也即是ThreadLocal為null)進行判斷,如果為null的話,那么是會對value置為null的。

這就意味著使用完ThreadLocal型奥,CurrentThread依然運行的前提下,就算忘記調用remove方法碉京,弱引用比強引用可以多一層保障:弱引用的ThreadLocal會被回收厢汹,對應的value在下一次ThreadLocalMap調用set,get,remove中的任一方法的時候會被清除,從而避免內存泄漏谐宙。

本篇博客是觀看B站黑馬關于ThreadLocal 的視頻后進行整理的烫葬,相當于是對應視頻的筆記。如有問題歡迎大家多多建議和指正凡蜻。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末搭综,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子划栓,更是在濱河造成了極大的恐慌兑巾,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,270評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件忠荞,死亡現(xiàn)場離奇詭異蒋歌,居然都是意外死亡,警方通過查閱死者的電腦和手機委煤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評論 3 395
  • 文/潘曉璐 我一進店門堂油,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人素标,你說我怎么就攤上這事∑笺玻” “怎么了头遭?”我有些...
    開封第一講書人閱讀 165,630評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長癣诱。 經常有香客問我计维,道長,這世上最難降的妖魔是什么撕予? 我笑而不...
    開封第一講書人閱讀 58,906評論 1 295
  • 正文 為了忘掉前任鲫惶,我火速辦了婚禮,結果婚禮上实抡,老公的妹妹穿的比我還像新娘欠母。我一直安慰自己欢策,他們只是感情好,可當我...
    茶點故事閱讀 67,928評論 6 392
  • 文/花漫 我一把揭開白布赏淌。 她就那樣靜靜地躺著踩寇,像睡著了一般。 火紅的嫁衣襯著肌膚如雪六水。 梳的紋絲不亂的頭發(fā)上俺孙,一...
    開封第一講書人閱讀 51,718評論 1 305
  • 那天,我揣著相機與錄音掷贾,去河邊找鬼睛榄。 笑死,一個胖子當著我的面吹牛想帅,可吹牛的內容都是我干的场靴。 我是一名探鬼主播,決...
    沈念sama閱讀 40,442評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼博脑,長吁一口氣:“原來是場噩夢啊……” “哼憎乙!你這毒婦竟也來了?” 一聲冷哼從身側響起叉趣,我...
    開封第一講書人閱讀 39,345評論 0 276
  • 序言:老撾萬榮一對情侶失蹤泞边,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后疗杉,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體阵谚,經...
    沈念sama閱讀 45,802評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,984評論 3 337
  • 正文 我和宋清朗相戀三年烟具,在試婚紗的時候發(fā)現(xiàn)自己被綠了梢什。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,117評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡朝聋,死狀恐怖嗡午,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情冀痕,我是刑警寧澤荔睹,帶...
    沈念sama閱讀 35,810評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站言蛇,受9級特大地震影響僻他,放射性物質發(fā)生泄漏。R本人自食惡果不足惜腊尚,卻給世界環(huán)境...
    茶點故事閱讀 41,462評論 3 331
  • 文/蒙蒙 一吨拗、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦劝篷、人聲如沸哨鸭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽兔跌。三九已至,卻和暖如春峡蟋,著一層夾襖步出監(jiān)牢的瞬間坟桅,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評論 1 272
  • 我被黑心中介騙來泰國打工蕊蝗, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留仅乓,地道東北人。 一個月前我還...
    沈念sama閱讀 48,377評論 3 373
  • 正文 我出身青樓蓬戚,卻偏偏與公主長得像夸楣,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子子漩,可洞房花燭夜當晚...
    茶點故事閱讀 45,060評論 2 355

推薦閱讀更多精彩內容