簡(jiǎn)析guava cache線程安全設(shè)計(jì)哲學(xué)

1耍共、 前言

guava cache是Google 出品的 Java 核心增強(qiáng)庫(kù)的緩存部分,有著非常廣泛的應(yīng)用猎塞,有別于ConcurrentHashMap试读,guava cache可以按照多種策略來(lái)清理存儲(chǔ)在其中的緩存值且保持很高的并發(fā)讀寫(xiě)性能。guava cache的設(shè)計(jì)運(yùn)用了LRU算法荠耽,java的設(shè)計(jì)模式钩骇,實(shí)現(xiàn)了緩存數(shù)據(jù)統(tǒng)計(jì),線程安全等很多功能铝量,本文僅僅從guava cache 線程安全和高并發(fā)性能的角度倘屹,對(duì)guava cache的設(shè)計(jì)哲學(xué)進(jìn)行簡(jiǎn)單分析。為了更好的理解guava cache的緩存設(shè)計(jì)慢叨,我們首先自己設(shè)計(jì)一個(gè)簡(jiǎn)單的線程安全的緩存系統(tǒng)纽匙,然后再看guava cache的相關(guān)源碼并簡(jiǎn)單分析。


2插爹、多階段設(shè)計(jì)線程安全的緩存系統(tǒng)#

假設(shè)公司有一個(gè)“time-honored”的類(lèi)叫ExpensiveCompution哄辣,其通過(guò)一個(gè)方法可以傳入一個(gè)long類(lèi)型的數(shù)字,返回一個(gè)BigDecimal類(lèi)型的數(shù)字赠尾,其代碼如下:

public class ExpensiveCompution {
    public BigDecimal compute(Long key) {
        BigDecimal along = new BigDecimal(0);
        /**
         * 利用string計(jì)算出一個(gè)長(zhǎng)整型的數(shù)復(fù)制給along
         * 方法比較耗時(shí)間
         */
        return  along;
    }
}

現(xiàn)在公司要求我們?cè)O(shè)計(jì)個(gè)緩存系統(tǒng)來(lái)改善下性能力穗,要求緩存系統(tǒng)線程安全。


第一階段

public class Cache1 {
    private final static ExpensiveCompution computer = new ExpensiveCompution();
    private final static Map<Long, BigDecimal> map = new HashMap<>();

    private Cache1() {
        throw new RuntimeException("the Cache1 cannot be instantiated!");
    }


    public static synchronized BigDecimal compute(long key) {
        BigDecimal value = map.get(key);
        if (value == null) {
            value = computer.compute(key);
            map.put(key, value);
        }

        return value;
   

大家都可以不加思索的設(shè)計(jì)出這這個(gè)版本气嫁,但是這個(gè)版本在并發(fā)效率上是非常低的当窗,在多線程環(huán)境下,有時(shí)候Cache1類(lèi)反而可能成為累贅寸宵。具體如下圖所示:


cache1.png

圖中淺紅色表示緩存沒(méi)有命中的情況崖面,淺藍(lán)色表示緩存命中的情況元咙。我們假設(shè)線程1,線程2巫员,線程3同時(shí)分別請(qǐng)求對(duì)1,2,3的計(jì)算請(qǐng)求庶香。為了方便,假設(shè)三個(gè)線程獲取鎖的先后順序?yàn)?線程1简识,線程2赶掖,線程3(如無(wú)特殊說(shuō)明,本文都采用這個(gè)假設(shè))七扰。因?yàn)镃ache1為了保證線程安全性奢赂,其用了synchrnozied關(guān)鍵字。這使得同一時(shí)間只能有一個(gè)線程調(diào)用Cache1.compute方法颈走。如果把cache不能命中時(shí)Cache1.compute方法的執(zhí)行時(shí)間設(shè)為1單位時(shí)間膳灶。cache命中時(shí)從map中取key對(duì)應(yīng)的value值用時(shí)為0.1單位時(shí)間(實(shí)際上用不了這么長(zhǎng)時(shí)間,為了直觀而用了一個(gè)較大的值)立由。取鎖(Lock)和釋放鎖(UnLock)用時(shí)為0.1個(gè)單位時(shí)間(實(shí)際上用時(shí)遠(yuǎn)遠(yuǎn)達(dá)不到0.1單位時(shí)間這么長(zhǎng)轧钓,這里為了直觀而用了一個(gè)較大的時(shí)間單位)。那么三個(gè)線程平均用時(shí)為1.9個(gè)單位時(shí)間((1.1+(1.1+1.1)+(1.1+1.1+0.2))/3 = 1.9)拆吆。Cache1的引用在某些情況下甚至起到了負(fù)作用聋迎,因?yàn)榧词共挥镁彺嬷苯邮褂肊xpensiveCompution.compute方法,其線程的平均用時(shí)也只有1單位時(shí)間枣耀。這肯定需要改善霉晕。


第二階段
根據(jù)第一階段的分析,我們知道即使像線程3那種緩存命中的情況下,也需要有取鎖和釋放鎖的操作,而這也增加了額外的耗時(shí)搪锣。受啟發(fā)設(shè)計(jì)Cache2類(lèi)如下:

public class Cache2 {
    private final static ExpensiveCompution computer = new ExpensiveCompution();
    private final static Map<Long, BigDecimal> map = new HashMap<Long, BigDecimal>();


    private Cache2() {
        throw new RuntimeException("the Cache2 cannot be instantiated!");
    }

    public static BigDecimal compute(Long key) {
        BigDecimal value = map.get(key); //1
        if (value == null) {
            synchronized(Cache2.class) {
                value = map.get(key);
                if (value == null) {
                    value = computer.compute(key);
                    map.put(key, value); //2
                }
            }
        }

        return value;
    }
}

Cache2類(lèi)首先在不加鎖的情況下判斷map中是否已有查詢(xún)的key值樱衷,如果存在那么直接返回其對(duì)應(yīng)的value值车胡;如果不存在,其算法和Cache1中的一樣:加鎖,判空,計(jì)算值筏养。這是在單例設(shè)計(jì)模式中很常見(jiàn)的DCL方式(double check lock)。
如果有線程1常拓、線程2渐溶、線程3同時(shí)分別調(diào)用Cache2.compute方法分別計(jì)算1、2弄抬、3對(duì)應(yīng)的返回值茎辐,則每個(gè)線程的平均用時(shí)為1.86((1.1 + (1.1+1.1)+(1.1+1.1+0.1))/3=1.86)。Cache2較Cache1時(shí)間上還是有一定的優(yōu)化,特別是在高并發(fā)的條件下拖陆,對(duì)于不加鎖的命中緩存情況效果是很可觀的弛槐。但是看過(guò)java內(nèi)存模型這篇文章的朋友應(yīng)該能發(fā)現(xiàn)Cache2有個(gè)線程安全問(wèn)題:線程3,在執(zhí)行 //1處的map.get(key)方法時(shí)依啰,不一定能獲取線程1在 //2 處放到map中的value值乎串,這是可見(jiàn)性問(wèn)題。而如果線程3不能在 //1處拿到值速警,則需要加鎖灌闺,判空,計(jì)算值坏瞄。這樣每個(gè)線程的平均用時(shí)和Cache1的一樣,都是1.9單位時(shí)間甩卓,沒(méi)有任何優(yōu)勢(shì)可言鸠匀。


第三階段
同樣根據(jù)java內(nèi)存模型可知,volatile關(guān)鍵字的設(shè)計(jì)就是為了滿(mǎn)足操作可見(jiàn)性的逾柿。受此啟發(fā)設(shè)計(jì)Cache3類(lèi)如下:

public class Cache3 {
    private final static ExpensiveCompution computer = new ExpensiveCompution();
    private final static Map<Long, BigDecimal> map = new HashMap<Long, BigDecimal>();
    private static volatile long num = 0;


    private Cache3() {
        throw new RuntimeException("the Cache3 cannot be instantiated!");
    }

    public static BigDecimal compute(Long key) {
        BigDecimal value;
        if (num > 0) { //1
            value = map.get(key); //2
            if (value == null) {
                synchronized (Cache3.class) {
                    value = map.get(key);
                    if (value == null) {
                        value = computer.compute(key); //3
                        map.put(key, value);
                        num++;
                    }
                }
            }
        } else {
            synchronized (Cache3.class) {
                value = map.get(key);
                if (value == null) {
                    value = computer.compute(key);
                    map.put(key, value);
                    num++;
                }
            }
        }

        return value;
    }
}

在Cache3中缀棍,num變量被定義為一個(gè)volatile的變量,//1處的讀volatile變量就是為了觸發(fā)了“volatile的happen-before原則”和“happen-before的傳遞性原則”机错。所以可以保證線程3在//2處一定可以拿到線程1放到map中的value值爬范,從而保證了在Cache3系統(tǒng)中,三個(gè)線程的平均用時(shí)為1.86個(gè)單位時(shí)間弱匪。Cache3的//3處某個(gè)key的具體的value值計(jì)算放在了鎖中青瀑,而這個(gè)計(jì)算時(shí)間是比較長(zhǎng)的(本文中假設(shè)為1時(shí)間單位),這意味著各個(gè)線程將長(zhǎng)時(shí)間持有鎖萧诫,而持有鎖的時(shí)間越長(zhǎng)斥难,線程之間對(duì)鎖的競(jìng)爭(zhēng)就越嚴(yán)重。我們知道多個(gè)線程同時(shí)競(jìng)爭(zhēng)統(tǒng)一把鎖時(shí)只能有一個(gè)勝出者帘饶,而失敗者因拿不到其運(yùn)行的必要資源而從運(yùn)行態(tài)進(jìn)入阻塞態(tài)哑诊,等勝出者釋放鎖以后,失敗者還要從阻塞態(tài)進(jìn)入就緒態(tài)然后重新等待拿鎖及刻,線程各種運(yùn)行態(tài)的切換是需要CPU成本镀裤,而線程長(zhǎng)時(shí)間的持有鎖則增加了這種CPU成本。這是需要改進(jìn)的缴饭。


第四階段
受到第三階段的啟發(fā)暑劝,我們把用時(shí)1時(shí)間單位的compute計(jì)算放在鎖的外面,來(lái)減少線程持有鎖的時(shí)間:

public class Cache4 {
    private final static ExpensiveCompution computer = new ExpensiveCompution();
    private final static Map<Long, BigDecimal> map = new ConcurrentHashMap<>();
    private static volatile long num = 0;


    private Cache4() {
        throw new RuntimeException("the Cache4 cannot be instantiated!");
    }

    public static BigDecimal compute(Long key) {
        BigDecimal value;
        if (num > 0) {
            value = map.get(key);
            if (value == null) {
                synchronized (Cache4.class) {  //1
                    value = map.get(key);      //1
                }                              //1
                if (value == null) {           
                    value = computer.compute(key);  //2
                    synchronized (Cache4.class) {   //3
                        map.put(key, value);        //3
                        num++;                      //3
                    }                               //3
                }

            }
        } else {
            synchronized (Cache4.class) {
                value = map.get(key);
            }
            if (value == null) {
                value = computer.compute(key);
                synchronized (Cache4.class) {
                    map.put(key, value);
                    num++;
                }
            }
        }

        return value;
    }
}

Cache4的最大持有鎖時(shí)間為0.2個(gè)時(shí)間單位(加鎖的獲取key值和加鎖的存儲(chǔ)key茴扁,其耗時(shí)分別假設(shè)為0.1個(gè)時(shí)間單位)铃岔,這極大的降低了線程對(duì)鎖的競(jìng)爭(zhēng),從而提高了并發(fā)效率。但是Cache4存在同一個(gè)key值可能重復(fù)計(jì)算的問(wèn)題:


cache4.png

上圖是Cache4的主體結(jié)構(gòu)圖毁习,lockedGet方法代表Cache4中的 //1部分智嚷,compute方法代表Cache4中的 //2部分,lockedSave方法代表Cache4中的 //3部分纺且。如果線程1在計(jì)算key=1時(shí)的value值時(shí)盏道,線程3在圖中0.1到1.1的時(shí)間范圍內(nèi)也開(kāi)始計(jì)算key=1時(shí)的value值,那么線程1和線程3將重復(fù)執(zhí)行 Cache4中的 //2部分载碌。同一個(gè)key值重復(fù)計(jì)算了2次猜嘱,這是和緩存系統(tǒng)設(shè)計(jì)的理念向佐的(同一個(gè)key值,計(jì)算且今計(jì)算一次)嫁艇。這是需要改進(jìn)的朗伶。


第五階段
根據(jù)Cache3和Cache4的分析可知,如果把compute放在鎖中計(jì)算步咪,存在著線程對(duì)鎖的競(jìng)爭(zhēng)嚴(yán)重問(wèn)題而浪費(fèi)CPU資源论皆,而不把compute放在鎖中則存在重復(fù)計(jì)算相同key值的問(wèn)題。有沒(méi)有即讓緩存系統(tǒng)那鎖時(shí)間短猾漫,同時(shí)又不重復(fù)計(jì)算相同key值呢点晴?答案是肯定的,具體見(jiàn)如下類(lèi)Cache5:


public class Cache5 {
    private final static ExpensiveCompution computer = new ExpensiveCompution();
    private final static Map<Long, FutureTask<BigDecimal>> map = new ConcurrentHashMap<>();
    private static volatile long num = 0;


    private Cache5() {
        throw new RuntimeException("the Cache5 cannot be instantiated!");
    }

    public static BigDecimal compute(Long key) throws Exception {
        FutureTask<BigDecimal> valueTask;
        boolean needRun = false; //是否需要?jiǎng)?chuàng)建一個(gè)計(jì)算任務(wù)
        if (num > 0) {
            valueTask = map.get(key);
            if (valueTask == null) {
                synchronized (Cache5.class) {  //1
                    valueTask = map.get(key);  //1
                    if (valueTask == null) {   //1
                        valueTask = new FutureTask<>(() -> {
                            return computer.compute(key);
                        });                    //1
                        map.put(key, valueTask); //1
                        needRun = true;          //1
                        num++;                   //1
                    }
                }
            }
            if (needRun) {
                valueTask.run();  //2 computer.compute 方法此刻開(kāi)始執(zhí)行
            }
        } else {
            synchronized (Cache5.class) {
                valueTask = map.get(key);
                if (valueTask == null) {
                    valueTask = new FutureTask<>(() -> {
                        return computer.compute(key);
                    });
                    map.put(key, valueTask);
                    num++;
                    needRun = true;
                }
            }
            if (needRun) {
                valueTask.run();
            }
        }

        return valueTask.get();
    }
}

Cache5用了java concurrent包中的FutureTask類(lèi)悯周,F(xiàn)utureTask代表一個(gè)計(jì)算操作可能正在執(zhí)行粒督,也可能已經(jīng)執(zhí)行完畢,F(xiàn)utureTask的get方法會(huì)在計(jì)算任務(wù)完成后的第一時(shí)間返回計(jì)算結(jié)果禽翼。這樣當(dāng)別的線程從map中通過(guò)指定的key值拿到其對(duì)應(yīng)的FutureTask對(duì)象時(shí)屠橄,那么這個(gè)線程最快的獲取這個(gè)key對(duì)應(yīng)的value值的方式不在是重新創(chuàng)建一個(gè)新的計(jì)算任務(wù),而是調(diào)用其對(duì)應(yīng)的FutureTask的get方法來(lái)獲取對(duì)應(yīng)的值捐康。
Cache5的主體結(jié)構(gòu)圖如下:


cache5.png

Cache5 的 //1 部分代表圖中的lockedGetOrPut模塊仇矾,//2部分代表圖中的compute模塊。當(dāng)線程3在0.1到1.1的時(shí)間范圍內(nèi)開(kāi)始計(jì)算key=1對(duì)應(yīng)的value值時(shí)解总,其不會(huì)在重新創(chuàng)建一個(gè)對(duì)key=1的計(jì)算任務(wù)贮匕,而是直接調(diào)用FutureTask的get方法。從而避免了對(duì)相同key值的重復(fù)計(jì)算花枫。至此一個(gè)簡(jiǎn)單的線程安全的緩存系統(tǒng)就設(shè)計(jì)完成了刻盐。

3、guava cache線程安全源碼分析

guava cache中我們先看其LoadingCache的get方法源碼的核心是調(diào)用LocalCache內(nèi)部類(lèi)Segement的get方法:

V get(K key, int hash, CacheLoader<? super K, V> loader) throws ExecutionException {
      checkNotNull(key);
      checkNotNull(loader);
      try {
        if (count != 0) { //3 read-volatile
          // don't call getLiveEntry, which would ignore loading values
          ReferenceEntry<K, V> e = getEntry(key, hash); //1
          if (e != null) {
            long now = map.ticker.read();
            V value = getLiveValue(e, now);
            if (value != null) {
              recordRead(e, now);
              statsCounter.recordHits(1);
              return scheduleRefresh(e, key, hash, value, now, loader);
            }
            ValueReference<K, V> valueReference = e.getValueReference();
            if (valueReference.isLoading()) {
              return waitForLoadingValue(e, key, valueReference);
            }
          }
        }

        // at this point e is either null or expired;
        return lockedGetOrLoad(key, hash, loader); //2
      } catch (ExecutionException ee) {
        Throwable cause = ee.getCause();
        if (cause instanceof Error) {
          throw new ExecutionError((Error) cause);
        } else if (cause instanceof RuntimeException) {
          throw new UncheckedExecutionException(cause);
        }
        throw ee;
      } finally {
        postReadCleanup();
      }
    }

代碼中//1處的getEntry方法通過(guò)key值拿到對(duì)應(yīng)的ReferenceEntry對(duì)象劳翰,而這個(gè)對(duì)象中存儲(chǔ)著所需要的value值敦锌,再看最后的//2處的lockedGetOrLoad方法:

V lockedGetOrLoad(K key, int hash, CacheLoader<? super K, V> loader) throws ExecutionException {
      ReferenceEntry<K, V> e;
      ValueReference<K, V> valueReference = null;
      LoadingValueReference<K, V> loadingValueReference = null;
      boolean createNewEntry = true;

      lock();
      try {
        // re-read ticker once inside the lock
        long now = map.ticker.read();
        preWriteCleanup(now);

        int newCount = this.count - 1;
        AtomicReferenceArray<ReferenceEntry<K, V>> table = this.table; //1
        int index = hash & (table.length() - 1); //1
        ReferenceEntry<K, V> first = table.get(index); //1

        for (e = first; e != null; e = e.getNext()) { //1
          K entryKey = e.getKey();                    //1
          if (e.getHash() == hash                     //1
              && entryKey != null                     //1
              && map.keyEquivalence.equivalent(key, entryKey)) {  //1
            valueReference = e.getValueReference();
            if (valueReference.isLoading()) {
              createNewEntry = false;
            } else {
              V value = valueReference.get();
              if (value == null) {
                enqueueNotification(
                    entryKey, hash, value, valueReference.getWeight(), RemovalCause.COLLECTED);
              } else if (map.isExpired(e, now)) {
                // This is a duplicate check, as preWriteCleanup already purged expired
                // entries, but let's accomodate an incorrect expiration queue.
                enqueueNotification(
                    entryKey, hash, value, valueReference.getWeight(), RemovalCause.EXPIRED);
              } else {
                recordLockedRead(e, now);
                statsCounter.recordHits(1);
                // we were concurrent with loading; don't consider refresh
                return value;
              }

              // immediately reuse invalid entries
              writeQueue.remove(e);
              accessQueue.remove(e);
              this.count = newCount; // write-volatile
            }
            break;
          }
        }

        if (createNewEntry) {
          loadingValueReference = new LoadingValueReference<K, V>(); //2

          if (e == null) {   //2
            e = newEntry(key, hash, first);  //2
            e.setValueReference(loadingValueReference);  //2
            table.set(index, e);         //2
          } else {
            e.setValueReference(loadingValueReference);   //2
          }
        }
      } finally {
        unlock();
        postWriteCleanup();
      }

      if (createNewEntry) {
        try {
          // Synchronizes on the entry to allow failing fast when a recursive load is
          // detected. This may be circumvented when an entry is copied, but will fail fast most
          // of the time.
          synchronized (e) {
            return loadSync(key, hash, loadingValueReference, loader); //3
          }
        } finally {
          statsCounter.recordMisses(1);
        }
      } else {
        // The entry already exists. Wait for loading.
        return waitForLoadingValue(e, key, valueReference);
      }
    }

lockedGetOrLoad 方法中的 //1 處代碼作用和 Segement.get方法中的getEntry方法的作用是一樣的:通過(guò)key值獲取其對(duì)應(yīng)的ReferenceEntry對(duì)象,不過(guò)這段代碼是在鎖中執(zhí)行的佳簸。再結(jié)合Segement.get方法 //3處的對(duì) volatile變量 count的讀取乙墙,這三處代碼的作用其實(shí)和我們?cè)O(shè)計(jì)的Cache3緩存是一樣的,都是盡可能的減少線程持有鎖的DCL方式颖变。再看lockedGetOrLoad方法的 //2處,其作用和Cache5的 //1處 “valueTask == null”成立后的代碼塊是一樣的听想,都是先在指定的key值處放一個(gè)對(duì)應(yīng)的表示計(jì)算過(guò)程對(duì)象腥刹,只不過(guò)在guava cache中這個(gè)對(duì)象是LoadingValueReference。最后我們看lockedGetOrLoad方法 //3處的loadSync代碼汉买,在這個(gè)方法內(nèi)會(huì)執(zhí)行LoadingValueReference.loadFuture方法:

public ListenableFuture<V> loadFuture(K key, CacheLoader<? super K, V> loader) {
      try {
        stopwatch.start();
        V previousValue = oldValue.get();
        if (previousValue == null) {
          V newValue = loader.load(key);   //1
          return set(newValue) ? futureValue : Futures.immediateFuture(newValue);
        }
        ListenableFuture<V> newValue = loader.reload(key, previousValue);
        if (newValue == null) {
          return Futures.immediateFuture(null);
        }
        // To avoid a race, make sure the refreshed value is set into loadingValueReference
        // *before* returning newValue from the cache query.
        return Futures.transform(
            newValue,
            new Function<V, V>() {
              @Override
              public V apply(V newValue) {
                LoadingValueReference.this.set(newValue);
                return newValue;
              }
            });
      } catch (Throwable t) {
        ListenableFuture<V> result = setException(t) ? futureValue : fullyFailedFuture(t);
        if (t instanceof InterruptedException) {
          Thread.currentThread().interrupt();
        }
        return result;
      }
    }

在loadFuture方法的 //1處會(huì)最終調(diào)用我們的在定義guava緩存時(shí)定義的CacheLoader對(duì)象的load方法衔峰。lockedGetOrLoad方法//3處的代碼和Cache5處 //2的代碼作用是一樣的:把真正執(zhí)行計(jì)算key值對(duì)應(yīng)的value值的代碼放在鎖外。這里的鎖是指Segement對(duì)象這個(gè)粗粒度鎖蛙粘。有別于Cache5中任何key值都用同一把鎖(Cache5.class 對(duì)象)垫卤,guava通過(guò)Segement類(lèi)和ReferenceEntry類(lèi)實(shí)現(xiàn)更細(xì)粒度的鎖(具體可參考ConcurrentHashMap設(shè)計(jì))從而實(shí)現(xiàn)不同key值可能用不同的鎖而提高并發(fā)性。


4出牧、 結(jié)束語(yǔ)

通過(guò)對(duì)guava cache的Segement.get方法和 Segement.lockedGetOrLoad方法的分析穴肘,我們發(fā)現(xiàn)其基本上和Cache5的設(shè)計(jì)理念是大同小異的。當(dāng)然設(shè)計(jì)一個(gè)高效的緩存遠(yuǎn)遠(yuǎn)不止Cache5考慮的那么簡(jiǎn)單舔痕,本文也全當(dāng)拋磚引玉梢褐,希望和大家一起討論guava cache的相關(guān)設(shè)計(jì)哲學(xué)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末赵讯,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子耿眉,更是在濱河造成了極大的恐慌边翼,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,525評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鸣剪,死亡現(xiàn)場(chǎng)離奇詭異组底,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)筐骇,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,203評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)债鸡,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人铛纬,你說(shuō)我怎么就攤上這事厌均。” “怎么了告唆?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,862評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵棺弊,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我擒悬,道長(zhǎng)模她,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,728評(píng)論 1 294
  • 正文 為了忘掉前任懂牧,我火速辦了婚禮侈净,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己畜侦,他們只是感情好元扔,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,743評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著夏伊,像睡著了一般摇展。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上溺忧,一...
    開(kāi)封第一講書(shū)人閱讀 51,590評(píng)論 1 305
  • 那天咏连,我揣著相機(jī)與錄音,去河邊找鬼鲁森。 笑死祟滴,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的歌溉。 我是一名探鬼主播垄懂,決...
    沈念sama閱讀 40,330評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼痛垛!你這毒婦竟也來(lái)了草慧?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,244評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤匙头,失蹤者是張志新(化名)和其女友劉穎漫谷,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體蹂析,經(jīng)...
    沈念sama閱讀 45,693評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡舔示,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,885評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了电抚。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片惕稻。...
    茶點(diǎn)故事閱讀 40,001評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖蝙叛,靈堂內(nèi)的尸體忽然破棺而出俺祠,到底是詐尸還是另有隱情,我是刑警寧澤借帘,帶...
    沈念sama閱讀 35,723評(píng)論 5 346
  • 正文 年R本政府宣布锻煌,位于F島的核電站,受9級(jí)特大地震影響姻蚓,放射性物質(zhì)發(fā)生泄漏宋梧。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,343評(píng)論 3 330
  • 文/蒙蒙 一狰挡、第九天 我趴在偏房一處隱蔽的房頂上張望捂龄。 院中可真熱鬧释涛,春花似錦、人聲如沸倦沧。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,919評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)展融。三九已至窖认,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間告希,已是汗流浹背扑浸。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,042評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留燕偶,地道東北人喝噪。 一個(gè)月前我還...
    沈念sama閱讀 48,191評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像指么,于是被迫代替她去往敵國(guó)和親酝惧。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,955評(píng)論 2 355

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