Android Handler機(jī)制1--ThreadLocal

移步Android Handler機(jī)制詳解

本片文章的主要內(nèi)容如下:

  • 1、Java中的ThreadLocal
  • 2、 Android中的ThreadLocal
  • 3、Android 面試中的關(guān)于ThreadLocal的問(wèn)題
  • 4、ThreadLocal的總結(jié)

Java中的ThreadLocal和Android中的ThreadLocal的源代碼是不一樣的

  • 注:基于Android 6.0(6.0.1_r10)/API 23 源碼

使用

public class ThreadLocalTest {
    private static ThreadLocal<String> sLocal = new ThreadLocal<>();
    private static ThreadLocal<Integer> sLocal2 = new ThreadLocal<>();

    public static void main(String[] args) {
        List<Thread> threads = new ArrayList<>();

        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    String threadName = Thread.currentThread().getName();
                    System.out.println("thread name " + threadName);
                    sLocal.set(threadName);
                    sLocal2.set(Integer.parseInt(threadName) + 10);
                    try {
                        Thread.sleep(500 * Integer.parseInt(threadName));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    System.out.println("thread name " + threadName + " ---local " + sLocal.get() + " ---local " + sLocal2.get());
                }
            });
            threads.add(thread);
            thread.setName(i + "");
            thread.start();
        }
    }

}

1 Java中的ThreadLocal

1.1 ThreadLocal的前世今生

  • 早在JDK1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal為解決多線程程序的并發(fā)問(wèn)題提供了一種新的思路,并在JDK1.5開(kāi)始支持泛型。
  • 當(dāng)使用ThreadLocal維護(hù)變量時(shí),ThreadLocal為每個(gè)使用該變量的線程提供獨(dú)立的變量副本,所以每一個(gè)線程都可以獨(dú)立的改變自己的副本,而不會(huì)影響其他線程所對(duì)應(yīng)的副本。
  • 從線程的角度來(lái)看,目標(biāo)變量就像是本地變量,這也是類名中"Local"所要表達(dá)的意思。所以,在Java中編寫線程局部變量的代碼相對(duì)來(lái)說(shuō)要"笨拙"一些,因此造成了線程局部變量沒(méi)有在Java開(kāi)發(fā)者得到很好的普及。

1.2 ThreadLocal類簡(jiǎn)介

1.2.1 Java源碼描述

  • ThreadLocal類用來(lái)提供線程內(nèi)部的局部變量,這種變量在多線程環(huán)境下訪問(wèn)(通過(guò)get或set方法訪問(wèn))時(shí)能保證各個(gè)線程里的變量相對(duì)獨(dú)立于其他線程內(nèi)的變量。ThreadLocal實(shí)例通常來(lái)說(shuō)都是private static 類型,用于關(guān)聯(lián)線程。

  • ThreadLocal設(shè)計(jì)的初衷:提供線程內(nèi)部的局部變量,在本地線程內(nèi)隨時(shí)隨地可取,隔離其他線程。

  • ThreadLocal的作用是提供線程內(nèi)的局部變量,這種局部變量?jī)H僅在線程的生命周期中起作用,減少同一個(gè)線程內(nèi)多個(gè)函數(shù)或者組件一些公共變量的傳遞的復(fù)雜度。

1.2.2 ThreadLocal類結(jié)構(gòu)

5713484-d89c4a36ff46b0be.png

ThreadLocal的結(jié)構(gòu)圖:


5713484-25b7d58cc48916c7.png

1.2.3 ThreadLocal常用的方法

1.2.3.1 set方法

設(shè)置當(dāng)前線程的線程局部變量的值

/**
     * Sets the current thread's copy of this thread-local variable
     * to the specified value.  Most subclasses will have no need to
     * override this method, relying solely on the {@link #initialValue}
     * method to set the values of thread-locals.
     *
     * @param value the value to be stored in the current thread's copy of
     *        this thread-local.
     */
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

代碼流程很清晰:

  • 先拿到保存鍵值對(duì)的ThreadLocalMap對(duì)象實(shí)例map,如果map為空(即第一次調(diào)用的時(shí)候map值為null),則去創(chuàng)建一個(gè)ThreadLocalMap對(duì)象并賦值給map,并把鍵值對(duì)保存在map
  • 我們看到首先是拿到當(dāng)前先線程實(shí)例t,任何將t作為參數(shù)構(gòu)造ThreadLocalMap對(duì)象,為什么需要通過(guò)Threadl來(lái)獲取ThreadLocalMap對(duì)象?
//ThreadLocal.java
    /**
     * Get the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param  t the current thread
     * @return the map
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
  • 我們看到getMap實(shí)現(xiàn)非常直接,就是直接返回Thread對(duì)象的threadLocal字段。Thread類中的ThreadLocalMap字段聲明如下:
//Thread.java
    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

我們總結(jié)一下:

  • ThreadLocal的set(T) 方法中,首先是拿到當(dāng)前線程Thread對(duì)象中的ThreadLocalMap對(duì)象實(shí)例threadLocals,然后再將需要保存的值保存到threadLocals里面。
  • 每個(gè)線程引用的ThreadLocal副本值都是保存在當(dāng)前Thread對(duì)象里面的。存儲(chǔ)結(jié)構(gòu)為ThreadLocalMap類型,ThreadLocalMap保存的類型為ThreadLocal,值為副本值

1.2.3.2 get方法

該方法返回當(dāng)前線程所對(duì)應(yīng)的線程局部變量

/**
     * Returns the value in the current thread's copy of this
     * thread-local variable.  If the variable has no value for the
     * current thread, it is first initialized to the value returned
     * by an invocation of the {@link #initialValue} method.
     *
     * @return the current thread's value of this thread-local
     */
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null)
                return (T)e.value;
        }
        return setInitialValue();
    }
  • 同樣的道理,拿到當(dāng)前線程Thread對(duì)象實(shí)例中保存的ThreadLocalMap對(duì)象map,然后從map中讀取鍵為this(即ThreadLocal類實(shí)例)對(duì)應(yīng)的值。
  • 如果map不是null,直接從map里面讀取就好了,如果map==null,那么我們需要對(duì)當(dāng)前線程Thread對(duì)象實(shí)例中保存ThreadLocalMap對(duì)象new一下。即通過(guò)setInitialValue()方法來(lái)創(chuàng)建,setInitialValue()方法的具體實(shí)現(xiàn)如下:
/**
     * Variant of set() to establish initialValue. Used instead
     * of set() in case user has overridden the set() method.
     *
     * @return the initial value
     */
    private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

代碼很清晰,通過(guò)createMap來(lái)創(chuàng)建ThreadLocalMap對(duì)象,前面set(T)方法里面的ThreadLocalMap也是通過(guò)createMap來(lái)的,我們看看createMap具體實(shí)現(xiàn):

/**
     * Create the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param t the current thread
     * @param firstValue value for the initial entry of the map
     * @param map the map to store.
     */
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

1.2.3.3

remove()方法

         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
  • 直接將當(dāng)前線程局部變量的值刪除,目的是為了減少內(nèi)存的占用,該方法是JDK1.5新增的方法。
  • 需要指出的,當(dāng)線程結(jié)束以后,對(duì)該應(yīng)線程的局部變量將自動(dòng)被垃圾回收,所以顯示調(diào)用該方法清除線程的局部變量并不是必須的操作,但是它可以加速內(nèi)存回收的數(shù)據(jù)。

1.3 內(nèi)部類ThreadLocalMap

/**
     * ThreadLocalMap is a customized hash map suitable only for
     * maintaining thread local values. No operations are exported
     * outside of the ThreadLocal class. The class is package private to
     * allow declaration of fields in class Thread.  To help deal with
     * very large and long-lived usages, the hash table entries use
     * WeakReferences for keys. However, since reference queues are not
     * used, stale entries are guaranteed to be removed only when
     * the table starts running out of space.
     */
    static class ThreadLocalMap {}
  • ThreadLocalMap是一個(gè)適用于維護(hù)線程本地值的自定義哈希映射(hash map),沒(méi)有任何操作可以讓它超出ThreadLocal這個(gè)類的范圍。
  • 該類是私有的,允許在Thread類中聲明字段。為了更好的幫助處理常使用的,hash表?xiàng)l目使用了WeakReferences的鍵。但是,由于不使用引用隊(duì)列,所以,只有在表空間不足的情況下,才會(huì)保留已經(jīng)刪除的條目

1.3.1 存儲(chǔ)結(jié)構(gòu)

  • 通過(guò)注釋我們知道ThreadLocalMap中存儲(chǔ)的是ThreadLocalMap.Entry(后面用Entry代替)對(duì)象。
  • 在ThreadLocalMap中管理的也就是Entry對(duì)象。
  • 首先ThreadlocalMap需要一個(gè)"容器"來(lái)存儲(chǔ)這些Entry對(duì)象,ThreadLocalMap中定義了額Entry數(shù)據(jù)實(shí)例table,用于存儲(chǔ)Entry
/**
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         */
        private Entry[] table;
  • ThreadLocalMap維護(hù)一張哈希表(一個(gè)數(shù)組),表里存儲(chǔ)Entry。既然是哈希表,那肯定會(huì)涉及到加載因子,即當(dāng)表里面存儲(chǔ)的對(duì)象達(dá)到容量的多少百分比的時(shí)候需要擴(kuò)容。
  • ThreadLocalMap中定義了threshold屬性,當(dāng)表里存儲(chǔ)的對(duì)象數(shù)量超過(guò)了threshold就會(huì)擴(kuò)容。
/**
 * The next size value at which to resize.
 */
private int threshold; // Default to 0

/**
 * Set the resize threshold to maintain at worst a 2/3 load factor.
 */
private void setThreshold(int len) {
    threshold = len * 2 / 3;
}

從上面代碼可以看出,加載因子設(shè)置為2/3。即每次容量超過(guò)設(shè)定的len2/3時(shí),需要擴(kuò)容。

1.3.2 存儲(chǔ)Entry對(duì)象

  • hash散列的數(shù)據(jù)在存儲(chǔ)過(guò)程中可能會(huì)發(fā)生碰撞,大家知道HashMap存儲(chǔ)的是一個(gè)Entry鏈,當(dāng)hash發(fā)生沖突后,將新的的Entry存放在鏈表的最前端。但是ThreadLocalMap不一樣,采用的是*index+1作為重散列的hash值寫入
  • 另外有一點(diǎn)需要注意key出現(xiàn)null的原因由于Entry的key是繼承了弱引用,在下一次GC是不管它有沒(méi)有被引用都會(huì)被回收
  • 當(dāng)出現(xiàn)null時(shí),會(huì)調(diào)用replaceStaleEntry()方法循環(huán)尋找相同的key,如果存在,直接替換舊值。如果不存在,則在當(dāng)前位置上重新創(chuàng)新的Entry。
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

看下代碼:

/**
         * Set the value associated with key.
         *
         * @param key the thread local object
         * @param value the value to be set
         */
        //設(shè)置當(dāng)前線程的線程局部變量的值
        private void set(ThreadLocal key, Object value) {

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.

            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);

            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal k = e.get();
                //替換掉舊值
                if (k == key) {
                    e.value = value;
                    return;
                }
                //和HashMap不一樣,因?yàn)镋ntry key 繼承了所引用,所以會(huì)出現(xiàn)key是null的情況!所以會(huì)接著在replaceStaleEntry()重新循環(huán)尋找相同的key
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }


       /**
         * Increment i modulo len.
         */
        private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }
  • 通過(guò)key(ThreadLocal類型)的hashcode來(lái)計(jì)算存儲(chǔ)的索引位置 i 。
  • 如果 i 位置已經(jīng)存儲(chǔ)了對(duì)象,那么就向后挪一個(gè)位置以此類推,直到找到空的位置,再講對(duì)象存放。
  • 在最后還需要判斷一下當(dāng)前的存儲(chǔ)的對(duì)象個(gè)數(shù)是否已經(jīng)超出了反之(threshold的值)大小,如果超出了,需要重新擴(kuò)充并將所有的對(duì)象重新計(jì)算位置(rehash函數(shù)來(lái)實(shí)現(xiàn))。

1.3.2.1 rehash()方法

/**
         * Re-pack and/or re-size the table. First scan the entire
         * table removing stale entries. If this doesn't sufficiently
         * shrink the size of the table, double the table size.
         */
        private void rehash() {
            expungeStaleEntries();

            // Use lower threshold for doubling to avoid hysteresis
            if (size >= threshold - threshold / 4)
                resize();
        }
  • rehash函數(shù)里面先是調(diào)用了expungeStaleEntries()函數(shù),然后再判斷當(dāng)前存儲(chǔ)對(duì)象的小時(shí)是否超出了閥值的3/4。如果超出了,再擴(kuò)容。

  • ThreadLocalMap里面存儲(chǔ)的Entry對(duì)象本質(zhì)上是一個(gè)WeakReference<ThreadLcoal>。也就是說(shuō),ThreadLocalMap里面存儲(chǔ)的對(duì)象本質(zhì)是一個(gè)隊(duì)ThreadLocal的弱引用,該ThreadLocal隨時(shí)可能會(huì)被回收!即導(dǎo)致ThreadLocalMap里面對(duì)應(yīng)的 value的Key是null。我們需要把這樣的Entry清除掉,不要讓他們占坑。

  • expungeStaleEntries函數(shù)就是做這樣的清理工作,清理完后,實(shí)際存儲(chǔ)的對(duì)象數(shù)量自然會(huì)減少,這也不難理解后面的判斷的約束條件為閥值的3/4,而不是閥值的大小。

1.3.2.2 expungeStaleEntries()與expungeStaleEntry()方法

/**
         * Expunge all stale entries in the table.
         */
        private void expungeStaleEntries() {
            Entry[] tab = table;
            int len = tab.length;
            for (int j = 0; j < len; j++) {
                Entry e = tab[j];
                if (e != null && e.get() == null)
                    expungeStaleEntry(j);
            }
        }

expungeStaleEntries()方法很簡(jiǎn)單,主要是遍歷table,然后調(diào)用expungeStaleEntry(),下面我們來(lái)主要講解下這個(gè)函數(shù)expungeStaleEntry()函數(shù)。

1.3.2.3 expungeStaleEntry()方法

ThreadLocalMap中的expungeStaleEntry(int)方法的可能被調(diào)用的處理有:

expungeStaleEntry的調(diào)用.png

通過(guò)上面的圖,不難看出,這個(gè)方法在ThreadLocal的set、get、remove時(shí)都會(huì)被調(diào)用

/**
         * Expunge a stale entry by rehashing any possibly colliding entries
         * lying between staleSlot and the next null slot.  This also expunges
         * any other stale entries encountered before the trailing null.  See
         * Knuth, Section 6.4
         *
         * @param staleSlot index of slot known to have null key
         * @return the index of the next null slot after staleSlot
         * (all between staleSlot and this slot will have been checked
         * for expunging).
         */
        private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // expunge entry at staleSlot
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // Rehash until we encounter null
            Entry e;
            int i;
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal k = e.get();
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) {
                        tab[i] = null;

                        // Unlike Knuth 6.4 Algorithm R, we must scan until
                        // null because multiple entries could have been stale.
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }
  • 看出先清理指定的Entry,再遍歷,如果發(fā)現(xiàn)有Entry的key為null,就清理。
  • Key==null,也就是ThreadLocal對(duì)象是null。所以當(dāng)程序中,將ThreadLocal對(duì)象設(shè)置為null,在該線程繼續(xù)執(zhí)行時(shí),如果執(zhí)行另一個(gè)ThreadLocal時(shí),就會(huì)觸發(fā)該方法。就有可能清理掉Key是null的那個(gè)ThreadLocal對(duì)應(yīng)的值。
  • 所以說(shuō)expungStaleEntry()方法清除線程ThreadLocalMap里面所有key為null的value。

1.3.3 獲取Entry對(duì)象getEntry()

/**
         * Get the entry associated with key.  This method
         * itself handles only the fast path: a direct hit of existing
         * key. It otherwise relays to getEntryAfterMiss.  This is
         * designed to maximize performance for direct hits, in part
         * by making this method readily inlinable.
         *
         * @param  key the thread local object
         * @return the entry associated with key, or null if no such
         */
        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);
        }
  • getEntry()方法很簡(jiǎn)單,直接通過(guò)哈希碼計(jì)算位置 i ,然后把哈希表對(duì)應(yīng)的 i 的位置Entry對(duì)象拿出來(lái)。
  • 如果對(duì)應(yīng)位置的值為null,這就存在如下幾種可能。
  • key 對(duì)應(yīng)的值為null
  • 由于位置沖突,key對(duì)應(yīng)的值存儲(chǔ)的位置并不是 i 位置上,即 i 位置上的null并不屬于 key 值

因此,需要一個(gè)函數(shù)去確認(rèn)key對(duì)應(yīng)的value的值,即getEntryAfterMiss()方法

1.3.3.1 getEntryAfterMiss()函數(shù)

/**
         * Version of getEntry method for use when key is not found in
         * its direct hash slot.
         *
         * @param  key the thread local object
         * @param  i the table index for key's hash code
         * @param  e the entry at table[i]
         * @return the entry associated with key, or null if no such
         */
        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;
        }
  • 第一步,從ThreadLocal的直接索引位置(通過(guò)ThreadLocal.threadLocalHashCode&(len-1)運(yùn)算得到)獲取Entry e,如果e不為null,并且key相同則返回e。
  • 第二步,如果e為null或者key不一致則向下一個(gè)位置查詢,如果下一個(gè)位置的key和當(dāng)前需要查詢的key相等,則返回應(yīng)對(duì)應(yīng)的Entry,否則,如果key值為null,則擦除該位置的Entry,否則繼續(xù)向一個(gè)位置查詢。

ThreadLocalMap整個(gè)get過(guò)程中遇到的key為null的Entry都被會(huì)擦除,那么value的上一個(gè)引用鏈就不存在了,自然會(huì)被回收。set也有類似的操作。這樣在你每次調(diào)用ThreadLocal的get方法去獲取值或者調(diào)用set方法去設(shè)置值的時(shí)候,都會(huì)去做這個(gè)操作,防止內(nèi)存泄露,當(dāng)然最保險(xiǎn)的還是通過(guò)手動(dòng)調(diào)用remove方法直接移除

1.3.4 ThreadLocalMap.Entry對(duì)象

前面很多地方都在說(shuō)ThreadLocalMap里面存儲(chǔ)的是ThreadLocalMap.Entry對(duì)象,那么ThreadLocalMap.Entry獨(dú)享到底是如何存儲(chǔ)鍵值對(duì)的?同時(shí)有是如何做到的對(duì)ThreadLocal對(duì)象進(jìn)行弱引用?

/**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal k, Object v) {
                super(k);
                value = v;
            }
        }
  • 從源碼的繼承關(guān)系可以看到,Entry是繼承WeakReference<ThreadLocal>。即Entry本質(zhì)上就是WeakReference<ThreadLocal>```
  • Entry就是一個(gè)弱引用,具體講,Entry實(shí)例就是對(duì)ThreadLocal某個(gè)實(shí)例的弱引用。只不過(guò),Entry同時(shí)還保存了value


    5713484-bc893ac35da6fd7b.png
5713484-fd837cb2b37254f1.png

1.4 總結(jié)

  • ThreadLocal是解決線程安全的一個(gè)很好的思路,它通過(guò)為每個(gè)線程提供了一個(gè)獨(dú)立的變量副本解決了額變量并發(fā)訪問(wèn)的沖突問(wèn)題。
  • 在很多情況下,ThreadLocal比直接使用synchronized同步機(jī)制解決線程安全問(wèn)題更簡(jiǎn)單,更方便,且結(jié)果程序擁有更高的并發(fā)性。
  • ThreadLocal和synchronize用一句話總結(jié)就是一個(gè)用存儲(chǔ)拷貝進(jìn)行空間換時(shí)間,一個(gè)是用鎖機(jī)制進(jìn)行時(shí)間換空間。
    其實(shí)補(bǔ)充知識(shí):
  • ThreadLocal官方建議已定義成private static 的這樣讓Thread不那么容易被回收
  • 真正涉及共享變量的時(shí)候ThreadLocal是解決不了的。它最多是當(dāng)每個(gè)線程都需要這個(gè)實(shí)例,如一個(gè)打開(kāi)數(shù)據(jù)庫(kù)的對(duì)象時(shí),保證每個(gè)線程拿到一個(gè)進(jìn)而去操作,互不不影響。但是這個(gè)對(duì)象其實(shí)并不是共享的。

4 ThreadLocal會(huì)存在內(nèi)存泄露嗎

ThreadLocal實(shí)例的弱引用,當(dāng)前thread被銷毀時(shí),ThreadLocal也會(huì)隨著銷毀被GC回收。

參考

Android Handler機(jī)制2之ThreadLocal
ThreadLocal

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,546評(píng)論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,570評(píng)論 3 418
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 176,505評(píng)論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 63,017評(píng)論 1 313
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,786評(píng)論 6 410
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 55,219評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,287評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 42,438評(píng)論 0 288
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,971評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,796評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,995評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,540評(píng)論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,230評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 34,662評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 35,918評(píng)論 1 286
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,697評(píng)論 3 392
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,991評(píng)論 2 374

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