ThreadLocal詳解

???????ThreadLocal在java.lang包中捎泻,其主要作用是提供一個和線程綁定的變量環(huán)境檩禾,即通過ThreadLocal在一個線程中存儲了一個變量之后刹勃,再在另一個線程中使用同一個ThreadLocal對象設(shè)置值柄错,第二個線程內(nèi)設(shè)置的值不會將第一個線程內(nèi)設(shè)置的值覆蓋,并且在同一個線程中可以獲取之前設(shè)置的值催蝗。如下是一個ThreadLocal的使用示例:

public class ThreadLocalExample {
  public static void main(String[] args) {
    ThreadLocal<String> threadLocal = new MyThreadLocal<>();
    Runnable task1 = () -> {
      threadLocal.set("task1");
      sleep(2);
      threadLocal.get();
    };

    Runnable task2 = () -> {
      sleep(1);
      threadLocal.set("task2");
      sleep(2);
      threadLocal.get();
    };

    Thread thread1 = new Thread(task1);
    Thread thread2 = new Thread(task2);
    thread1.start();
    thread2.start();
  }

  private static void sleep(int seconds) {
    try {
      TimeUnit.SECONDS.sleep(seconds);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }

  private static final class MyThreadLocal<T> extends ThreadLocal<T> {
    @Override
    public T get() {
      T result = super.get();
      System.out.println(Thread.currentThread().getName() + " invoke get method, result is " + result);
      return result;
    }

    @Override
    public void set(T value) {
      System.out.println(Thread.currentThread().getName() + " invoke set method, value is " + value);
      super.set(value);
    }
  }
}

???????如下是上述代碼的執(zhí)行結(jié)果:

Thread-0 invoke set method, value is task1
Thread-1 invoke set method, value is task2
Thread-0 invoke get method, result is task1
Thread-1 invoke get method, result is task2

???????可以看到切威,Thread-0首先往ThreadLocal中設(shè)置了一個值,接著Thread-1也設(shè)置了一個值丙号,但是Thread-1并沒有將Thread-0設(shè)置的值覆蓋先朦,因為接下來Thread-0從ThreadLocal中獲取的值還是其先前設(shè)置的值且预,并且Thread-1獲取的值也是其先前設(shè)置的值。

???????在項目中烙无,借助于ThreadLocal我們可以編寫出一些非常優(yōu)雅的代碼,并且實現(xiàn)一些類似于緩存的功能遍尺,比如如下util類:

public class ParamUtil {
  private static final ThreadLocal<Map<String, Object>> params = new ThreadLocal<>();

  public static void clear() {
    params.remove();
  }

  public static void setParam(String key, Object obj) {
    Map<String, Object> paramsMap = params.get();
    if (null == paramsMap) {
      paramsMap = new HashMap<>();
      params.set(paramsMap);
    }
    paramsMap.put(key, obj);
  }
  
  public static <T> T getParam(String key) {
    Map<String, Object> paramMap = params.get();
    if (paramMap == null) {
      return null;
    }

    @SuppressWarnings("unchecked") T result = (T) paramMap.get(key);
    return result;
  }
}

???????在ParamUtil中截酷,我們聲明了一個ThreadLocal類型的變量params,其存儲的是一個Map<String, Object>類型的數(shù)據(jù)乾戏,也就是我們實際存儲的數(shù)據(jù)是放在這個map中的迂苛,這里的Map使用HashMap即可,因為ThreadLocal針對每個線程都是保存有其存儲的變量的一個副本鼓择,因而針對每個線程其都有一個Map對象三幻,也就不存在并發(fā)的問題,如下是該util類的使用示例:

@Service
public class MlsOrgServiceImpl implements MlsOrgService {
  @Autowired
  private MlsOrgDao mlsOrgDao;
  
  @Override
  public MlsOrgInfo getMlsOrg(Long id) {
    MlsOrgInfo mlsOrgInfo = ParamUtil.getParam("mlsOrgInfo");
    if (null == mlsOrgInfo) {
      mlsOrgInfo = mlsOrgDao.getByMlsOrgId(id);
      ParamUtil.setParam("mlsOrgInfo", mlsOrgInfo);
    }
    
    return mlsOrgInfo;
  }
}

???????這里在service方法中直接從ParamUtil獲取緩存的數(shù)據(jù)呐能,如果存在念搬,則直接返回,如果不存在摆出,則從dao從查詢朗徊,并且將其設(shè)置到ParamUtil中。這里需要說明的是偎漫,通過這種方式進行緩存有三個優(yōu)點:

  • 緩存實效性較好爷恳。因為用戶的一次請求的時間非常短,因而該緩存只會在這一次請求中有效象踊,實時更改數(shù)據(jù)庫中的數(shù)據(jù)對后續(xù)的請求都是生效的温亲;
  • 重復(fù)調(diào)用時效果明顯。當(dāng)需要緩存的信息在請求中需要經(jīng)常用到的時候該緩存的效果將非常明顯杯矩;
  • 可以跨多層調(diào)用栈虚。在Java web項目中,相較于將數(shù)據(jù)緩存在request中菊碟,ThreadLocal可以跨多個層(controller节芥,service等)進行緩存,而request一般只用于controller層中逆害;

???????這里需要說明一點是头镊,使用ThreadLocal進行緩存的時候,由于其是和線程綁定的魄幕,而服務(wù)端框架中相艇,不會每次請求都新建一個線程進行處理,而是有空余線程時則復(fù)用該線程處理新的請求纯陨,因而這種緩存方式需要在每次請求開始時(比如Java web中的攔截器)對ThreadLocal中存儲的數(shù)據(jù)進行清理坛芽,這樣可以避免當(dāng)前請求中獲取到了之前某次請求中緩存的數(shù)據(jù)留储。

???????通過上述示例可以看出,ThreadLocal主要有兩個方法:get和set方法咙轩。get方法用于獲取其存儲的值获讳,set方法則將數(shù)據(jù)存儲在ThreadLocal中。ThreadLocal在底層維護了一個Map對象活喊,其鍵是一個ThreadLocal對象丐膝,而值則為該ThreadLocal對象中存儲的值,在調(diào)用ThreadLocal的get和set方法的時候?qū)嶋H上底層調(diào)用的是該map對象的對應(yīng)方法钾菊。并且ThreadLocal實現(xiàn)將數(shù)據(jù)與線程綁定的方式則主要是將這個Map對象實例保存在每個Thread對象中帅矗,如下所示Thread類中的Map對象的聲明:

ThreadLocal.ThreadLocalMap threadLocals = null;

???????首先我們看看ThreadLocal的set方法實現(xiàn),如下是其代碼:

public void set(T value) {
  Thread t = Thread.currentThread();
  ThreadLocalMap map = getMap(t);
  if (map != null)
    map.set(this, value);
  else
    createMap(t, value);
}

???????可以看到煞烫,每次調(diào)用set方法時浑此,其都會首先獲取當(dāng)前執(zhí)行該set方法的線程,然后獲取該線程中保存的ThreadLocalMap實例滞详,如果該map不為空凛俱,則將當(dāng)前ThreadLocal和其值設(shè)置到該map中,否則創(chuàng)建一個ThreadLocalMap實例茵宪,然后將當(dāng)前設(shè)置的值初始化到該map中最冰,并且還會將該實例設(shè)置到當(dāng)前線程的ThreadLocalMap實例中。如下是getMap(Thread)和createMap(Thread, T)方法的實現(xiàn):

ThreadLocalMap getMap(Thread t) {
  return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
  t.threadLocals = new ThreadLocalMap(this, firstValue);
}

???????在講解ThreadLocalMap.set(ThreadLocal, T)方法之前稀火,我們首先看看ThreadLocalMap的數(shù)據(jù)結(jié)構(gòu)及其存儲方式:

static class ThreadLocalMap {
  static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
      super(k);
      value = v;
    }
  }
  
  private static final int INITIAL_CAPACITY = 16;
  private Entry[] table;
  private int size = 0;
  private int threshold;
  
  private void setThreshold(int len) {
    threshold = len * 2 / 3;
  }

  private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
  }

  private static int prevIndex(int i, int len) {
    return ((i - 1 >= 0) ? i - 1 : len - 1);
  }
}

???????這里列出了ThreadLocalMap的主要屬性以及其余比較簡單的方法暖哨。可以看出凰狞,ThreadLocalMap底層和HashMap類似篇裁,是使用一個Entry類型的數(shù)組來存儲鍵值對的,但是不同的是其Entry是繼承自WeakReference的赡若,值為Object類型的一個屬性达布。WeakReference的作用是其可以存儲一個引用,如果在其他位置(比如某個全局或局部變量)沒有存儲相同的引用逾冬,那么Java垃圾回收機制則會對該引用對象進行回收黍聂。這里使用WeakReference的用意則在于線程是可消逝的,那么當(dāng)其消逝之后和其綁定的ThreadLocal對象如果沒有引用到則可以被垃圾回收機制回收身腻〔梗回過頭來,ThreadLocalMap中的另外幾個屬性的意義如下:INITIAL_CAPACITY表示Entry數(shù)組的默認初始化長度嘀趟,size存儲了當(dāng)前鍵值對的數(shù)量脐区,threshold存儲了當(dāng)前Entry中最多存儲的鍵值對數(shù)目,超過該數(shù)目時就會對當(dāng)前ThreadLocalMap進行rehash()操作她按,通過setThreshold(int)方法可以看出牛隅,當(dāng)前Map的默認負載銀子是2/3炕柔。

???????在ThreadLocalMap中,其Entry只有兩個屬性:鍵和值媒佣。相較于HashMap匕累,其在當(dāng)前Entry中保存有一個Entry類型的next指針,即使用一個單向鏈表的方式來解決hash沖突默伍。ThreadLocalMap的Entry由于只有兩個屬性哩罪,因而其解決沖突的方式不是使用單向鏈表的方式,并且由于每個Entry對象也是一個WeakReference實例巡验,這也導(dǎo)致其不能使用單項鏈表的方式解決沖突。這里ThreadLocalMap使用的是線性探測再散列法解決hash沖突的碘耳,即當(dāng)一個鍵映射到某個槽位之后显设,其會先檢查該槽位是否存儲有值,如果有值則檢查數(shù)組的下一位是否有值辛辨,如此查找直到找到一個沒有存儲數(shù)據(jù)的槽位將當(dāng)前鍵值對存儲其中捕捂。這里實際探測時,如果發(fā)生沖突斗搞,其還會檢查當(dāng)前槽位的Entry的鍵是否為null指攒,因為其可能被垃圾回收機制給回收,如果為空則將當(dāng)前鍵值對存儲于該槽位中僻焚,并且還會對從該槽位到后續(xù)第一個為空的槽位的沒有被回收的鍵值對進行再散列允悦,并且清除已經(jīng)被回收的鍵值對,這樣做的目的有助于減少hash表中鍵值對的數(shù)量虑啤,減少發(fā)生沖突的概率和rehash的次數(shù)隙弛。

???????下面我們通過ThreadLocalMap.set()方法具體看看其是如何存儲鍵值對的。如下是set()方法的具體實現(xiàn):

private void set(ThreadLocal<?> key, Object value) {
  Entry[] tab = table;
  int len = tab.length;
  int i = key.threadLocalHashCode & (len-1);    // 對數(shù)組長度取余計算將存儲的槽位

  // 從計算得到的槽位開始依次往后遍歷狞山,直到找到對應(yīng)的鍵或者是遇到了null槽位
  for (Entry e = tab[i];
       e != null;
       e = tab[i = nextIndex(i, len)]) {
    ThreadLocal<?> k = e.get();

    // 如果k == key說明之前存在該鍵及其值全闷,那么直接替換其值為新的值即可
    if (k == key) {
      e.value = value;
      return;
    }

    // 如果k為空,說明當(dāng)前entry的鍵已經(jīng)被垃圾回收機制回收了萍启,那么將要設(shè)置的鍵值對替換當(dāng)前鍵值對
    // 并且對其后沒有被回收的鍵值對進行再散列
    if (k == null) {
      replaceStaleEntry(key, value, i);
      return;
    }
  }

  // 在for循環(huán)中沒有找到當(dāng)前key對應(yīng)的值总珠,說明之前沒有設(shè)置相同鍵的鍵值對
  // 此時i指向的是沖突槽位之后第一個為空的槽位,那么在該槽位上新建一個entry存儲當(dāng)前鍵值對
  tab[i] = new Entry(key, value);
  int sz = ++size;
  
  // cleanSomeSlots的作用是選擇性的對一些槽位進行檢測勘纯,如果其已經(jīng)被回收局服,則對其進行清理
  if (!cleanSomeSlots(i, sz) && sz >= threshold)
    rehash();
}

???????總結(jié)ThreadLocalMap.set()方法,其首先會計算當(dāng)前ThreadLocal對象將要存儲的位置屡律,然后使用一個for循環(huán)從該位置處往后依次遍歷腌逢,檢查每一個鍵值對是否已經(jīng)被回收,被回收了則將當(dāng)前的鍵值對存儲于該位置超埋,或者是判斷當(dāng)前鍵是否就是要存儲的鍵搏讶,相等則將當(dāng)前當(dāng)前鍵對應(yīng)的值替換為新的值佳鳖。這里我們看看replaceStaleEntry()方法的具體實現(xiàn):

private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                               int staleSlot) {
  Entry[] tab = table;
  int len = tab.length;
  Entry e;

  int slotToExpunge = staleSlot;
  // 從被回收的槽位開始往前遍歷查找第一個已經(jīng)被回收的鍵值對
  for (int i = prevIndex(staleSlot, len);
       (e = tab[i]) != null;
       i = prevIndex(i, len))
    if (e.get() == null)
      slotToExpunge = i;

  // 從被回收的槽位開始向后遍歷,查找是否有鍵為當(dāng)前要設(shè)置的鍵媒惕,有則將其與被回收的槽位進行替換
  // 并且對后續(xù)第一個被回收的槽位開始的后續(xù)元素進行再散列系吩,因為第一個被回收的槽位將重置為空
  for (int i = nextIndex(staleSlot, len);
       (e = tab[i]) != null;
       i = nextIndex(i, len)) {
    ThreadLocal<?> k = e.get();

    // 在被回收的槽位之后找到與要設(shè)置的鍵相同的鍵,那么將其值替換為新值妒蔚,并且與被回收的槽位替換位置
    if (k == key) {
      // 更新其值為新值
      e.value = value;

      // 替換槽位
      tab[i] = tab[staleSlot];
      tab[staleSlot] = e;

      // slotToExpunge記錄的是被回收的槽位(staleSlot)之后第一個被回收的槽位
      if (slotToExpunge == staleSlot)
        slotToExpunge = i;
      // 對被回收的槽位之后第一個被回收的槽位之后的元素進行再散列穿挨,
      // 因為該第一個被回收的槽位將被重置為空
      cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
      return;
    }

    // 記錄被回收的槽位之后第一個被回收的槽位
    if (k == null && slotToExpunge == staleSlot)
      slotToExpunge = i;
  }

  // 如果在被回收的槽位之后沒有找到與要設(shè)置的鍵相同的鍵,那么直接新建一個entry替換被回收的槽位
  tab[staleSlot].value = null;
  tab[staleSlot] = new Entry(key, value);

  // 如果slotToExpunge != staleSlot則說明被回收的槽位之后有被回收的鍵值對肴盏,那么就從該槽位開始
  // 對后續(xù)元素進行回收或者再散列
  if (slotToExpunge != staleSlot)
    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

???????總結(jié)來說科盛,replaceStaleEntry()方法主要是將要設(shè)置的鍵值對替換為當(dāng)前已經(jīng)被回收的鍵值對,并且其會在當(dāng)前被回收的鍵值對之后找到第一個被回收的鍵值對菜皂,找到了則將其重置為空贞绵,并且對其后的數(shù)據(jù)進行再散列。這里再散列的工作是通過expungeStaleEntry()方法實現(xiàn)的恍飘,我們可以看看該方法是如何實現(xiàn)的:

private int expungeStaleEntry(int staleSlot) {
  Entry[] tab = table;
  int len = tab.length;

  // 對已經(jīng)被回收的鍵值對進行重置
  tab[staleSlot].value = null;
  tab[staleSlot] = null;
  size--;

  Entry e;
  int i;
  // 循環(huán)查找后續(xù)被回收的鍵值對
  for (i = nextIndex(staleSlot, len);
       (e = tab[i]) != null;
       i = nextIndex(i, len)) {
    ThreadLocal<?> k = e.get();
    if (k == null) {    // 為空說明已經(jīng)被回收榨崩,則對其進行重置
      e.value = null;
      tab[i] = null;
      size--;
    } else {    // 不為空則對其進行再散列
      int h = k.threadLocalHashCode & (len - 1);
      if (h != i) {
        tab[i] = null;

        while (tab[h] != null)  // 查找目標槽位之后第一個可以存儲數(shù)據(jù)的槽位
          h = nextIndex(h, len);
        tab[h] = e;
      }
    }
  }
  return i;
}

???????以上是ThreadLocalMap.set()方法的具體實現(xiàn),通過上面的說明可以看出章母,其會經(jīng)常檢查槽位中的數(shù)據(jù)是否已經(jīng)被回收母蛛,這樣做的目的是減少當(dāng)前map中entry的數(shù)量,減少get和set方法需要檢查的entry的數(shù)量乳怎,并且也可以避免rehash操作的數(shù)量彩郊。這里也說明了線性探測再散列法的一個缺點,即其將數(shù)據(jù)直接存儲在數(shù)組上會大大增加沖突發(fā)生的數(shù)量蚪缀,因而需要經(jīng)常對槽位進行清理焦辅。至于為什么會增加沖突發(fā)生的數(shù)量,這也很好理解椿胯,比如對于key1筷登,其計算的槽位是3,但是由于3哩盲,4和5號槽位都因為沖突存儲了數(shù)據(jù)前方,那么其只能存儲在6號槽位上。此時另一個key2計算的槽位是4廉油,其會發(fā)現(xiàn)4號槽位已經(jīng)存儲了數(shù)據(jù)开仰,因而只能存儲在7號槽位上乃沙』耍可以發(fā)現(xiàn)夕冲,不同hash值的數(shù)據(jù)因為這種存儲方式而扎堆的存儲在了一個局部沖突塊中。

???????前面我們對ThreadLocalMap.set()方法實現(xiàn)方式進行了講解嘶炭,下面我們來看看ThreadLocalMap.get()方法抱慌,其實現(xiàn)思路和set()方法非常類似逊桦,即確認要查找的key對應(yīng)的槽位之后查找是否有key和當(dāng)前key相同,相同則返回抑进,否則一直查找直到遇到null槽位為止强经。期間其還會對找到的已經(jīng)被回收的槽位進行處理。如下是ThreadLocalMap.get()方法的具體代碼:

private Entry getEntry(ThreadLocal<?> key) {
  int i = key.threadLocalHashCode & (table.length - 1);
  Entry e = table[i];
  if (e != null && e.get() == key)
    return e;
  else
    return getEntryAfterMiss(key, i, e);
}

???????可以看出寺渗,其會對計算得到的槽位進行判斷匿情,如果其為要查找的key,則直接返回信殊,否則會從該槽位開始往后查找炬称,如下是getEntryAfterMiss()方法的代碼:

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
  Entry[] tab = table;
  int len = tab.length;

  while (e != null) {
    ThreadLocal<?> k = e.get();
    if (k == key)
      return e;
    if (k == null)
      expungeStaleEntry(i);
    else
      i = nextIndex(i, len);
    e = tab[i];
  }
  return null;
}

????????在從目標槽位開始往后查找的過程中,其會判斷當(dāng)前key是否為要查找的key涡拘,如果是則返回转砖,不是則會判斷該key是否為空,如果為空則對其進行回收鲸伴,并且對其后的鍵值對進行再散列。最后晋控,如果沒有找到目標key汞窗,則返回空。

????????最后需要說明的一點是赡译,上述ThreadLocalMap中的鍵值對為ThreadLocal對象及其存儲的值仲吏,而在每個Thread對象中都是有一個獨立的ThreadLocalMap對象的,這里講到的Map中的沖突解決等指的都是在同一個線程中創(chuàng)建了多個ThreadLocal對象時發(fā)生的蝌焚,即在同一個線程中裹唆,其會把該線程中使用的所有ThreadLocal對象都存儲到同一個ThreadLocalMap對象中。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末只洒,一起剝皮案震驚了整個濱河市许帐,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌毕谴,老刑警劉巖成畦,帶你破解...
    沈念sama閱讀 210,914評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異涝开,居然都是意外死亡循帐,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評論 2 383
  • 文/潘曉璐 我一進店門舀武,熙熙樓的掌柜王于貴愁眉苦臉地迎上來拄养,“玉大人,你說我怎么就攤上這事银舱”衲洌” “怎么了跛梗?”我有些...
    開封第一講書人閱讀 156,531評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長柿顶。 經(jīng)常有香客問我茄袖,道長,這世上最難降的妖魔是什么嘁锯? 我笑而不...
    開封第一講書人閱讀 56,309評論 1 282
  • 正文 為了忘掉前任宪祥,我火速辦了婚禮,結(jié)果婚禮上家乘,老公的妹妹穿的比我還像新娘蝗羊。我一直安慰自己,他們只是感情好仁锯,可當(dāng)我...
    茶點故事閱讀 65,381評論 5 384
  • 文/花漫 我一把揭開白布耀找。 她就那樣靜靜地躺著,像睡著了一般业崖。 火紅的嫁衣襯著肌膚如雪野芒。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,730評論 1 289
  • 那天双炕,我揣著相機與錄音狞悲,去河邊找鬼。 笑死妇斤,一個胖子當(dāng)著我的面吹牛摇锋,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播站超,決...
    沈念sama閱讀 38,882評論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼荸恕,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了死相?” 一聲冷哼從身側(cè)響起融求,我...
    開封第一講書人閱讀 37,643評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎算撮,沒想到半個月后双肤,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,095評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡钮惠,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,448評論 2 325
  • 正文 我和宋清朗相戀三年茅糜,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片素挽。...
    茶點故事閱讀 38,566評論 1 339
  • 序言:一個原本活蹦亂跳的男人離奇死亡蔑赘,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情缩赛,我是刑警寧澤耙箍,帶...
    沈念sama閱讀 34,253評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站酥馍,受9級特大地震影響辩昆,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜旨袒,卻給世界環(huán)境...
    茶點故事閱讀 39,829評論 3 312
  • 文/蒙蒙 一汁针、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧砚尽,春花似錦施无、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至敷搪,卻和暖如春兴想,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背赡勘。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評論 1 264
  • 我被黑心中介騙來泰國打工嫂便, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人狮含。 一個月前我還...
    沈念sama閱讀 46,248評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像曼振,于是被迫代替她去往敵國和親几迄。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,440評論 2 348

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