深度揭秘Netty中的FastThreadLocal為什么比ThreadLocal效率更高?

file

閱讀這篇文章之前,建議先閱讀和這篇文章關(guān)聯(lián)的內(nèi)容。

1. 詳細(xì)剖析分布式微服務(wù)架構(gòu)下網(wǎng)絡(luò)通信的底層實(shí)現(xiàn)原理(圖解)

2. (年薪60W的技巧)工作了5年,你真的理解Netty以及為什么要用嗎?(深度干貨)

3. 深度解析Netty中的核心組件(圖解+實(shí)例)

4. BAT面試必問細(xì)節(jié):關(guān)于Netty中的ByteBuf詳解

5. 通過大量實(shí)戰(zhàn)案例分解Netty中是如何解決拆包黏包問題的?

6. 基于Netty實(shí)現(xiàn)自定義消息通信協(xié)議(協(xié)議設(shè)計(jì)及解析應(yīng)用實(shí)戰(zhàn))

7. 全網(wǎng)最詳細(xì)最齊全的序列化技術(shù)及深度解析與應(yīng)用實(shí)戰(zhàn)

8. 手把手教你基于Netty實(shí)現(xiàn)一個(gè)基礎(chǔ)的RPC框架(通俗易懂)

9. (年薪60W分水嶺)基于Netty手寫實(shí)現(xiàn)RPC框架進(jìn)階篇(帶注冊中心和注解)

FastThreadLocal的實(shí)現(xiàn)與J.U.C包中的ThreadLocal非常類似。

了解過ThreadLocal原理的同學(xué)應(yīng)該都清楚,它有幾個(gè)關(guān)鍵的對象.

  1. Thread
  2. ThreadLocalMap
  3. ThreadLocal

同樣,Netty專門為FastThreadLocal量身打造了FastThreadLocalThreadInternalThreadLocalMap兩個(gè)重要的類。下面我們看下這兩個(gè)類是如何實(shí)現(xiàn)的。

PS,如果不懂ThreadLocal的朋友,可以看我這篇文章:ThreadLocal的使用及原理分析

FastThreadLocalThread是對Thread類的一層包裝,每個(gè)線程對應(yīng)一個(gè)InternalThreadLocalMap實(shí)例。只有FastThreadLocalFastThreadLocalThread組合使用時(shí),才能發(fā)揮 FastThreadLocal的性能優(yōu)勢。首先看下FastThreadLocalThread的源碼定義:

public class FastThreadLocalThread extends Thread {

    private InternalThreadLocalMap threadLocalMap;
    // 省略其他代碼
}

可以看出 FastThreadLocalThread 主要擴(kuò)展了 InternalThreadLocalMap 字段,我們可以猜測到 FastThreadLocalThread 主要使用 InternalThreadLocalMap 存儲(chǔ)數(shù)據(jù),而不再是使用 Thread 中的 ThreadLocalMap。所以想知道 FastThreadLocalThread 高性能的奧秘,必須要了解 InternalThreadLocalMap 的設(shè)計(jì)原理。

InternalThreadLocalMap

public final class InternalThreadLocalMap extends UnpaddedInternalThreadLocalMap {

    private static final int DEFAULT_ARRAY_LIST_INITIAL_CAPACITY = 8;

    private static final int STRING_BUILDER_INITIAL_SIZE;

    private static final int STRING_BUILDER_MAX_SIZE;

    public static final Object UNSET = new Object();

    private BitSet cleanerFlags;
    private InternalThreadLocalMap() {
        indexedVariables = newIndexedVariableTable();
    }
    private static Object[] newIndexedVariableTable() {
        Object[] array = new Object[INDEXED_VARIABLE_TABLE_INITIAL_SIZE];
        Arrays.fill(array, UNSET);
        return array;
    }
    public static int lastVariableIndex() {
        return nextIndex.get() - 1;
    }

    public static int nextVariableIndex() {
        int index = nextIndex.getAndIncrement();
        if (index < 0) {
            nextIndex.decrementAndGet();
            throw new IllegalStateException("too many thread-local indexed variables");
        }
        return index;
    }
    // 省略

}

從 InternalThreadLocalMap 內(nèi)部實(shí)現(xiàn)來看,與 ThreadLocalMap 一樣都是采用數(shù)組的存儲(chǔ)方式。

了解ThreadLocal的同學(xué)都知道,它內(nèi)部也是采用數(shù)組的方式來實(shí)現(xiàn)hash表,對于hash沖突,采用了線性探索的方式來實(shí)現(xiàn)。

但是 InternalThreadLocalMap 并沒有使用線性探測法來解決 Hash 沖突,而是在 FastThreadLocal 初始化的時(shí)候分配一個(gè)數(shù)組索引 index,index 的值采用原子類 AtomicInteger 保證順序遞增,通過調(diào)用 InternalThreadLocalMap.nextVariableIndex() 方法獲得。然后在讀寫數(shù)據(jù)的時(shí)候通過數(shù)組下標(biāo) index 直接定位到 FastThreadLocal 的位置,時(shí)間復(fù)雜度為 O(1)。如果數(shù)組下標(biāo)遞增到非常大,那么數(shù)組也會(huì)比較大,所以 FastThreadLocal 是通過空間換時(shí)間的思想提升讀寫性能。

下面通過一幅圖描述 InternalThreadLocalMap、index 和 FastThreadLocal 之間的關(guān)系。

image-20211123112056607

通過上面 FastThreadLocal 的內(nèi)部結(jié)構(gòu)圖,我們對比下與 ThreadLocal 有哪些區(qū)別呢?

FastThreadLocal 使用 Object 數(shù)組替代了 Entry 數(shù)組,Object[0] 存儲(chǔ)的是一個(gè)Set<FastThreadLocal<?>> 集合。

從數(shù)組下標(biāo) 1 開始都是直接存儲(chǔ)的 value 數(shù)據(jù),不再采用 ThreadLocal 的鍵值對形式進(jìn)行存儲(chǔ)。

假設(shè)現(xiàn)在我們有一批數(shù)據(jù)需要添加到數(shù)組中,分別為 value1、value2、value3、value4,對應(yīng)的 FastThreadLocal 在初始化的時(shí)候生成的數(shù)組索引分別為 1、2、3、4。如下圖所示。

image-20211123112505405

至此,我們已經(jīng)對 FastThreadLocal 有了一個(gè)基本的認(rèn)識(shí),下面我們結(jié)合具體的源碼分析 FastThreadLocal 的實(shí)現(xiàn)原理。

FastThreadLocal的set方法源碼分析

在講解源碼之前,我們回過頭看下上文中的 ThreadLocal 示例,如果把示例中 ThreadLocal 替換成 FastThread,應(yīng)當(dāng)如何使用呢?

public class FastThreadLocalTest {

    private static final FastThreadLocal<String> THREAD_NAME_LOCAL = new FastThreadLocal<>();
    private static final FastThreadLocal<TradeOrder> TRADE_THREAD_LOCAL = new FastThreadLocal<>();
    public static void main(String[] args) {
        for (int i = 0; i < 2; i++) {
            int tradeId = i;
            String threadName = "thread-" + i;
            new FastThreadLocalThread(() -> {
                THREAD_NAME_LOCAL.set(threadName);
                TradeOrder tradeOrder = new TradeOrder(tradeId, tradeId % 2 == 0 ? "已支付" : "未支付");
                TRADE_THREAD_LOCAL.set(tradeOrder);
                System.out.println("threadName: " + THREAD_NAME_LOCAL.get());
                System.out.println("tradeOrder info:" + TRADE_THREAD_LOCAL.get());
            }, threadName).start();

        }
    }
}

可以看出,F(xiàn)astThreadLocal 的使用方法幾乎和 ThreadLocal 保持一致,只需要把代碼中 Thread、ThreadLocal 替換為 FastThreadLocalThread 和 FastThreadLocal 即可,Netty 在易用性方面做得相當(dāng)棒。下面我們重點(diǎn)對示例中用得到 FastThreadLocal.set()/get() 方法做深入分析。

首先看下 FastThreadLocal.set() 的源碼:

public final void set(V value) {
    if (value != InternalThreadLocalMap.UNSET) {
        InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();
        setKnownNotUnset(threadLocalMap, value);
    } else {
        remove();
    }
}

FastThreadLocal.set() 方法實(shí)現(xiàn)并不難理解,先抓住代碼主干,一步步進(jìn)行拆解分析。set() 的過程主要分為三步:

  1. 判斷 value 是否為缺省值,如果等于缺省值,那么直接調(diào)用 remove() 方法。這里我們還不知道缺省值和 remove() 之間的聯(lián)系是什么,我們暫且把 remove() 放在最后分析。
  2. 如果 value 不等于缺省值,接下來會(huì)獲取當(dāng)前線程的 InternalThreadLocalMap。
  3. 然后將 InternalThreadLocalMap 中對應(yīng)數(shù)據(jù)替換為新的 value。

InternalThreadLocalMap.get()

先來看InternalThreadLocalMap.get()方法:

public static InternalThreadLocalMap get() {
    Thread thread = Thread.currentThread();
    if (thread instanceof FastThreadLocalThread) {
        return fastGet((FastThreadLocalThread) thread);
    } else {
        return slowGet();
    }
}

如果thread實(shí)例類型是FastThreadLocalThread,則調(diào)用fastGet()。

InternalThreadLocalMap.get() 邏輯很簡單.

  1. 如果當(dāng)前線程是 FastThreadLocalThread 類型,那么直接通過 fastGet() 方法獲取 FastThreadLocalThread 的 threadLocalMap 屬性即可
  2. 如果此時(shí) InternalThreadLocalMap 不存在,直接創(chuàng)建一個(gè)返回。

關(guān)于 InternalThreadLocalMap 的初始化在上文中已經(jīng)介紹過,它會(huì)初始化一個(gè)長度為 32 的 Object 數(shù)組,數(shù)組中填充著 32 個(gè)缺省對象 UNSET 的引用。

private static InternalThreadLocalMap fastGet(FastThreadLocalThread thread) {
  InternalThreadLocalMap threadLocalMap = thread.threadLocalMap();
  if (threadLocalMap == null) {
    thread.setThreadLocalMap(threadLocalMap = new InternalThreadLocalMap());
  }
  return threadLocalMap;
}

否則,則調(diào)用slowGet(),從代碼實(shí)現(xiàn)來看,slowGet() 是針對非 FastThreadLocalThread 類型的線程發(fā)起調(diào)用時(shí)的一種兜底方案。如果當(dāng)前線程不是 FastThreadLocalThread,內(nèi)部是沒有 InternalThreadLocalMap 屬性的,Netty 在 UnpaddedInternalThreadLocalMap 中保存了一個(gè) JDK 原生的 ThreadLocal,ThreadLocal 中存放著 InternalThreadLocalMap,此時(shí)獲取 InternalThreadLocalMap 就退化成 JDK 原生的 ThreadLocal 獲取。

private static InternalThreadLocalMap slowGet() {
  InternalThreadLocalMap ret = slowThreadLocalMap.get();
  if (ret == null) {
    ret = new InternalThreadLocalMap();
    slowThreadLocalMap.set(ret);
  }
  return ret;
}

setKnownNotUnset

獲取 InternalThreadLocalMap 的過程已經(jīng)講完了,下面看下 setKnownNotUnset() 如何將數(shù)據(jù)添加到 InternalThreadLocalMap 的。

private void setKnownNotUnset(InternalThreadLocalMap threadLocalMap, V value) {
    if (threadLocalMap.setIndexedVariable(index, value)) {
        addToVariablesToRemove(threadLocalMap, this);
    }
}

setKnownNotUnset() 主要做了兩件事:

  1. 找到數(shù)組下標(biāo) index 位置,設(shè)置新的 value。
  2. 將 FastThreadLocal 對象保存到待清理的 Set 中。

首先我們看下第一步 threadLocalMap.setIndexedVariable() 的源碼實(shí)現(xiàn):

public boolean setIndexedVariable(int index, Object value) {
    Object[] lookup = indexedVariables;
    if (index < lookup.length) {
        Object oldValue = lookup[index];
        lookup[index] = value;
        return oldValue == UNSET;
    } else {
        expandIndexedVariableTableAndSet(index, value);
        return true;
    }
}

indexedVariables 就是 InternalThreadLocalMap 中用于存放數(shù)據(jù)的數(shù)組,如果數(shù)組容量大于 FastThreadLocal 的 index 索引,那么直接找到數(shù)組下標(biāo) index 位置將新 value 設(shè)置進(jìn)去,事件復(fù)雜度為 O(1)。在設(shè)置新的 value 之前,會(huì)將之前 index 位置的元素取出,如果舊的元素還是 UNSET 缺省對象,那么返回成功。

如果數(shù)組容量不夠了怎么辦呢?InternalThreadLocalMap 會(huì)自動(dòng)擴(kuò)容,然后再設(shè)置 value。接下來看看 expandIndexedVariableTableAndSet() 的擴(kuò)容邏輯:

private void expandIndexedVariableTableAndSet(int index, Object value) {
    Object[] oldArray = indexedVariables;
    final int oldCapacity = oldArray.length;
    int newCapacity = index;
    newCapacity |= newCapacity >>>  1;
    newCapacity |= newCapacity >>>  2;
    newCapacity |= newCapacity >>>  4;
    newCapacity |= newCapacity >>>  8;
    newCapacity |= newCapacity >>> 16;
    newCapacity ++;

    Object[] newArray = Arrays.copyOf(oldArray, newCapacity);
    Arrays.fill(newArray, oldCapacity, newArray.length, UNSET);
    newArray[index] = value;
    indexedVariables = newArray;
}

可以看出 InternalThreadLocalMap 實(shí)現(xiàn)數(shù)組擴(kuò)容幾乎和 HashMap 完全是一模一樣的,所以多讀源碼還是可以給我們很多啟發(fā)的。InternalThreadLocalMap 以 index 為基準(zhǔn)進(jìn)行擴(kuò)容,將數(shù)組擴(kuò)容后的容量向上取整為 2 的次冪。然后將原數(shù)組內(nèi)容拷貝到新的數(shù)組中,空余部分填充缺省對象 UNSET,最終把新數(shù)組賦值給 indexedVariables。

思考關(guān)于基準(zhǔn)擴(kuò)容

思考:為什么 InternalThreadLocalMap 以 index 為基準(zhǔn)進(jìn)行擴(kuò)容,而不是原數(shù)組長度呢?

假設(shè)現(xiàn)在初始化了 70 個(gè) FastThreadLocal,但是這些 FastThreadLocal 從來沒有調(diào)用過 set() 方法,此時(shí)數(shù)組還是默認(rèn)長度 32。當(dāng)?shù)?index = 70 的 FastThreadLocal 調(diào)用 set() 方法時(shí),如果按原數(shù)組容量 32 進(jìn)行擴(kuò)容 2 倍后,還是無法填充 index = 70 的數(shù)據(jù)。所以使用 index 為基準(zhǔn)進(jìn)行擴(kuò)容可以解決這個(gè)問題,但是如果 FastThreadLocal 特別多,數(shù)組的長度也是非常大的。

回到 setKnownNotUnset() 的主流程,向 InternalThreadLocalMap 添加完數(shù)據(jù)之后,接下就是將 FastThreadLocal 對象保存到待清理的 Set 中。我們繼續(xù)看下 addToVariablesToRemove() 是如何實(shí)現(xiàn)的:

addToVariablesToRemove

private static void addToVariablesToRemove(InternalThreadLocalMap threadLocalMap, FastThreadLocal<?> variable) {
    Object v = threadLocalMap.indexedVariable(variablesToRemoveIndex);
    Set<FastThreadLocal<?>> variablesToRemove;
    if (v == InternalThreadLocalMap.UNSET || v == null) {
        variablesToRemove = Collections.newSetFromMap(new IdentityHashMap<FastThreadLocal<?>, Boolean>());
        threadLocalMap.setIndexedVariable(variablesToRemoveIndex, variablesToRemove);
    } else {
        variablesToRemove = (Set<FastThreadLocal<?>>) v;
    }

    variablesToRemove.add(variable);
}

variablesToRemoveIndex 是采用 static final 修飾的變量,在 FastThreadLocal 初始化時(shí) variablesToRemoveIndex 被賦值為 0。InternalThreadLocalMap 首先會(huì)找到數(shù)組下標(biāo)為 0 的元素.

  1. 如果該元素是缺省對象 UNSET 或者不存在,那么會(huì)創(chuàng)建一個(gè) FastThreadLocal 類型的 Set 集合,然后把 Set 集合填充到數(shù)組下標(biāo) 0 的位置。
  2. 如果數(shù)組第一個(gè)元素不是缺省對象 UNSET,說明 Set 集合已經(jīng)被填充,直接強(qiáng)轉(zhuǎn)獲得 Set 集合即可。這就解釋了 InternalThreadLocalMap 的 value 數(shù)據(jù)為什么是從下標(biāo)為 1 的位置開始存儲(chǔ)了,因?yàn)?0 的位置已經(jīng)被 Set 集合占用了。

思考關(guān)于Set集合設(shè)計(jì)

思考:為什么 InternalThreadLocalMap 要在數(shù)組下標(biāo)為 0 的位置存放一個(gè) FastThreadLocal 類型的 Set 集合呢?這時(shí)候我們回過頭看下 remove() 方法。

public final void remove(InternalThreadLocalMap threadLocalMap) {
  if (threadLocalMap == null) {
    return;
  }

  Object v = threadLocalMap.removeIndexedVariable(index);
  removeFromVariablesToRemove(threadLocalMap, this);

  if (v != InternalThreadLocalMap.UNSET) {
    try {
      onRemoval((V) v);
    } catch (Exception e) {
      PlatformDependent.throwException(e);
    }
  }
}

在執(zhí)行 remove 操作之前,會(huì)調(diào)用 InternalThreadLocalMap.getIfSet() 獲取當(dāng)前 InternalThreadLocalMap。

有了之前的基礎(chǔ),理解 getIfSet() 方法就非常簡單了。

  1. 如果是 FastThreadLocalThread 類型,直接取 FastThreadLocalThread 中 threadLocalMap 屬性。
  2. 如果是普通線程 Thread,從 ThreadLocal 類型的 slowThreadLocalMap 中獲取。

找到 InternalThreadLocalMap 之后,InternalThreadLocalMap 會(huì)從數(shù)組中定位到下標(biāo) index 位置的元素,并將 index 位置的元素覆蓋為缺省對象 UNSET。

接下來就需要清理當(dāng)前的 FastThreadLocal 對象,此時(shí) Set 集合就派上了用場,InternalThreadLocalMap 會(huì)取出數(shù)組下標(biāo) 0 位置的 Set 集合,然后刪除當(dāng)前 FastThreadLocal。最后 onRemoval() 方法起到什么作用呢?Netty 只是留了一處擴(kuò)展,并沒有實(shí)現(xiàn),用戶需要在刪除的時(shí)候做一些后置操作,可以繼承 FastThreadLocal 實(shí)現(xiàn)該方法。

FastThreadLocal.get()源碼分析

再來看一下 FastThreadLocal.get() 的源碼:

public final V get() {
    InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();
    Object v = threadLocalMap.indexedVariable(index);
    if (v != InternalThreadLocalMap.UNSET) {
        return (V) v;
    }

    return initialize(threadLocalMap);
}

首先根據(jù)當(dāng)前線程是否是 FastThreadLocalThread 類型找到 InternalThreadLocalMap,然后取出從數(shù)組下標(biāo) index 的元素,如果 index 位置的元素不是缺省對象 UNSET,說明該位置已經(jīng)填充過數(shù)據(jù),直接取出返回即可。

public Object indexedVariable(int index) {
  Object[] lookup = indexedVariables;
  return index < lookup.length? lookup[index] : UNSET;
}

如果 index 位置的元素是缺省對象 UNSET,那么需要執(zhí)行初始化操作。可以看到,initialize() 方法會(huì)調(diào)用用戶重寫的 initialValue 方法構(gòu)造需要存儲(chǔ)的對象數(shù)據(jù).

private V initialize(InternalThreadLocalMap threadLocalMap) {
    V v = null;
    try {
        v = initialValue();
    } catch (Exception e) {
        PlatformDependent.throwException(e);
    }

    threadLocalMap.setIndexedVariable(index, v);
    addToVariablesToRemove(threadLocalMap, this);
    return v;
}

initialValue方法的構(gòu)造方式如下。

private final FastThreadLocal<String> threadLocal = new FastThreadLocal<String>() {
  @Override
  protected String initialValue() {
    return "hello world";
  }
};

構(gòu)造完用戶對象數(shù)據(jù)之后,接下來就會(huì)將它填充到數(shù)組 index 的位置,然后再把當(dāng)前 FastThreadLocal 對象保存到待清理的 Set 中。整個(gè)過程我們在分析 FastThreadLocal.set() 時(shí)都已經(jīng)介紹過,就不再贅述了。

到此為止,F(xiàn)astThreadLocal 最核心的兩個(gè)方法 set()/get() 我們已經(jīng)分析完了。下面有兩個(gè)問題我們再深入思考下。

  1. FastThreadLocal 真的一定比 ThreadLocal 快嗎?答案是不一定的,只有使用FastThreadLocalThread 類型的線程才會(huì)更快,如果是普通線程反而會(huì)更慢。
  2. FastThreadLocal 會(huì)浪費(fèi)很大的空間嗎?雖然 FastThreadLocal 采用的空間換時(shí)間的思路,但是在 FastThreadLocal 設(shè)計(jì)之初就認(rèn)為不會(huì)存在特別多的 FastThreadLocal 對象,而且在數(shù)據(jù)中沒有使用的元素只是存放了同一個(gè)缺省對象的引用,并不會(huì)占用太多內(nèi)存空間。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,333評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,491評論 3 416
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,263評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,946評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,708評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,409評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,939評論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,774評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,976評論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,641評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,872評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,650評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,958評論 2 373

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