HashMap1.7和ConcurrentHashMap1.7原理分析

面試的時候經(jīng)常問到产喉。這里對jdk1.7的HashMap和ConcurrentHashMap原理進(jìn)行分析矫限。下篇將詳細(xì)分析jdk1.8的實現(xiàn)彭雾。

1 HashMap

HashMap是基于哈希表的Map接口的非同步實現(xiàn)沪铭。此實現(xiàn)提供所有可選的映射操作旱易,并允許使用null值和null鍵。此類不保證映射的順序建丧,特別是它不保證該順序恒久不變(擴(kuò)容的時候會變排龄,后面會詳細(xì)說明)。

1.1 成員結(jié)構(gòu)

  • 數(shù)組
    采用一段連續(xù)的存儲單元來存儲數(shù)據(jù)翎朱。對于指定下標(biāo)的查找橄维,時間復(fù)雜度為O(1);通過給定值進(jìn)行查找,需要對數(shù)組進(jìn)行全部遍歷拴曲,時間復(fù)雜度為O(n)争舞;對一般的插入操作,涉及到數(shù)組元素的移動澈灼,平均復(fù)雜度為O(n)竞川。

  • 線性鏈表
    對于鏈表的新增、刪除操作(在找到指定操作位置后)叁熔,僅需要處理節(jié)點之間的引用即可委乌,時間復(fù)雜度為O(1);查找的時候需要遍歷整個鏈表。

  • 二叉樹
    對一顆平衡的有序二叉樹荣回,插入遭贸、查找、刪除心软。時間復(fù)雜度為O(logn)

  • 哈希表
    相比上訴幾種數(shù)據(jù)結(jié)構(gòu)革砸,在哈希表中進(jìn)行添加、刪除糯累、查找算利、性能十分的高,在不考慮hash沖突的情況下僅需要一次定位即可泳姐,時間復(fù)雜度為O(1)效拭。
    數(shù)據(jù)結(jié)構(gòu)的物理存儲結(jié)構(gòu)只有兩種:順序存儲和鏈?zhǔn)酱鎯?像棧、隊列、樹缎患、圖等是從邏輯結(jié)構(gòu)去抽象的慕的,映射到內(nèi)存中只有兩種結(jié)構(gòu))。上面提到根據(jù)下標(biāo)去查找數(shù)據(jù)挤渔,一次定位就可以找到肮街,哈希表就是利用了這種特性。因此哈希表的主干就是數(shù)組判导。
    hash沖突
    兩個數(shù)據(jù)可能取hash之后是相同的值嫉父,這樣存儲的時候就產(chǎn)生了hash碰撞。對于解決hash碰撞有多種方案:開放地址法(發(fā)生沖突眼刃,繼續(xù)尋找下一塊未被占用的存儲地址)绕辖、再散列法、鏈地址法擂红。HashMap就是采用了鏈地址法仪际。

HashMap的主干就是一個數(shù)組(Entry),每一個Entry是一個(key,value)的鍵值對

//初始化一個Entry數(shù)組,數(shù)組的長度一定是2的次冪昵骤。
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

Entry是HashMap的一個靜態(tài)內(nèi)部類树碱,代碼如下:

static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;//存儲指向下一個Entry的引用,單鏈表結(jié)構(gòu)
        int hash;//對key的hashcode值進(jìn)行hash運(yùn)算后得到的值变秦,存儲在Entry成榜,避免重復(fù)計算
        /**
         * Creates new entry.
         */
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        } 
HashMap結(jié)構(gòu)圖

如圖,HashMap主干是Entry數(shù)組伴栓,縱向是鏈表結(jié)構(gòu)伦连。
其他幾個重要字段:

//實際存儲的key-value鍵值對的個數(shù)
transient int size;
//閾值雨饺,當(dāng)table == {}時钳垮,該值為初始容量(初始容量默認(rèn)為16);當(dāng)table被填充了额港,也就是為table分配內(nèi)存空間后饺窿,threshold一般為 capacity*loadFactory。HashMap在進(jìn)行擴(kuò)容時需要參考threshold移斩,后面會詳細(xì)談到
int threshold;
//負(fù)載因子肚医,代表了table的填充度有多少,默認(rèn)是0.75
final float loadFactor;
//用于快速失敗向瓷,由于HashMap非線程安全肠套,在對HashMap進(jìn)行迭代時,如果期間其他線程的參與導(dǎo)致HashMap的結(jié)構(gòu)發(fā)生變化了(比如put猖任,remove等操作)你稚,需要拋出異常ConcurrentModificationException
transient int modCount;

1.2 構(gòu)造函數(shù)

public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +loadFactor);
        this.loadFactor = loadFactor;
        threshold = initialCapacity;
        init();//init方法在HashMap中沒有實際實現(xiàn),不過在其子類如 linkedHashMap中就會有對應(yīng)實現(xiàn)
    }

1.3 put()原理

通過上面的構(gòu)造函數(shù)發(fā)現(xiàn),在初始化的時候沒有個table分配內(nèi)存刁赖,而是在put的時候才初始化分配內(nèi)存搁痛。

public V put(K key, V value) {
        //如果table數(shù)組為空數(shù)組,進(jìn)行數(shù)組填充(為table分配實際內(nèi)存空間)宇弛,
        //入?yún)閠hreshold鸡典,此時threshold為initialCapacity 默認(rèn)是1<<4(16)
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
       //如果key為null,存儲位置為table[0]或table[0]的沖突鏈上
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);//對key的hashcode進(jìn)一步計算枪芒,確保散列均勻
        int i = indexFor(hash, table.length);//獲取在table中的實際位置
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        //如果該對應(yīng)數(shù)據(jù)已存在彻况,執(zhí)行覆蓋操作。用新value替換舊value病苗,并返回舊value
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;//保證并發(fā)訪問時疗垛,若HashMap內(nèi)部結(jié)構(gòu)發(fā)生變化,快速響應(yīng)失敗
        addEntry(hash, key, value, i);//新增一個entry
        return null;
    }

先來看看inflateTable這個方法

private void inflateTable(int toSize) {
        int capacity = roundUpToPowerOf2(toSize);//capacity一定是2的次冪
        //此處為threshold賦值硫朦,取capacity*loadFactor和MAXIMUM_CAPACITY+1的最小值贷腕,
        //capaticy一定不會超過MAXIMUM_CAPACITY,除非loadFactor大于1
        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        table = new Entry[capacity];
        initHashSeedAsNeeded(capacity);
    }

保證數(shù)組的長度是2的次冪

   private static int roundUpToPowerOf2(int number) {
        // assert number >= 0 : "number must be non-negative";
        return number >= MAXIMUM_CAPACITY
                ? MAXIMUM_CAPACITY
                : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
    }

對key進(jìn)行hash盡量讓key分散咬展,避免hash碰撞

//這是一個神奇的函數(shù)泽裳,用了很多的異或,移位等運(yùn)算破婆,
//對key的hashcode進(jìn)一步進(jìn)行計算以及二進(jìn)制位的調(diào)整等來保證最終獲取的存儲位置盡量分布均勻
    final int hash(Object k) {
        int h = hashSeed;
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }
        h ^= k.hashCode();
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

計算新插入數(shù)據(jù)的數(shù)組下標(biāo)

  /**
     * 返回數(shù)組下標(biāo)
     */
    static int indexFor(int h, int length) {
        return h & (length-1);
    }

我們可以想到的是取模運(yùn)算就能分散到數(shù)組中涮总,這里使用與運(yùn)算(&),因為與運(yùn)算比取牡灰ǎ快瀑梗。
為什么他們會相等?
如下裳扯,假設(shè)數(shù)組長度是16抛丽,當(dāng)前的hash(k)=17,取模計算后是index=1饰豺。這時候length-1=15,二進(jìn)制運(yùn)算如下亿鲜,算出來的也是index=1。

       0001 0001
   &   0000 1111
---------------------
       0000 0001    =   1

這就是HashMap在put的時候下標(biāo)確定流程


put時數(shù)組下標(biāo)確定

再來看看addEntry的實現(xiàn):

    void addEntry(int hash, K key, V value, int bucketIndex) {
        //當(dāng)size超過臨界閾值threshold冤吨,并且即將發(fā)生哈希沖突時進(jìn)行擴(kuò)容
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }

        createEntry(hash, key, value, bucketIndex);
    }

1.4 擴(kuò)容原理

繼續(xù)看看resize方法

     void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }
        Entry[] newTable = new Entry[newCapacity];
        //把舊數(shù)據(jù)移入新的table中
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

繼續(xù)看transfer方法

void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        //for循環(huán)中的代碼蒿柳,逐個遍歷鏈表,重新計算索引位置,
         //將老數(shù)組數(shù)據(jù)復(fù)制到新數(shù)組中去(數(shù)組不存儲實際數(shù)據(jù)漩蟆,所以僅僅是拷貝引用而已)
        for (Entry<K,V> e : table) {
            while(null != e) {
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);
        //將當(dāng)前entry的next鏈指向新的索引位置,newTable[i]有可能為空垒探,
           //有可能也是個entry鏈,如果是entry鏈怠李,直接在鏈表頭部插入圾叼。
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }

transfer方法是在擴(kuò)容的時候執(zhí)行仔引,把舊的table中的數(shù)據(jù)拷貝到新的table中。

為何HashMap的數(shù)組長度一定是2的次冪褐奥?

indexFor的時候是使用h&(length-1),這時候數(shù)組的長度是2的次冪咖耘。假如是16,這時候二進(jìn)制是0001 0000撬码,length-1后就是 0000 1111儿倒,做與運(yùn)算的時候就只與低位有關(guān)。擴(kuò)容后是32呜笑,length-1后是0001 1111夫否,這樣與
h求與運(yùn)算就不會改變原來數(shù)據(jù)在數(shù)組中的位置。
總結(jié)就三個原因

  • 在計算數(shù)組下標(biāo)的時候更快速
  • h的高位對數(shù)組位置計算不產(chǎn)生影響
  • 擴(kuò)容的時候叫胁,不會改變原來已經(jīng)散列好的數(shù)據(jù)在數(shù)組中的位置

1.5 get()原理

 public V get(Object key) {
     //如果key為null,則直接去table[0]處去檢索即可凰慈。
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);
        return null == entry ? null : entry.getValue();
 }

get方法通過key值返回對應(yīng)value,如果key為null驼鹅,直接去table[0]處檢索微谓。我們再看一下getEntry這個方法

final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }
        //通過key的hashcode值計算hash值
        int hash = (key == null) ? 0 : hash(key);
        //indexFor (hash&length-1) 獲取最終數(shù)組索引,然后遍歷鏈表输钩,通過equals方法比對找出對應(yīng)記錄
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
             Object k;
             if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }   

可以看出get方法是很簡單的豺型,具體流程就是先計算key的hash值,然后找到對應(yīng)的table[i],然后遍歷鏈表找到hash值相同的Entry,返回e.value()买乃。

2 ConcurrentHashMap

在理解了HashMap的原理之后再來理解ConcurrentHashMap就簡單很多姻氨。

1.1 成員結(jié)構(gòu)

由 segment數(shù)組和多個HashEntry組成,如下圖所示


數(shù)據(jù)結(jié)構(gòu)

segment數(shù)組的意義是將一個大的table分割成多個小的table,使用的是鎖分離技術(shù)剪验。每個segment存儲的是HashEntry數(shù)組+鏈表肴焊,這個就和HashMap的數(shù)據(jù)存儲一樣
初始化

int sshift = 0;
int ssize = 1;
while (ssize < concurrencyLevel) {
    ++sshift;
    ssize <<= 1;
}

初始化的時候需要初始化segment的容量

1.2 put()原理

ConcurrentHashMap在進(jìn)行數(shù)據(jù)插入的時候需要兩次hash去定位數(shù)據(jù)的存儲位置

/**
 * map的put方法,定位segment
 */
public V put(K key, V value) {
    Segment<K,V> s;
    // value不能為空
    if (value == null)
        throw new NullPointerException();
    // 獲取hash
    int hash = hash(key);
    // 定位segments 數(shù)組的位置
    int j = (hash >>> segmentShift) & segmentMask;
    // 獲取這個segment
    if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
            (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
        // 為null 初始化當(dāng)前位置的segment
        s = ensureSegment(j);
    return s.put(key, hash, value, false);
}
    /**
     * put到table方法
     */
    final V put(K key, int hash, V value, boolean onlyIfAbsent) {
        // 是否獲取鎖,失敗自旋獲取鎖(直到成功)
        HashEntry<K,V> node = tryLock() ? null :
                scanAndLockForPut(key, hash, value);
        V oldValue;
        try {
            HashEntry<K,V>[] tab = table;
            // 定義位置
            int index = (tab.length - 1) & hash;
            // 獲取第一個桶的第一個元素
            // entryAt 底層調(diào)用getObjectVolatile 具有volatile讀語義
            HashEntry<K,V> first = entryAt(tab, index);
            for (HashEntry<K,V> e = first;;) {
                if (e != null) { // 證明鏈?zhǔn)浇Y(jié)構(gòu)有數(shù)據(jù) 遍歷節(jié)點數(shù)據(jù)替換,直到e=null
                    K k;
                    if ((k = e.key) == key ||
                            (e.hash == hash && key.equals(k))) { //  找到了相同的key
                        oldValue = e.value;
                        if (!onlyIfAbsent) { // 默認(rèn)值false
                            e.value = value; // 替換value
                            ++modCount;
                        }
                        break; // 結(jié)束循環(huán)
                    }
                    e = e.next;
                }
                else { // e=null (1) 之前沒有數(shù)據(jù) (2) 沒有找到替換的元素
                    // node是否為空,這個獲取鎖的是有關(guān)系的
                    // (1) node不為null,設(shè)置node的next為first
                    // (2) node為null,創(chuàng)建頭節(jié)點,指定next為first
                    if (node != null)
                        // 底層使用 putOrderedObject 方法 具有volatile寫語義
                        node.setNext(first);
                    else
                        node = new HashEntry<K,V>(hash, key, value, first);
                    int c = count + 1;
                    // 擴(kuò)容條件 (1)entry數(shù)量大于閾值 (2) 當(dāng)前table的數(shù)量小于最大容量  滿足以上條件就擴(kuò)容
                    if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                        // 擴(kuò)容方法,方法里面具體講
                        rehash(node);
                    else
                        // 給table的index位置設(shè)置為node,
                        // node為頭結(jié)點,原來的頭結(jié)點first為node的next節(jié)點
                        // 底層也是調(diào)用的 putOrderedObject 方法 具有volatile寫語義
                        setEntryAt(tab, index, node);
                    ++modCount;
                    count = c;
                    oldValue = null;
                    break;
                }
            }
        } finally {
            unlock();
        }
        return oldValue;
    }

segment繼承自ReentrantLock,因此帶有鎖的功能功戚,當(dāng)在put數(shù)據(jù)的時候娶眷,
1:先獲取對應(yīng)segment的位置(第一次hash)
2:獲取segment的鎖,如果獲取到鎖疫铜,再計算在HashEntry數(shù)組中的位置(第二次hash)茂浮,添加到鏈表的尾部双谆。

1.3 get()原理

ConcurrentHashMap的get操作跟HashMap類似壳咕,只是ConcurrentHashMap第一次需要經(jīng)過一次hash定位到Segment的位置,然后再hash定位到指定的HashEntry顽馋,遍歷該HashEntry下的鏈表進(jìn)行對比谓厘,成功就返回,不成功就返回null

/**
 * get 方法
 */
public V get(Object key) {
    Segment<K,V> s; // manually integrate access methods to reduce overhead
    HashEntry<K,V>[] tab;
    int h = hash(key);
    long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE; // 獲取segment的位置
    // getObjectVolatile getObjectVolatile語義讀取最新的segment,獲取table
    if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
            (tab = s.table) != null) {
        // getObjectVolatile getObjectVolatile語義讀取最新的hashEntry,并遍歷
        for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
                (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
             e != null; e = e.next) {
            K k;
            // 找到相同的key 返回
            if ((k = e.key) == key || (e.hash == h && key.equals(k)))
                return e.value;
        }
    }
    return null;
}

1.4 擴(kuò)容原理

/**
     *擴(kuò)容方法
     */
    private void rehash(HashEntry<K,V> node) {

        // 舊的table
        HashEntry<K,V>[] oldTable = table;
        // 舊的table的長度
        int oldCapacity = oldTable.length;
        // 擴(kuò)容原來capacity的一倍
        int newCapacity = oldCapacity << 1;
        // 新的閾值
        threshold = (int)(newCapacity * loadFactor);
        // 新的table
        HashEntry<K,V>[] newTable =
                (HashEntry<K,V>[]) new HashEntry[newCapacity];
        // 新的掩碼
        int sizeMask = newCapacity - 1;
        // 遍歷舊的table
        for (int i = 0; i < oldCapacity ; i++) {
            // table中的每一個鏈表元素
            HashEntry<K,V> e = oldTable[i];
            if (e != null) { // e不等于null
                HashEntry<K,V> next = e.next; // 下一個元素
                int idx = e.hash & sizeMask;  // 重新計算位置,計算在新的table的位置
                if (next == null)   //  Single node on list 證明只有一個元素
                    newTable[idx] = e; // 把當(dāng)前的e設(shè)置給新的table
                else { // Reuse consecutive sequence at same slot
                    HashEntry<K,V> lastRun = e; // 當(dāng)前e
                    int lastIdx = idx;          // 在新table的位置
                    for (HashEntry<K,V> last = next;
                         last != null;
                         last = last.next) { // 遍歷鏈表
                        int k = last.hash & sizeMask; // 確定在新table的位置
                        if (k != lastIdx) { // 頭結(jié)點和頭結(jié)點的next元素的節(jié)點發(fā)生了變化
                            lastIdx = k;    // 記錄變化位置
                            lastRun = last; // 記錄變化節(jié)點
                        }
                    }
                    // 以下把鏈表設(shè)置到新table分為兩種情況
                    // (1) lastRun 和 lastIdx 沒有發(fā)生變化,也就是整個鏈表的每個元素位置和一樣,都沒有發(fā)生變化
                    // (2) lastRun 和 lastIdx 發(fā)生了變化,記錄變化位置和變化節(jié)點,然后把變化的這個節(jié)點設(shè)置到新table
                    //     ,但是整個鏈表的位置只有變化節(jié)點和它后面關(guān)聯(lián)的節(jié)點是對的
                    //      下面的這個遍歷就是處理這個問題,遍歷當(dāng)前頭節(jié)點e,找出不等于變化節(jié)點(lastRun)的節(jié)點重新處理
                    newTable[lastIdx] = lastRun;
                    // Clone remaining nodes
                    for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {
                        V v = p.value;
                        int h = p.hash;
                        int k = h & sizeMask;
                        HashEntry<K,V> n = newTable[k];
                        newTable[k] = new HashEntry<K,V>(h, p.key, v, n);
                    }
                }
            }
        }
        // 處理擴(kuò)容時那個添加的節(jié)點

        // 計算位置
        int nodeIndex = node.hash & sizeMask; // add the new node
        // 設(shè)置next節(jié)點,此時已經(jīng)擴(kuò)容完成,要從新table里面去當(dāng)前位置的頭結(jié)點為next節(jié)點
        node.setNext(newTable[nodeIndex]);
        // 設(shè)置位置
        newTable[nodeIndex] = node;
        // 新table替換舊的table
        table = newTable;
    }
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末寸谜,一起剝皮案震驚了整個濱河市竟稳,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖他爸,帶你破解...
    沈念sama閱讀 216,591評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件聂宾,死亡現(xiàn)場離奇詭異,居然都是意外死亡诊笤,警方通過查閱死者的電腦和手機(jī)系谐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來讨跟,“玉大人纪他,你說我怎么就攤上這事×澜常” “怎么了茶袒?”我有些...
    開封第一講書人閱讀 162,823評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長凉馆。 經(jīng)常有香客問我薪寓,道長,這世上最難降的妖魔是什么澜共? 我笑而不...
    開封第一講書人閱讀 58,204評論 1 292
  • 正文 為了忘掉前任预愤,我火速辦了婚禮,結(jié)果婚禮上咳胃,老公的妹妹穿的比我還像新娘植康。我一直安慰自己,他們只是感情好展懈,可當(dāng)我...
    茶點故事閱讀 67,228評論 6 388
  • 文/花漫 我一把揭開白布销睁。 她就那樣靜靜地躺著,像睡著了一般存崖。 火紅的嫁衣襯著肌膚如雪冻记。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,190評論 1 299
  • 那天来惧,我揣著相機(jī)與錄音冗栗,去河邊找鬼。 笑死供搀,一個胖子當(dāng)著我的面吹牛隅居,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播葛虐,決...
    沈念sama閱讀 40,078評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼胎源,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了屿脐?” 一聲冷哼從身側(cè)響起涕蚤,我...
    開封第一講書人閱讀 38,923評論 0 274
  • 序言:老撾萬榮一對情侶失蹤宪卿,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后万栅,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體佑钾,經(jīng)...
    沈念sama閱讀 45,334評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,550評論 2 333
  • 正文 我和宋清朗相戀三年烦粒,在試婚紗的時候發(fā)現(xiàn)自己被綠了次绘。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,727評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡撒遣,死狀恐怖邮偎,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情义黎,我是刑警寧澤禾进,帶...
    沈念sama閱讀 35,428評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站廉涕,受9級特大地震影響泻云,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜狐蜕,卻給世界環(huán)境...
    茶點故事閱讀 41,022評論 3 326
  • 文/蒙蒙 一宠纯、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧层释,春花似錦婆瓜、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至乖寒,卻和暖如春猴蹂,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背楣嘁。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評論 1 269
  • 我被黑心中介騙來泰國打工磅轻, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人逐虚。 一個月前我還...
    沈念sama閱讀 47,734評論 2 368
  • 正文 我出身青樓聋溜,卻偏偏與公主長得像,于是被迫代替她去往敵國和親痊班。 傳聞我的和親對象是個殘疾皇子勤婚,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,619評論 2 354

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