【多線程與并發(fā)】Java內(nèi)存模型(JMM)

通過上篇文章【多線程與并發(fā)】Java并發(fā)理論基礎(chǔ)我們了解到:Java內(nèi)存模型是一種虛擬機規(guī)范,用于屏蔽掉各種硬件和操作系統(tǒng)的內(nèi)存訪問差異茄唐。并從兩個維度去理解了它享钞。那么本篇文章就近距離的來認識下Java內(nèi)存模型岖寞。

從堆棧說起

JVM內(nèi)部使用的Java內(nèi)存模型在線程棧和堆之間劃分內(nèi)存恋拍。 此圖從邏輯角度說明了Java內(nèi)存模型:
java-memory-model-1.png
  • 線程棧內(nèi)包含正在執(zhí)行的每個方法的所有局部變量嘀粱,線程的訪問是私有的,每個線程只能訪問自己棧豆同。
  • 當(dāng)局部變量是基本類型(boolean,byte含鳞,short影锈,char,int蝉绷,long鸭廷,float,double)時熔吗,它完全保留在線程棧上辆床。
  • 當(dāng)局部變量是對象的引用時,引用(局部變量)存儲在線程棧中磁滚,對象本身存儲在堆上佛吓。
  • 對象的成員變量與對象本身一起存儲在堆(Heap)上,不論成員變量是基本數(shù)據(jù)類型還是對象引用類型垂攘。
  • 靜態(tài)成員變量跟隨著類定義一起也存放在堆上维雇。
  • 存放在堆上的對象可以被所有持有對這個對象引用的線程訪問。
    • 當(dāng)一個線程可以訪問一個對象時晒他,它也可以訪問該對象的成員變量吱型。
    • 如果兩個線程同時調(diào)用同一個對象上的同一個方法,那它們都可以訪問該對象的成員變量陨仅,但是每一個線程都擁有這個成員變量的副本津滞。


      java-memory-model-3.png

      此圖能夠直觀地反映上述關(guān)于堆棧的幾點說明。

硬件內(nèi)存結(jié)構(gòu)

現(xiàn)代硬件內(nèi)存架構(gòu)與內(nèi)部Java內(nèi)存模型略有不同灼伤。

先來看一張現(xiàn)代計算機硬件架構(gòu)圖:
WechatIMG195.jpg

通過上圖我們來對硬件內(nèi)存結(jié)構(gòu)進行詳細說明:

  • CPU:
    • 現(xiàn)代計算機通常是有2個或多個CPU触徐。
    • 其中一些CPU還可能是多核。在這種現(xiàn)代計算機上狐赡,可以同時運行多個線程撞鹉。
    • 每個CPU都能夠在任何給定時間運行一個線程。這就意味著,如果Java應(yīng)用程序是多線程的鸟雏,線程是可能同時運行享郊。
  • 寄存器:
    • 每個CPU基本上都包含一組在CPU內(nèi)存中的寄存器。
    • CPU訪問這些寄存器要比訪問主存儲器快的多孝鹊。
  • 高速緩存存儲器:
    • 每個CPU還可以具有CPU高速緩存存儲器層炊琉。
    • 由于計算機內(nèi)存與CPU之間的運算速度有著巨大差距,現(xiàn)代計算機為了解決這一問題又活,在內(nèi)存和CPU之間加入了一層讀寫速度盡可能接近CPU運算速度的高速緩存來最為緩沖:將運算需要使用到的數(shù)據(jù)復(fù)制到緩存中苔咪,讓運算能快速進行。當(dāng)運算結(jié)束后再從緩存同步回內(nèi)存之中柳骄,這樣處理器就無需等待緩慢地內(nèi)存讀寫了悼泌。
    • CPU訪問速度上:CPU內(nèi)部寄存器 > 高速緩存存儲器 > 內(nèi)存
    • CPU可能有一個或者多個緩存層(級別1和級別2)(要了解Java內(nèi)存模型如何與內(nèi)存交互,這一點并不重要夹界,重要的是要知道CPU可以有某種緩存存儲層)馆里。
  • 內(nèi)存:
    • 計算機還包含主存儲區(qū)(RAM),所有CPU都可以訪問主內(nèi)存可柿。
    • 主存儲區(qū)通常比CPU的高速緩存存儲器大得多鸠踪,同時訪問速度也就較慢。

通常复斥,當(dāng)CPU需要訪問主存儲器時营密,它會將部分主存儲器讀入其CPU緩存。甚至可以將部分緩存讀入其內(nèi)部寄存器目锭,然后對其執(zhí)行操作评汰。當(dāng)CPU需要講結(jié)果寫回主存儲器時,他會將值從其內(nèi)部寄存器刷新到高速緩沖存儲器痢虹,并在某個時間點將值刷入主存儲器被去。

硬件內(nèi)存結(jié)構(gòu)&CPU普及導(dǎo)致的問題

緩存一致性問題

如上所述,Java內(nèi)存模型和硬件內(nèi)存架構(gòu)是不同的奖唯,硬件內(nèi)存架構(gòu)不區(qū)分線程棧和堆惨缆。在硬件上,線程棧和堆都位于主存儲器中丰捷。線程棧和堆的一部分有時可能存在于CPU高速緩存和內(nèi)部寄存器中坯墨。如下圖:


java-memory-model-5.png

當(dāng)對象和變量可以存儲在計算機的各種不同的存儲區(qū)域中時,可能會出現(xiàn)某些問題(統(tǒng)稱為:緩存一致性問題)病往,兩個主要問題:

  • 線程更新(寫入)到共享變量的可見性

  • 讀寫共享變量時的競態(tài)條件

對象共享后的可見性:

如果兩個或多個線程共享一個對象捣染,而沒有正確使用volatile聲明或同步,則一個線程對共享對象的更新可能對其他線程不可見停巷。

舉例說明:共享對象最初存儲在主存儲器中耍攘。 然后累贤,在CPU上運行的線程將共享對象讀入其CPU緩存中。 它在那里對共享對象進行了更改少漆。 只要CPU緩存尚未刷新回主內(nèi)存示损,共享對象的更改版本對于在其他CPU上運行的線程是不可見的脆贵。 這樣负懦,每個線程最終都可能擁有自己的共享對象副本,每個副本都位于不同的CPU緩存中肯尺。

競態(tài)條件:

如果兩個或多個線程共享一個對象逾滥,并且多個線程更新該共享對象中的變量掀亩,則可能會出現(xiàn)競態(tài)。

舉例說明:如果線程A和線程B將共享變量count讀入到各自CPU緩存中(不同的CPU緩存),而后捉超,線程A將count加1惜纸,線程B也做了同樣的事椭更。count被增加了兩次虑瀑,每個CPU緩存中一次湿滓,如果這些增加操作被順序的執(zhí)行,則變量count應(yīng)該增加兩次并將原始值 +2寫會主存儲器舌狗。但是叽奥,兩次增加都是在沒有適當(dāng)?shù)耐较虏l(fā)執(zhí)行的。無論是線程A還是線程B將count修改后的版本寫回到主存中取痛侍,修改后的值僅會被原值大1朝氓。

CPU 為了解決內(nèi)存緩存不一致性問題可以通過制定緩存一致協(xié)議(比如 MESI 協(xié)議open in new window)或者其他手段來解決。 這個緩存一致性協(xié)議指的是在 CPU 高速緩存與主內(nèi)存交互的時候需要遵守的原則和規(guī)范主届。不同的 CPU 中赵哲,使用的緩存一致性協(xié)議通常也會有所不同。

WechatIMG196.jpg
我們的程序運行在操作系統(tǒng)之上君丁,操作系統(tǒng)屏蔽了底層硬件的操作細節(jié)枫夺,將各種硬件資源虛擬化。于是绘闷,操作系統(tǒng)也就同樣需要解決內(nèi)存緩存不一致性問題橡庞。

操作系統(tǒng)通過 內(nèi)存模型(Memory Model) 定義一系列規(guī)范來解決這個問題较坛。無論是 Windows 系統(tǒng),還是 Linux 系統(tǒng)扒最,它們都有特定的內(nèi)存模型丑勤。

指令重排

除了緩存一致性問題,還存在另外一種硬件問題吧趣,也比較重要:為了使 CPU 內(nèi)部的運算單元能夠盡量被充分利用法竞,處理器可能會對輸入的字節(jié)碼指令進行重排序處理,也就是處理器優(yōu)化再菊。除了 CPU 之外,很多編程語言的編譯器也會有類似的優(yōu)化颜曾,比如 Java虛擬機的即時編譯器(JIT)也會做指令重排纠拔。在上一篇文章【多線程與并發(fā)】Java并發(fā)理論基礎(chǔ) #有序性:重排序引起 這節(jié)中對指令重排有詳細說明,可自行查閱泛豪,這里就不在贅述稠诲。

Java內(nèi)存模型(JMM)

什么是Java內(nèi)存模型

Java 是最早嘗試提供內(nèi)存模型的編程語言。由于早期內(nèi)存模型存在一些缺陷(比如非常容易削弱編譯器的優(yōu)化能力)诡曙,從 Java5 開始臀叙,Java 開始使用新的內(nèi)存模型 《JSR-133:Java Memory Model and Thread Specification》open in new window

對于 Java 來說价卤,你可以把 JMM 看作是 Java 定義的并發(fā)編程相關(guān)的一組規(guī)范劝萤,除了抽象了線程和主內(nèi)存之間的關(guān)系之外,其還規(guī)定了從 Java 源代碼到 CPU 可執(zhí)行指令的這個轉(zhuǎn)化過程要遵守哪些和并發(fā)相關(guān)的原則和規(guī)范慎璧,其主要目的是為了簡化多線程編程床嫌,增強程序可移植性的。

為什么需要Java內(nèi)存模型

  • 一般來說胸私,編程語言也可以直接復(fù)用操作系統(tǒng)層面的內(nèi)存模型厌处。不過,不同的操作系統(tǒng)內(nèi)存模型不同岁疼。如果直接復(fù)用操作系統(tǒng)層面的內(nèi)存模型阔涉,就可能會導(dǎo)致同樣一套代碼換了一個操作系統(tǒng)就無法執(zhí)行了。Java 語言是跨平臺的捷绒,它需要自己提供一套內(nèi)存模型以屏蔽系統(tǒng)差異瑰排。

  • 在并發(fā)編程下,像 CPU 多級緩存和指令重排這類設(shè)計可能會導(dǎo)致程序運行出現(xiàn)一些問題暖侨。比如上面所提到的凶伙,為此,JMM 抽象了 happens-before 原則(在上一篇文章【多線程與并發(fā)】Java并發(fā)理論基礎(chǔ) 有詳細說明它碎,可自行查閱函荣,本篇文章會稍作補充說明)來解決這個指令重排序問題显押。

  • Java內(nèi)存模型通俗來說就是定義了一些規(guī)范來解決上面這些問題,開發(fā)者可以利用這些規(guī)范更方便地開發(fā)多線程程序傻挂。對于 Java 開發(fā)者說乘碑,你不需要了解底層原理,直接使用并發(fā)相關(guān)的一些關(guān)鍵字和類(比如 volatile金拒、synchronized兽肤、各種 Lock)即可開發(fā)出并發(fā)安全的程序。

Java內(nèi)存模型的抽象

JMM抽象了線程和主內(nèi)存之間的關(guān)系:

  • 線程之間的共享變量存儲在內(nèi)存(Main Memory)中绪抛。

  • 每個線程都有一個私有的本地內(nèi)存(Local Memory)资铡,本地內(nèi)存是JMM的一個抽象概念,并不真實存在幢码,它涵蓋了緩存笤休、寫緩沖區(qū)、寄存器以及其他的硬件和編譯優(yōu)化症副。本地內(nèi)存中存儲了該線程以讀/寫共享變量的副本店雅。

  • 從更低的層次來說,主內(nèi)存就是硬件的內(nèi)存贞铣,為了獲取更好的運行速度闹啦,虛擬機及硬件系統(tǒng)可能會讓工作內(nèi)存優(yōu)先存儲于寄存器和高速緩存中。

Java 內(nèi)存模型的抽象示意圖如下:


java-jmm-1.png

從上圖來看辕坝,線程 A 與線程 B 之間如要通信的話窍奋,必須要經(jīng)歷下面 2 個步驟:

  • 首先,線程 A 把本地內(nèi)存 A 中更新過的共享變量刷新到主內(nèi)存中去酱畅。
  • 然后费变,線程 B 到主內(nèi)存中去讀取線程 A 之前已更新過的共享變量。

下面通過示意圖來說明這兩個步驟:


java-jmm-2.png

關(guān)于主內(nèi)存與工作內(nèi)存之間的具體交互協(xié)議圣贸,即一個變量如何從主內(nèi)存拷貝到工作內(nèi)存挚歧、如何從工作內(nèi)存同步到主內(nèi)存之間的實現(xiàn)細節(jié),Java內(nèi)存模型定義了以下八種操作來完成:

  • lock(鎖定): 作用于主內(nèi)存中的變量吁峻,將他標(biāo)記為一個線程獨享變量滑负。

  • unlock(解鎖): 作用于主內(nèi)存中的變量,解除變量的鎖定狀態(tài)用含,被解除鎖定狀態(tài)的變量才能被其他線程鎖定矮慕。

  • read(讀取):作用于主內(nèi)存的變量啄骇,它把一個變量的值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存中痴鳄,以便隨后的 load 動作使用。

  • load(載入):把 read 操作從主內(nèi)存中得到的變量值放入工作內(nèi)存的變量的副本中缸夹。

  • use(使用):把工作內(nèi)存中的一個變量的值傳給執(zhí)行引擎痪寻,每當(dāng)虛擬機遇到一個使用到變量的指令時都會使用該指令螺句。

  • assign(賦值):作用于工作內(nèi)存的變量,它把一個從執(zhí)行引擎接收到的值賦給工作內(nèi)存的變量橡类,每當(dāng)虛擬機遇到一個給變量賦值的字節(jié)碼指令時執(zhí)行這個操作蛇尚。

  • store(存儲):作用于工作內(nèi)存的變量,它把工作內(nèi)存中一個變量的值傳送到主內(nèi)存中顾画,以便隨后的 write 操作使用取劫。

  • write(寫入):作用于主內(nèi)存的變量,它把 store 操作從工作內(nèi)存中得到的變量的值放入主內(nèi)存的變量中研侣。

Java內(nèi)存模型還規(guī)定了在執(zhí)行上述八種基本操作時谱邪,必須滿足如下規(guī)則:

  • 如果要把一個變量從主內(nèi)存中復(fù)制到工作內(nèi)存,就需要按順尋地執(zhí)行read和load操作庶诡, 如果把變量從工作內(nèi)存中同步回主內(nèi)存中惦银,就要按順序地執(zhí)行store和write操作。但Java內(nèi)存模型只要求上述操作必須按順序執(zhí)行灌砖,而沒有保證必須是連續(xù)執(zhí)行璧函。

  • 不允許read和load傀蚌、store和write操作之一單獨出現(xiàn)

  • 不允許一個線程丟棄它的最近assign的操作基显,即變量在工作內(nèi)存中改變了之后必須同步到主內(nèi)存中。

  • 不允許一個線程無原因地(沒有發(fā)生過任何assign操作)把數(shù)據(jù)從工作內(nèi)存同步回主內(nèi)存中善炫。

  • 一個新的變量只能在主內(nèi)存中誕生撩幽,不允許在工作內(nèi)存中直接使用一個未被初始化(load或assign)的變量。即就是對一個變量實施use和store操作之前箩艺,必須先執(zhí)行過了assign和load操作窜醉。

  • 一個變量在同一時刻只允許一條線程對其進行l(wèi)ock操作,但lock操作可以被同一條線程重復(fù)執(zhí)行多次艺谆,多次執(zhí)行l(wèi)ock后榨惰,只有執(zhí)行相同次數(shù)的unlock操作,變量才會被解鎖静汤。lock和unlock必須成對出現(xiàn)

  • 如果對一個變量執(zhí)行l(wèi)ock操作琅催,將會清空工作內(nèi)存中此變量的值,在執(zhí)行引擎使用這個變量前需要重新執(zhí)行l(wèi)oad或assign操作初始化變量的值

  • 如果一個變量事先沒有被lock操作鎖定虫给,則不允許對它執(zhí)行unlock操作藤抡;也不允許去unlock一個被其他線程鎖定的變量。

  • 對一個變量執(zhí)行unlock操作之前抹估,必須先把此變量同步到主內(nèi)存中(執(zhí)行store和write操作)缠黍。

happens-before原則

happens-before 設(shè)計思想

happens-before 這個概念最早誕生于 Leslie Lamport 于 1978 年發(fā)表的論文《Time,Clocks and the Ordering of Events in a Distributed System》open in new window药蜻。在這篇論文中瓷式,Leslie Lamport 提出了邏輯時鐘open in new window的概念替饿,這也成了第一個邏輯時鐘算法 。在分布式環(huán)境中蒿往,通過一系列規(guī)則來定義邏輯時鐘的變化盛垦,從而能通過邏輯時鐘來對分布式系統(tǒng)中的事件的先后順序進行判斷。邏輯時鐘并不度量時間本身瓤漏,僅區(qū)分事件發(fā)生的前后順序腾夯,其本質(zhì)就是定義了一種 happens-before 關(guān)系。

JSR 133 引入了 happens-before 這個概念來描述兩個操作之間的內(nèi)存可見性蔬充。

為什么需要 happens-before 原則蝶俱? happens-before 原則的誕生是為了程序員和編譯器、處理器之間的平衡饥漫。程序員追求的是易于理解和編程的強內(nèi)存模型榨呆,遵守既定規(guī)則編碼即可。編譯器和處理器追求的是較少約束的弱內(nèi)存模型庸队,讓它們盡己所能地去優(yōu)化性能积蜻,讓性能最大化。happens-before 原則的設(shè)計思想其實非常簡單:

  • 為了對編譯器和處理器的約束盡可能少彻消,只要不改變程序的執(zhí)行結(jié)果(單線程程序和正確執(zhí)行的多線程程序)竿拆,編譯器和處理器怎么進行重排序優(yōu)化都行。

  • 對于會改變程序執(zhí)行結(jié)果的重排序宾尚,JMM 要求編譯器和處理器必須禁止這種重排序丙笋。

JMM 設(shè)計思想的示意圖:


image-20220731155332375.png

了解了 happens-before 原則的設(shè)計思想,我們再來看看 JSR-133 對 happens-before 原則的定義:

  • 如果一個操作 happens-before 另一個操作煌贴,那么第一個操作的執(zhí)行結(jié)果將對第二個操作可見御板,并且第一個操作的執(zhí)行順序排在第二個操作之前。

  • 兩個操作之間存在 happens-before 關(guān)系牛郑,并不意味著 Java 平臺的具體實現(xiàn)必須要按照 happens-before 關(guān)系指定的順序來執(zhí)行怠肋。如果重排序之后的執(zhí)行結(jié)果與按 happens-before 關(guān)系來執(zhí)行的結(jié)果一致,那么 JMM 也允許這樣的重排序淹朋,否則不允許重排序笙各。

舉例說明:計算圓面積。

double pi  = 3.14;    //A
double r   = 1.0;     //B
double area = pi * r * r; //C

上面計算圓的面積的示例代碼存在三個 happens- before 關(guān)系:

  1. A happens- before B

  2. B happens- before C

  3. A happens- before C

雖然 A happens-before B瑞你,但對 A 和 B 進行重排序不會影響代碼的執(zhí)行結(jié)果酪惭,所以 JMM 是允許編譯器和處理器執(zhí)行這種重排序的。但 A 和 B 必須是在 C 執(zhí)行之前者甲,也就是說 A和B happens-before C 春感。

happens-before 原則表達的意義其實并不是一個操作發(fā)生在另外一個操作的前面,雖然這從程序員的角度上來說也并無大礙嫩实。更準確地來說,它更想表達的意義是前一個操作的結(jié)果對于后一個操作是可見的甲献,無論這兩個操作是否在同一個線程中。

happens-before 規(guī)則

關(guān)于happens-before規(guī)則可在【多線程與并發(fā)】Java并發(fā)理論基礎(chǔ) 中自行查閱颂翼。

happens-before 與 JMM 的關(guān)系

happens-before 與 JMM 的關(guān)系如下圖所示:


java-jmm-5.png

如上圖所示晃洒,一個 happens-before 規(guī)則通常對應(yīng)于多個編譯器重排序規(guī)則和處理器重排序規(guī)則朦乏。對于 java 程序員來說,happens-before 規(guī)則簡單易懂呻疹,它避免程序員為了理解 JMM 提供的內(nèi)存可見性保證而去學(xué)習(xí)復(fù)雜的重排序規(guī)則以及這些規(guī)則的具體實現(xiàn)吃引。

再看并發(fā)編程三要素

可自行查閱上篇文章【多線程與并發(fā)】Java并發(fā)理論基礎(chǔ) #第二個理解維度:可見性,有序性刽锤,原子性 這一節(jié)镊尺,會不會有種豁然開朗的感覺呢并思?

參考:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末纺荧,一起剝皮案震驚了整個濱河市旭愧,隨后出現(xiàn)的幾起案子宙暇,更是在濱河造成了極大的恐慌议泵,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,539評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件型奥,死亡現(xiàn)場離奇詭異,居然都是意外死亡厢汹,警方通過查閱死者的電腦和手機谐宙,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評論 3 396
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人垢箕,你說我怎么就攤上這事兑巾。” “怎么了蒋歌?”我有些...
    開封第一講書人閱讀 165,871評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長锄开。 經(jīng)常有香客問我,道長称诗,這世上最難降的妖魔是什么萍悴? 我笑而不...
    開封第一講書人閱讀 58,963評論 1 295
  • 正文 為了忘掉前任癣诱,我火速辦了婚禮,結(jié)果婚禮上撕予,老公的妹妹穿的比我還像新娘蜈首。我一直安慰自己,他們只是感情好欢策,可當(dāng)我...
    茶點故事閱讀 67,984評論 6 393
  • 文/花漫 我一把揭開白布吆寨。 她就那樣靜靜地躺著踩寇,像睡著了一般。 火紅的嫁衣襯著肌膚如雪俺孙。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,763評論 1 307
  • 那天荣茫,我揣著相機與錄音场靴,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛叉趣,可吹牛的內(nèi)容都是我干的该押。 我是一名探鬼主播,決...
    沈念sama閱讀 40,468評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼烟具,長吁一口氣:“原來是場噩夢啊……” “哼奠蹬!你這毒婦竟也來了朝聋?” 一聲冷哼從身側(cè)響起囤躁,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤狸演,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后宵距,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,850評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡婿斥,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,002評論 3 338
  • 正文 我和宋清朗相戀三年哨鸭,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片勘高。...
    茶點故事閱讀 40,144評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡坟桅,死狀恐怖蕊蝗,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情蓬戚,我是刑警寧澤,帶...
    沈念sama閱讀 35,823評論 5 346
  • 正文 年R本政府宣布豫喧,位于F島的核電站,受9級特大地震影響紧显,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜涉兽,卻給世界環(huán)境...
    茶點故事閱讀 41,483評論 3 331
  • 文/蒙蒙 一篙程、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧虱饿,春花似錦、人聲如沸袋倔。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽扇售。三九已至,卻和暖如春华弓,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背寂屏。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評論 1 272
  • 我被黑心中介騙來泰國打工娜搂, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人考廉。 一個月前我還...
    沈念sama閱讀 48,415評論 3 373
  • 正文 我出身青樓携御,卻偏偏與公主長得像既绕,于是被迫代替她去往敵國和親涮坐。 傳聞我的和親對象是個殘疾皇子凄贩,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,092評論 2 355