【轉】Java8系列之重新認識HashMap

摘要

HashMap是Java程序員使用頻率最高的用于映射(鍵值對)處理的數(shù)據(jù)類型冬耿。隨著JDK(Java Developmet Kit)版本的更新舌菜,JDK1.8對HashMap底層的實現(xiàn)進行了優(yōu)化,例如引入紅黑樹的數(shù)據(jù)結構和擴容的優(yōu)化等亦镶。本文結合JDK1.7和JDK1.8的區(qū)別日月,深入探討HashMap的結構實現(xiàn)和功能原理。

簡介

Java為數(shù)據(jù)結構中的映射定義了一個接口java.util.Map缤骨,此接口主要有四個常用的實現(xiàn)類爱咬,分別是HashMap、Hashtable荷憋、LinkedHashMap和TreeMap台颠,類繼承關系如下圖所示:

下面針對各個實現(xiàn)類的特點做一些說明:

  • (1) HashMap:它根據(jù)鍵的hashCode值存儲數(shù)據(jù)褐望,大多數(shù)情況下可以直接定位到它的值勒庄,因而具有很快的訪問速度,但遍歷順序卻是不確定的瘫里。 HashMap最多只允許一條記錄的鍵為null实蔽,允許多條記錄的值為null。HashMap非線程安全谨读,即任一時刻可以有多個線程同時寫HashMap局装,可能會導致數(shù)據(jù)的不一致。如果需要滿足線程安全劳殖,可以用 Collections的synchronizedMap方法使HashMap具有線程安全的能力铐尚,或者使用ConcurrentHashMap。

  • (2) Hashtable:Hashtable是遺留類哆姻,很多映射的常用功能與HashMap類似宣增,不同的是它承自Dictionary類,并且是線程安全的矛缨,任一時間只有一個線程能寫Hashtable爹脾,并發(fā)性不如ConcurrentHashMap帖旨,因為ConcurrentHashMap引入了分段鎖。Hashtable不建議在新代碼中使用灵妨,不需要線程安全的場合可以用HashMap替換解阅,需要線程安全的場合可以用ConcurrentHashMap替換。

  • (3) LinkedHashMap:LinkedHashMap是HashMap的一個子類泌霍,保存了記錄的插入順序货抄,在用Iterator遍歷LinkedHashMap時,先得到的記錄肯定是先插入的朱转,也可以在構造時帶參數(shù)碉熄,按照訪問次序排序。

  • (4) TreeMap:TreeMap實現(xiàn)SortedMap接口肋拔,能夠把它保存的記錄根據(jù)鍵排序锈津,默認是按鍵值的升序排序,也可以指定排序的比較器凉蜂,當用Iterator遍歷TreeMap時琼梆,得到的記錄是排過序的。如果使用排序的映射窿吩,建議使用TreeMap茎杂。在使用TreeMap時,key必須實現(xiàn)Comparable接口或者在構造TreeMap傳入自定義的Comparator纫雁,否則會在運行時拋出java.lang.ClassCastException類型的異常煌往。

對于上述四種Map類型的類,要求映射中的key是不可變對象轧邪。不可變對象是該對象在創(chuàng)建后它的哈希值不會被改變刽脖。如果對象的哈希值發(fā)生變化,Map對象很可能就定位不到映射的位置了忌愚。

通過上面的比較曲管,我們知道了HashMap是Java的Map家族中一個普通成員,鑒于它可以滿足大多數(shù)場景的使用條件硕糊,所以是使用頻度最高的一個院水。下文我們主要結合源碼,從存儲結構简十、常用方法分析檬某、擴容以及安全性等方面深入講解HashMap的工作原理。

內部實現(xiàn)

搞清楚HashMap螟蝙,首先需要知道HashMap是什么恢恼,即它的存儲結構-字段;其次弄明白它能干什么胶逢,即它的功能實現(xiàn)-方法厅瞎。下面我們針對這兩個方面詳細展開講解饰潜。

存儲結構-字段

從結構實現(xiàn)來講,HashMap是數(shù)組+鏈表+紅黑樹(JDK1.8增加了紅黑樹部分)實現(xiàn)的和簸,如下如所示彭雾。

這里需要講明白兩個問題:數(shù)據(jù)底層具體存儲的是什么?這樣的存儲方式有什么優(yōu)點呢锁保?

(1) 從源碼可知薯酝,HashMap類中有一個非常重要的字段,就是 Node[] table爽柒,即哈希桶數(shù)組吴菠,明顯它是一個Node的數(shù)組。我們來看Node[JDK1.8]是何物浩村。

Node是HashMap的一個內部類做葵,實現(xiàn)了Map.Entry接口,本質是就是一個映射(鍵值對)心墅。上圖中的每個黑色圓點就是一個Node對象酿矢。

(2) HashMap就是使用哈希表來存儲的。哈希表為解決沖突怎燥,可以采用開放地址法和鏈地址法等來解決問題瘫筐,Java中HashMap采用了鏈地址法。鏈地址法铐姚,簡單來說策肝,就是數(shù)組加鏈表的結合。在每個數(shù)組元素上都一個鏈表結構隐绵,當數(shù)據(jù)被Hash后之众,得到數(shù)組下標,把數(shù)據(jù)放在對應下標元素的鏈表上氢橙。例如程序執(zhí)行下面代碼:

map.put("美團", "小美")

系統(tǒng)將調用"美團"這個key的hashCode()方法得到其hashCode 值(該方法適用于每個Java對象)酝枢,然后再通過Hash算法的后兩步運算(高位運算和取模運算,下文有介紹)來定位該鍵值對的存儲位置悍手,有時兩個key會定位到相同的位置,表示發(fā)生了Hash碰撞袍患。當然Hash算法計算結果越分散均勻坦康,Hash碰撞的概率就越小,map的存取效率就會越高诡延。

如果哈希桶數(shù)組很大滞欠,即使較差的Hash算法也會比較分散,如果哈希桶數(shù)組數(shù)組很小肆良,即使好的Hash算法也會出現(xiàn)較多碰撞筛璧,所以就需要在空間成本和時間成本之間權衡逸绎,其實就是在根據(jù)實際情況確定哈希桶數(shù)組的大小,并在此基礎上設計好的hash算法減少Hash碰撞夭谤。那么通過什么方式來控制map使得Hash碰撞的概率又小棺牧,哈希桶數(shù)組(Node[] table)占用空間又少呢?答案就是好的Hash算法和擴容機制朗儒。

在理解Hash和擴容流程之前颊乘,我們得先了解下HashMap的幾個字段。從HashMap的默認構造函數(shù)源碼可知醉锄,構造函數(shù)就是對下面幾個字段進行初始化乏悄,源碼如下:

首先,Node[] table的初始化長度length(默認值是16)恳不,Load factor為負載因子(默認值是0.75)檩小,threshold是HashMap所能容納的最大數(shù)據(jù)量的Node(鍵值對)個數(shù)。threshold = length * Load factor烟勋。也就是說识啦,在數(shù)組定義好長度之后,負載因子越大神妹,所能容納的鍵值對個數(shù)越多颓哮。

結合負載因子的定義公式可知,threshold就是在此Load factor和length(數(shù)組長度)對應下允許的最大元素數(shù)目鸵荠,超過這個數(shù)目就重新resize(擴容)冕茅,擴容后的HashMap容量是之前容量的兩倍。默認的負載因子0.75是對空間和時間效率的一個平衡選擇蛹找,建議大家不要修改姨伤,除非在時間和空間比較特殊的情況下,如果內存空間很多而又對時間效率要求很高庸疾,可以降低負載因子Load factor的值乍楚;相反,如果內存空間緊張而對時間效率要求不高届慈,可以增加負載因子loadFactor的值徒溪,這個值可以大于1。

size這個字段其實很好理解金顿,就是HashMap中實際存在的鍵值對數(shù)量臊泌。注意和table的長度length、容納最大鍵值對數(shù)量threshold的區(qū)別揍拆。而modCount字段主要用來記錄HashMap內部結構發(fā)生變化的次數(shù)渠概,主要用于迭代的快速失敗。強調一點嫂拴,內部結構發(fā)生變化指的是結構發(fā)生變化播揪,例如put新鍵值對贮喧,但是某個key對應的value值被覆蓋不屬于結構變化。

在HashMap中猪狈,哈希桶數(shù)組table的長度length大小必須為2的n次方(一定是合數(shù))箱沦,這是一種非常規(guī)的設計,常規(guī)的設計是把桶的大小設計為素數(shù)罪裹。相對來說素數(shù)導致沖突的概率要小于合數(shù)饱普,具體證明可以參考http://blog.csdn.net/liuqiyao_01/article/details/14475159,Hashtable初始化桶大小為11状共,就是桶大小設計為素數(shù)的應用(Hashtable擴容后不能保證還是素數(shù))套耕。HashMap采用這種非常規(guī)設計,主要是為了在取模和擴容時做優(yōu)化峡继,同時為了減少沖突冯袍,HashMap定位哈希桶索引位置時,也加入了高位參與運算的過程碾牌。

這里存在一個問題康愤,即使負載因子和Hash算法設計的再合理,也免不了會出現(xiàn)拉鏈過長的情況舶吗,一旦出現(xiàn)拉鏈過長征冷,則會嚴重影響HashMap的性能。于是誓琼,在JDK1.8版本中检激,對數(shù)據(jù)結構做了進一步的優(yōu)化,引入了紅黑樹腹侣。而當鏈表長度太長(默認超過8)時叔收,鏈表就轉換為紅黑樹,利用紅黑樹快速增刪改查的特點提高HashMap的性能傲隶,其中會用到紅黑樹的插入饺律、刪除、查找等算法跺株。本文不再對紅黑樹展開討論复濒,想了解更多紅黑樹數(shù)據(jù)結構的工作原理可以參考http://blog.csdn.net/v_july_v/article/details/6105630

功能實現(xiàn)-方法

HashMap的內部功能實現(xiàn)很多帖鸦,本文主要從根據(jù)key獲取哈希桶數(shù)組索引位置芝薇、put方法的詳細執(zhí)行、擴容過程三個具有代表性的點深入展開講解作儿。

1. 確定哈希桶數(shù)組索引位置

不管增加、刪除馋劈、查找鍵值對攻锰,定位到哈希桶數(shù)組的位置都是很關鍵的第一步晾嘶。前面說過HashMap的數(shù)據(jù)結構是數(shù)組和鏈表的結合,所以我們當然希望這個HashMap里面的元素位置盡量分布均勻些娶吞,盡量使得每個位置上的元素數(shù)量只有一個垒迂,那么當我們用Hash算法求得這個位置的時候,馬上就可以知道對應位置的元素就是我們要的妒蛇,不用遍歷鏈表机断,大大優(yōu)化了查詢的效率。HashMap定位數(shù)組索引位置绣夺,直接決定了hash方法的離散性能吏奸。先看看源碼的實現(xiàn)(方法一+方法二):

這里的Hash算法本質上就是三步:取key的hashCode值、高位運算陶耍、取模運算奋蔚。

對于任意給定的對象,只要它的hashCode()返回值相同烈钞,那么程序調用方法一所計算得到的Hash碼值總是相同的泊碑。我們首先想到的就是把hash值對數(shù)組長度取模運算,這樣一來毯欣,元素的分布相對來說是比較均勻的砸抛。但是,模運算的消耗還是比較大的尸昧,在HashMap中是這樣做的:調用方法二來計算該對象應該保存在table數(shù)組的哪個索引處藤树。

這個方法非常巧妙,它通過h & (table.length -1)來得到該對象的保存位算吩,而HashMap底層數(shù)組的長度總是2的n次方留凭,這是HashMap在速度上的優(yōu)化。當length總是2的n次方時偎巢,h& (length-1)運算等價于對length取模蔼夜,也就是h%length,但是&比%具有更高的效率压昼。

在JDK1.8的實現(xiàn)中求冷,優(yōu)化了高位運算的算法,通過hashCode()的高16位異或低16位實現(xiàn)的:(h = k.hashCode()) ^ (h >>> 16)窍霞,主要是從速度匠题、功效、質量來考慮的但金,這么做可以在數(shù)組table的length比較小的時候韭山,也能保證考慮到高低Bit都參與到Hash的計算中,同時不會有太大的開銷。

下面舉例說明下钱磅,n為table的長度梦裂。

2. 分析HashMap的put方法

HashMap的put方法執(zhí)行過程可以通過下圖來理解,自己有興趣可以去對比源碼更清楚地研究學習盖淡。

①.判斷鍵值對數(shù)組table[i]是否為空或為null年柠,否則執(zhí)行resize()進行擴容;

②.根據(jù)鍵值key計算hash值得到插入的數(shù)組索引i褪迟,如果table[i]==null冗恨,直接新建節(jié)點添加,轉向⑥味赃,如果table[i]不為空掀抹,轉向③;

③.判斷table[i]的首個元素是否和key一樣洁桌,如果相同直接覆蓋value渴丸,否則轉向④,這里的相同指的是hashCode以及equals另凌;

④.判斷table[i] 是否為treeNode谱轨,即table[i] 是否是紅黑樹,如果是紅黑樹吠谢,則直接在樹中插入鍵值對土童,否則轉向⑤;

⑤.遍歷table[i]工坊,判斷鏈表長度是否大于8献汗,大于8的話把鏈表轉換為紅黑樹,在紅黑樹中執(zhí)行插入操作王污,否則進行鏈表的插入操作罢吃;遍歷過程中若發(fā)現(xiàn)key已經(jīng)存在直接覆蓋value即可;

⑥.插入成功后昭齐,判斷實際存在的鍵值對數(shù)量size是否超多了最大容量threshold尿招,如果超過,進行擴容阱驾。

JDK1.8HashMap的put方法源碼如下:

3. 擴容機制

擴容(resize)就是重新計算容量就谜,向HashMap對象里不停的添加元素,而HashMap對象內部的數(shù)組無法裝載更多的元素時里覆,對象就需要擴大數(shù)組的長度丧荐,以便能裝入更多的元素。當然Java里的數(shù)組是無法自動擴容的喧枷,方法是使用一個新的數(shù)組代替已有的容量小的數(shù)組虹统,就像我們用一個小桶裝水弓坞,如果想裝更多的水,就得換大水桶窟却。

我們分析下resize的源碼昼丑,鑒于JDK1.8融入了紅黑樹呻逆,較復雜夸赫,為了便于理解我們仍然使用JDK1.7的代碼,好理解一些咖城,本質上區(qū)別不大茬腿,具體區(qū)別后文再說。

這里就是使用一個容量更大的數(shù)組來代替已有的容量小的數(shù)組宜雀,transfer()方法將原有Entry數(shù)組的元素拷貝到新的Entry數(shù)組里切平。

newTable[i]的引用賦給了e.next,也就是使用了單鏈表的頭插入方式辐董,同一位置上新元素總會被放在鏈表的頭部位置;這樣先放在一個索引上的元素終會被放到Entry鏈的尾部(如果發(fā)生了hash沖突的話)苔严,這一點和Jdk1.8有區(qū)別届氢,下文詳解。在舊數(shù)組中同一條Entry鏈上的元素覆旭,通過重新計算索引位置后型将,有可能被放到了新數(shù)組的不同位置上七兜。

下面舉個例子說明下擴容過程惊搏。假設了我們的hash算法就是簡單的用key mod 一下表的大邢虿稹(也就是數(shù)組的長度)浓恳。其中的哈希桶數(shù)組table的size=2, 所以key = 3梢夯、7、5死姚,put順序依次為 5色罚、7戳护、3腌且。在mod 2以后都沖突在table[1]這里了。這里假設負載因子 loadFactor=1柄粹,即當鍵值對的實際大小size 大于 table的實際大小時進行擴容驻右。接下來的三個步驟是哈希桶數(shù)組 resize成4堪夭,然后所有的Node重新rehash的過程。

下面我們講解下JDK1.8做了哪些優(yōu)化爬迟。經(jīng)過觀測可以發(fā)現(xiàn)付呕,我們使用的是2次冪的擴展(指長度擴為原來2倍)象颖,所以说订,元素的位置要么是在原位置克蚂,要么是在原位置再移動2次冪的位置摸恍”诎溃看下圖可以明白這句話的意思嗜逻,n為table的長度栈顷,圖(a)表示擴容前的key1和key2兩種key確定索引位置的示例,圖(b)表示擴容后key1和key2兩種key確定索引位置的示例靡努,其中hash1是key1對應的哈希與高位運算結果。

元素在重新計算hash之后漾月,因為n變?yōu)?倍飘千,那么n-1的mask范圍在高位多1bit(紅色),因此新的index就會發(fā)生這樣的變化:

因此霉旗,我們在擴充HashMap的時候,不需要像JDK1.7的實現(xiàn)那樣重新計算hash鸵闪,只需要看看原來的hash值新增的那個bit是1還是0就好了,是0的話索引沒變个榕,是1的話索引變成“原索引+oldCap”凰萨,可以看看下圖為16擴充為32的resize示意圖:

這個設計確實非常的巧妙,既省去了重新計算hash值的時間,而且同時食棕,由于新增的1bit是0還是1可以認為是隨機的,因此resize的過程憔儿,均勻的把之前的沖突的節(jié)點分散到新的bucket了朝刊。這一塊就是JDK1.8新增的優(yōu)化點拾氓。有一點注意區(qū)別,JDK1.7中rehash的時候,舊鏈表遷移新鏈表的時候疲酌,如果在新表的數(shù)組索引位置相同,則鏈表元素會倒置,但是從上圖可以看出,JDK1.8不會倒置冀自。有興趣的同學可以研究下JDK1.8的resize源碼,寫的很贊驻呐,如下:

線程安全性

在多線程使用場景中佣盒,應該盡量避免使用線程不安全的HashMap盯仪,而使用線程安全的ConcurrentHashMap。那么為什么說HashMap是線程不安全的,下面舉例子說明在并發(fā)的多線程使用場景中使用HashMap可能造成死循環(huán)奔浅。代碼例子如下(便于理解馆纳,仍然使用JDK1.7的環(huán)境):

其中,map初始化為一個長度為2的數(shù)組鲁驶,loadFactor=0.75舞骆,threshold=2*0.75=1督禽,也就是說當put第二個key的時候胧谈,map就需要進行resize稳强。

通過設置斷點讓線程1和線程2同時debug到transfer方法(3.3小節(jié)代碼塊)的首行。注意此時兩個線程已經(jīng)成功添加數(shù)據(jù)。放開thread1的斷點至transfer方法的“Entry next = e.next;” 這一行;然后放開線程2的的斷點刹帕,讓線程2進行resize。結果如下圖挫掏。

注意侦另,Thread1的 e 指向了key(3),而next指向了key(7)袄友,其在線程二rehash后殿托,指向了線程二重組后的鏈表。

線程一被調度回來執(zhí)行剧蚣,先是執(zhí)行 newTalbe[i] = e碌尔, 然后是e = next,導致了e指向了key(7)券敌,而下一次循環(huán)的next = e.next導致了next指向了key(3)。

e.next = newTable[i] 導致 key(3).next 指向了 key(7)柳洋。注意:此時的key(7).next 已經(jīng)指向了key(3)待诅, 環(huán)形鏈表就這樣出現(xiàn)了。

于是熊镣,當我們用線程一調用map.get(11)時卑雁,悲劇就出現(xiàn)了——Infinite Loop。

JDK1.8與JDK1.7的性能對比

HashMap中绪囱,如果key經(jīng)過hash算法得出的數(shù)組索引位置全部不相同测蹲,即Hash算法非常好,那樣的話鬼吵,getKey方法的時間復雜度就是O(1)扣甲,如果Hash算法技術的結果碰撞非常多,假如Hash算極其差,所有的Hash算法結果得出的索引位置一樣琉挖,那樣所有的鍵值對都集中到一個桶中启泣,或者在一個鏈表中,或者在一個紅黑樹中示辈,時間復雜度分別為O(n)和O(lgn)寥茫。 鑒于JDK1.8做了多方面的優(yōu)化,總體性能優(yōu)于JDK1.7矾麻,下面我們從兩個方面用例子證明這一點纱耻。

Hash比較均勻的情況

為了便于測試,我們先寫一個類Key险耀,如下:


這個類復寫了equals方法弄喘,并且提供了相當好的hashCode函數(shù),任何一個值的hashCode都不會相同胰耗,因為直接使用value當做hashcode限次。為了避免頻繁的GC,我將不變的Key實例緩存了起來柴灯,而不是一遍一遍的創(chuàng)建它們卖漫。代碼如下:

現(xiàn)在開始我們的試驗,測試需要做的僅僅是赠群,創(chuàng)建不同size的HashMap(1羊始、10、100查描、......10000000)突委,屏蔽了擴容的情況,代碼如下:

在測試中會查找不同的值冬三,然后度量花費的時間匀油,為了計算getKey的平均時間,我們遍歷所有的get方法勾笆,計算總的時間敌蚜,除以key的數(shù)量,計算一個平均值窝爪,主要用來比較弛车,絕對值可能會受很多環(huán)境因素的影響。結果如下:

通過觀測測試結果可知蒲每,JDK1.8的性能要高于JDK1.7 15%以上纷跛,在某些size的區(qū)域上,甚至高于100%邀杏。由于Hash算法較均勻贫奠,JDK1.8引入的紅黑樹效果不明顯,下面我們看看Hash不均勻的的情況。

Hash極不均勻的情況

假設我們又一個非常差的Key叮阅,它們所有的實例都返回相同的hashCode值刁品。這是使用HashMap最壞的情況。代碼修改如下:

仍然執(zhí)行main方法浩姥,得出的結果如下表所示:

從表中結果中可知挑随,隨著size的變大,JDK1.7的花費時間是增長的趨勢勒叠,而JDK1.8是明顯的降低趨勢兜挨,并且呈現(xiàn)對數(shù)增長穩(wěn)定。當一個鏈表太長的時候眯分,HashMap會動態(tài)的將它替換成一個紅黑樹拌汇,這話的話會將時間復雜度從O(n)降為O(logn)。hash算法均勻和不均勻所花費的時間明顯也不相同弊决,這兩種情況的相對比較噪舀,可以說明一個好的hash算法的重要性。

測試環(huán)境:處理器為2.2 GHz Intel Core i7飘诗,內存為16 GB 1600 MHz DDR3与倡,SSD硬盤,使用默認的JVM參數(shù)昆稿,運行在64位的OS X 10.10.1上纺座。

小結

(1) 擴容是一個特別耗性能的操作,所以當程序員在使用HashMap的時候溉潭,估算map的大小净响,初始化的時候給一個大致的數(shù)值,避免map進行頻繁的擴容喳瓣。

(2) 負載因子是可以修改的馋贤,也可以大于1,但是建議不要輕易修改畏陕,除非情況非常特殊掸掸。

(3) HashMap是線程不安全的,不要在并發(fā)的環(huán)境中同時操作HashMap蹭秋,建議使用ConcurrentHashMap。

(4) JDK1.8引入紅黑樹大程度優(yōu)化了HashMap的性能堤撵。

(5) 還沒升級JDK1.8的仁讨,現(xiàn)在開始升級吧。HashMap的性能提升僅僅是JDK1.8的冰山一角实昨。

參考

  1. JDK1.7&JDK1.8 源碼洞豁。
  2. CSDN博客頻道,HashMap多線程死循環(huán)問題,2014丈挟。
  3. 紅黑聯(lián)盟刁卜,Java類集框架之HashMap(JDK1.8)源碼剖析,2015曙咽。
  4. CSDN博客頻道蛔趴, 教你初步了解紅黑樹,2010例朱。
  5. Java Code Geeks孝情,HashMap performance improvements in Java 8,2014洒嗤。
  6. Importnew箫荡,危險!在HashMap中將可變對象用作Key渔隶,2014羔挡。
  7. CSDN博客頻道,為什么一般hashtable的桶數(shù)會取一個素數(shù)间唉,2013绞灼。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市终吼,隨后出現(xiàn)的幾起案子镀赌,更是在濱河造成了極大的恐慌,老刑警劉巖际跪,帶你破解...
    沈念sama閱讀 216,919評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件商佛,死亡現(xiàn)場離奇詭異,居然都是意外死亡姆打,警方通過查閱死者的電腦和手機良姆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,567評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來幔戏,“玉大人玛追,你說我怎么就攤上這事∠醒樱” “怎么了痊剖?”我有些...
    開封第一講書人閱讀 163,316評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長垒玲。 經(jīng)常有香客問我陆馁,道長,這世上最難降的妖魔是什么合愈? 我笑而不...
    開封第一講書人閱讀 58,294評論 1 292
  • 正文 為了忘掉前任叮贩,我火速辦了婚禮击狮,結果婚禮上,老公的妹妹穿的比我還像新娘益老。我一直安慰自己彪蓬,他們只是感情好,可當我...
    茶點故事閱讀 67,318評論 6 390
  • 文/花漫 我一把揭開白布捺萌。 她就那樣靜靜地躺著档冬,像睡著了一般。 火紅的嫁衣襯著肌膚如雪互婿。 梳的紋絲不亂的頭發(fā)上捣郊,一...
    開封第一講書人閱讀 51,245評論 1 299
  • 那天,我揣著相機與錄音慈参,去河邊找鬼呛牲。 笑死,一個胖子當著我的面吹牛驮配,可吹牛的內容都是我干的娘扩。 我是一名探鬼主播,決...
    沈念sama閱讀 40,120評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼壮锻,長吁一口氣:“原來是場噩夢啊……” “哼琐旁!你這毒婦竟也來了?” 一聲冷哼從身側響起猜绣,我...
    開封第一講書人閱讀 38,964評論 0 275
  • 序言:老撾萬榮一對情侶失蹤灰殴,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后掰邢,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體牺陶,經(jīng)...
    沈念sama閱讀 45,376評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,592評論 2 333
  • 正文 我和宋清朗相戀三年辣之,在試婚紗的時候發(fā)現(xiàn)自己被綠了掰伸。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,764評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡怀估,死狀恐怖狮鸭,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情多搀,我是刑警寧澤歧蕉,帶...
    沈念sama閱讀 35,460評論 5 344
  • 正文 年R本政府宣布,位于F島的核電站康铭,受9級特大地震影響廊谓,放射性物質發(fā)生泄漏。R本人自食惡果不足惜麻削,卻給世界環(huán)境...
    茶點故事閱讀 41,070評論 3 327
  • 文/蒙蒙 一蒸痹、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧呛哟,春花似錦叠荠、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,697評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至鳖孤,卻和暖如春者娱,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背苏揣。 一陣腳步聲響...
    開封第一講書人閱讀 32,846評論 1 269
  • 我被黑心中介騙來泰國打工黄鳍, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人平匈。 一個月前我還...
    沈念sama閱讀 47,819評論 2 370
  • 正文 我出身青樓框沟,卻偏偏與公主長得像,于是被迫代替她去往敵國和親增炭。 傳聞我的和親對象是個殘疾皇子忍燥,可洞房花燭夜當晚...
    茶點故事閱讀 44,665評論 2 354

推薦閱讀更多精彩內容