聊一聊Spring中的線程安全性

Spring與線程安全


Spring作為一個(gè)IOC/DI容器见转,幫助我們管理了許許多多的“bean”命雀。但其實(shí),Spring并沒有保證這些對(duì)象的線程安全斩箫,需要由開發(fā)者自己編寫解決線程安全問題的代碼吏砂。

Spring對(duì)每個(gè)bean提供了一個(gè)scope屬性來表示該bean的作用域。它是bean的生命周期乘客。例如狐血,一個(gè)scope為singleton的bean,在第一次被注入時(shí)易核,會(huì)創(chuàng)建為一個(gè)單例對(duì)象匈织,該對(duì)象會(huì)一直被復(fù)用到應(yīng)用結(jié)束。

  • singleton:默認(rèn)的scope,每個(gè)scope為singleton的bean都會(huì)被定義為一個(gè)單例對(duì)象缀匕,該對(duì)象的生命周期是與Spring IOC容器一致的(但在第一次被注入時(shí)才會(huì)創(chuàng)建)纳决。

  • prototype:bean被定義為在每次注入時(shí)都會(huì)創(chuàng)建一個(gè)新的對(duì)象。

  • request:bean被定義為在每個(gè)HTTP請(qǐng)求中創(chuàng)建一個(gè)單例對(duì)象弦追,也就是說在單個(gè)請(qǐng)求中都會(huì)復(fù)用這一個(gè)單例對(duì)象岳链。

  • session:bean被定義為在一個(gè)session的生命周期內(nèi)創(chuàng)建一個(gè)單例對(duì)象。

  • application:bean被定義為在ServletContext的生命周期中復(fù)用一個(gè)單例對(duì)象劲件。

  • websocket:bean被定義為在websocket的生命周期中復(fù)用一個(gè)單例對(duì)象掸哑。

我們交由Spring管理的大多數(shù)對(duì)象其實(shí)都是一些無狀態(tài)的對(duì)象,這種不會(huì)因?yàn)槎嗑€程而導(dǎo)致狀態(tài)被破壞的對(duì)象很適合Spring的默認(rèn)scope零远,每個(gè)單例的無狀態(tài)對(duì)象都是線程安全的(也可以說只要是無狀態(tài)的對(duì)象苗分,不管單例多例都是線程安全的,不過單例畢竟節(jié)省了不斷創(chuàng)建對(duì)象與GC的開銷)牵辣。

無狀態(tài)的對(duì)象即是自身沒有狀態(tài)的對(duì)象摔癣,自然也就不會(huì)因?yàn)槎鄠€(gè)線程的交替調(diào)度而破壞自身狀態(tài)導(dǎo)致線程安全問題。無狀態(tài)對(duì)象包括我們經(jīng)常使用的DO纬向、DTO择浊、VO這些只作為數(shù)據(jù)的實(shí)體模型的貧血對(duì)象,還有Service逾条、DAO和Controller琢岩,這些對(duì)象并沒有自己的狀態(tài),它們只是用來執(zhí)行某些操作的师脂。例如担孔,每個(gè)DAO提供的函數(shù)都只是對(duì)數(shù)據(jù)庫的CRUD,而且每個(gè)數(shù)據(jù)庫Connection都作為函數(shù)的局部變量(局部變量是在用戶棧中的吃警,而且用戶棧本身就是線程私有的內(nèi)存區(qū)域糕篇,所以不存在線程安全問題),用完即關(guān)(或交還給連接池)酌心。

有人可能會(huì)認(rèn)為拌消,我使用request作用域不就可以避免每個(gè)請(qǐng)求之間的安全問題了嗎?這是完全錯(cuò)誤的安券,因?yàn)镃ontroller默認(rèn)是單例的墩崩,一個(gè)HTTP請(qǐng)求是會(huì)被多個(gè)線程執(zhí)行的,這就又回到了線程的安全問題完疫。當(dāng)然泰鸡,你也可以把Controller的scope改成prototype债蓝,實(shí)際上Struts2就是這么做的壳鹤,但有一點(diǎn)要注意,Spring MVC對(duì)請(qǐng)求的攔截粒度是基于每個(gè)方法的饰迹,而Struts2是基于每個(gè)類的芳誓,所以把Controller設(shè)為多例將會(huì)頻繁的創(chuàng)建與回收對(duì)象余舶,嚴(yán)重影響到了性能。

通過閱讀上文其實(shí)已經(jīng)說的很清楚了锹淌,Spring根本就沒有對(duì)bean的多線程安全問題做出任何保證與措施匿值。對(duì)于每個(gè)bean的線程安全問題,根本原因是每個(gè)bean自身的設(shè)計(jì)赂摆。不要在bean中聲明任何有狀態(tài)的實(shí)例變量或類變量挟憔,如果必須如此,那么就使用ThreadLocal把變量變?yōu)榫€程私有的烟号,如果bean的實(shí)例變量或類變量需要在多個(gè)線程之間共享绊谭,那么就只能使用synchronized、lock汪拥、CAS等這些實(shí)現(xiàn)線程同步的方法了达传。

下面將通過解析ThreadLocal的源碼來了解它的實(shí)現(xiàn)與作用,ThreadLocal是一個(gè)很好用的工具類迫筑,它在某些情況下解決了線程安全問題(在變量不需要被多個(gè)線程共享時(shí))宪赶。

本文作者為SylvanasSun(sylvanas.sun@gmail.com),首發(fā)于SylvanasSun’s Blog脯燃。
原文鏈接:https://sylvanassun.github.io/2017/11/06/2017-11-06-spring_and_thread-safe/
(轉(zhuǎn)載請(qǐng)務(wù)必保留本段聲明搂妻,并且保留超鏈接熊咽。)

ThreadLocal


ThreadLocal是一個(gè)為線程提供線程局部變量的工具類沸版。它的思想也十分簡單赴穗,就是為線程提供一個(gè)線程私有的變量副本剧浸,這樣多個(gè)線程都可以隨意更改自己線程局部的變量诅愚,不會(huì)影響到其他線程垃瞧。不過需要注意的是酝锅,ThreadLocal提供的只是一個(gè)淺拷貝到涂,如果變量是一個(gè)引用類型懈糯,那么就要考慮它內(nèi)部的狀態(tài)是否會(huì)被改變涤妒,想要解決這個(gè)問題可以通過重寫ThreadLocal的initialValue()函數(shù)來自己實(shí)現(xiàn)深拷貝,建議在使用ThreadLocal時(shí)一開始就重寫該函數(shù)赚哗。

ThreadLocal與像synchronized這樣的鎖機(jī)制是不同的她紫。首先,它們的應(yīng)用場景與實(shí)現(xiàn)思路就不一樣屿储,鎖更強(qiáng)調(diào)的是如何同步多個(gè)線程去正確地共享一個(gè)變量贿讹,ThreadLocal則是為了解決同一個(gè)變量如何不被多個(gè)線程共享。從性能開銷的角度上來講够掠,如果鎖機(jī)制是用時(shí)間換空間的話民褂,那么ThreadLocal就是用空間換時(shí)間。

ThreadLocal中含有一個(gè)叫做ThreadLocalMap的內(nèi)部類,該類為一個(gè)采用線性探測(cè)法實(shí)現(xiàn)的HashMap赊堪。它的key為ThreadLocal對(duì)象而且還使用了WeakReference面殖,ThreadLocalMap正是用來存儲(chǔ)變量副本的。

    /**
     * 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 {
        /**
         * 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;
            }
        }
        ....
    }   

ThreadLocal中只含有三個(gè)成員變量哭廉,這三個(gè)變量都是與ThreadLocalMap的hash策略相關(guān)的脊僚。

    /**
     * ThreadLocals rely on per-thread linear-probe hash maps attached
     * to each thread (Thread.threadLocals and
     * inheritableThreadLocals).  The ThreadLocal objects act as keys,
     * searched via threadLocalHashCode.  This is a custom hash code
     * (useful only within ThreadLocalMaps) that eliminates collisions
     * in the common case where consecutively constructed ThreadLocals
     * are used by the same threads, while remaining well-behaved in
     * less common cases.
     */
    private final int threadLocalHashCode = nextHashCode();

    /**
     * The next hash code to be given out. Updated atomically. Starts at
     * zero.
     */
    private static AtomicInteger nextHashCode =
        new AtomicInteger();

    /**
     * The difference between successively generated hash codes - turns
     * implicit sequential thread-local IDs into near-optimally spread
     * multiplicative hash values for power-of-two-sized tables.
     */
    private static final int HASH_INCREMENT = 0x61c88647;

    /**
     * Returns the next hash code.
     */
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

唯一的實(shí)例變量threadLocalHashCode是用來進(jìn)行尋址的hashcode,它由函數(shù)nextHashCode()生成遵绰,該函數(shù)簡單地通過一個(gè)增量HASH_INCREMENT來生成hashcode辽幌。至于為什么這個(gè)增量為0x61c88647,主要是因?yàn)門hreadLocalMap的初始大小為16椿访,每次擴(kuò)容都會(huì)為原來的2倍舶衬,這樣它的容量永遠(yuǎn)為2的n次方,該增量選為0x61c88647也是為了盡可能均勻地分布赎离,減少碰撞沖突逛犹。

        /**
         * The initial capacity -- MUST be a power of two.
         */
        private static final int INITIAL_CAPACITY = 16; 

        /**
         * Construct a new map initially containing (firstKey, firstValue).
         * ThreadLocalMaps are constructed lazily, so we only create
         * one when we have at least one entry to put in it.
         */
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }

要獲得當(dāng)前線程私有的變量副本需要調(diào)用get()函數(shù)。首先梁剔,它會(huì)調(diào)用getMap()函數(shù)去獲得當(dāng)前線程的ThreadLocalMap虽画,這個(gè)函數(shù)需要接收當(dāng)前線程的實(shí)例作為參數(shù)。如果得到的ThreadLocalMap為null荣病,那么就去調(diào)用setInitialValue()函數(shù)來進(jìn)行初始化码撰,如果不為null,就通過map來獲得變量副本并返回个盆。

setInitialValue()函數(shù)會(huì)去先調(diào)用initialValue()函數(shù)來生成初始值脖岛,該函數(shù)默認(rèn)返回null,我們可以通過重寫這個(gè)函數(shù)來返回我們想要在ThreadLocal中維護(hù)的變量颊亮。之后柴梆,去調(diào)用getMap()函數(shù)獲得ThreadLocalMap,如果該map已經(jīng)存在终惑,那么就用新獲得value去覆蓋舊值绍在,否則就調(diào)用createMap()函數(shù)來創(chuàng)建新的map。

    /**
     * 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) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
    
    /**
     * 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;
    }
    
    protected T initialValue() {
        return null;
    }   

ThreadLocal的set()與remove()函數(shù)要比get()的實(shí)現(xiàn)還要簡單雹有,都只是通過getMap()來獲得ThreadLocalMap然后對(duì)其進(jìn)行操作偿渡。

    /**
     * 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);
    }

    /**
     * Removes the current thread's value for this thread-local
     * variable.  If this thread-local variable is subsequently
     * {@linkplain #get read} by the current thread, its value will be
     * reinitialized by invoking its {@link #initialValue} method,
     * unless its value is {@linkplain #set set} by the current thread
     * in the interim.  This may result in multiple invocations of the
     * {@code initialValue} method in the current thread.
     *
     * @since 1.5
     */
     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

getMap()函數(shù)與createMap()函數(shù)的實(shí)現(xiàn)也十分簡單,但是通過觀察這兩個(gè)函數(shù)可以發(fā)現(xiàn)一個(gè)秘密:ThreadLocalMap是存放在Thread中的霸奕。

    /**
     * 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;
    }

    /**
     * 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
     */
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
    
    // Thread中的源碼
    
    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

    /*
     * InheritableThreadLocal values pertaining to this thread. This map is
     * maintained by the InheritableThreadLocal class.
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;  

仔細(xì)想想其實(shí)就能夠理解這種設(shè)計(jì)的思想溜宽。有一種普遍的方法是通過一個(gè)全局的線程安全的Map來存儲(chǔ)各個(gè)線程的變量副本,但是這種做法已經(jīng)完全違背了ThreadLocal的本意质帅,設(shè)計(jì)ThreadLocal的初衷就是為了避免多個(gè)線程去并發(fā)訪問同一個(gè)對(duì)象适揉,盡管它是線程安全的合武。而在每個(gè)Thread中存放與它關(guān)聯(lián)的ThreadLocalMap是完全符合ThreadLocal的思想的,當(dāng)想要對(duì)線程局部變量進(jìn)行操作時(shí)涡扼,只需要把Thread作為key來獲得Thread中的ThreadLocalMap即可。這種設(shè)計(jì)相比采用一個(gè)全局Map的方法會(huì)多占用很多內(nèi)存空間盟庞,但也因此不需要額外的采取鎖等線程同步方法而節(jié)省了時(shí)間上的消耗吃沪。

ThreadLocal中的內(nèi)存泄漏


我們要考慮一種會(huì)發(fā)生內(nèi)存泄漏的情況,如果ThreadLocal被設(shè)置為null后什猖,而且沒有任何強(qiáng)引用指向它票彪,根據(jù)垃圾回收的可達(dá)性分析算法,ThreadLocal將會(huì)被回收不狮。這樣一來降铸,ThreadLocalMap中就會(huì)含有key為null的Entry,而且ThreadLocalMap是在Thread中的摇零,只要線程遲遲不結(jié)束推掸,這些無法訪問到的value會(huì)形成內(nèi)存泄漏。為了解決這個(gè)問題驻仅,ThreadLocalMap中的getEntry()谅畅、set()和remove()函數(shù)都會(huì)清理key為null的Entry,以下面的getEntry()函數(shù)的源碼為例噪服。

        /**
         * 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);
        }

        /**
         * 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;
            
            // 清理key為null的Entry
            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;
        }

在上文中我們發(fā)現(xiàn)了ThreadLocalMap的key是一個(gè)弱引用毡泻,那么為什么使用弱引用呢?使用強(qiáng)引用key與弱引用key的差別如下:

  • 強(qiáng)引用key:ThreadLocal被設(shè)置為null粘优,由于ThreadLocalMap持有ThreadLocal的強(qiáng)引用仇味,如果不手動(dòng)刪除,那么ThreadLocal將不會(huì)回收雹顺,產(chǎn)生內(nèi)存泄漏丹墨。

  • 弱引用key:ThreadLocal被設(shè)置為null,由于ThreadLocalMap持有ThreadLocal的弱引用嬉愧,即便不手動(dòng)刪除带到,ThreadLocal仍會(huì)被回收,ThreadLocalMap在之后調(diào)用set()英染、getEntry()和remove()函數(shù)時(shí)會(huì)清除所有key為null的Entry揽惹。

但要注意的是,ThreadLocalMap僅僅含有這些被動(dòng)措施來補(bǔ)救內(nèi)存泄漏問題四康。如果你在之后沒有調(diào)用ThreadLocalMap的set()搪搏、getEntry()和remove()函數(shù)的話,那么仍然會(huì)存在內(nèi)存泄漏問題闪金。

在使用線程池的情況下疯溺,如果不及時(shí)進(jìn)行清理论颅,內(nèi)存泄漏問題事小,甚至還會(huì)產(chǎn)生程序邏輯上的問題囱嫩。所以恃疯,為了安全地使用ThreadLocal,必須要像每次使用完鎖就解鎖一樣墨闲,在每次使用完ThreadLocal后都要調(diào)用remove()來清理無用的Entry今妄。

參考文獻(xiàn)


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市鸳碧,隨后出現(xiàn)的幾起案子盾鳞,更是在濱河造成了極大的恐慌,老刑警劉巖瞻离,帶你破解...
    沈念sama閱讀 216,324評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件腾仅,死亡現(xiàn)場離奇詭異,居然都是意外死亡套利,警方通過查閱死者的電腦和手機(jī)推励,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來肉迫,“玉大人吹艇,你說我怎么就攤上這事“悍鳎” “怎么了受神?”我有些...
    開封第一講書人閱讀 162,328評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長格侯。 經(jīng)常有香客問我鼻听,道長,這世上最難降的妖魔是什么联四? 我笑而不...
    開封第一講書人閱讀 58,147評(píng)論 1 292
  • 正文 為了忘掉前任撑碴,我火速辦了婚禮,結(jié)果婚禮上朝墩,老公的妹妹穿的比我還像新娘醉拓。我一直安慰自己,他們只是感情好收苏,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,160評(píng)論 6 388
  • 文/花漫 我一把揭開白布亿卤。 她就那樣靜靜地躺著,像睡著了一般鹿霸。 火紅的嫁衣襯著肌膚如雪排吴。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,115評(píng)論 1 296
  • 那天懦鼠,我揣著相機(jī)與錄音钻哩,去河邊找鬼屹堰。 笑死,一個(gè)胖子當(dāng)著我的面吹牛街氢,可吹牛的內(nèi)容都是我干的扯键。 我是一名探鬼主播,決...
    沈念sama閱讀 40,025評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼珊肃,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼荣刑!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起近范,我...
    開封第一講書人閱讀 38,867評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎延蟹,沒想到半個(gè)月后评矩,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,307評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡阱飘,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,528評(píng)論 2 332
  • 正文 我和宋清朗相戀三年斥杜,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片沥匈。...
    茶點(diǎn)故事閱讀 39,688評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡蔗喂,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出高帖,到底是詐尸還是另有隱情缰儿,我是刑警寧澤,帶...
    沈念sama閱讀 35,409評(píng)論 5 343
  • 正文 年R本政府宣布散址,位于F島的核電站乖阵,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏预麸。R本人自食惡果不足惜瞪浸,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,001評(píng)論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望吏祸。 院中可真熱鬧对蒲,春花似錦、人聲如沸贡翘。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽鸣驱。三九已至含滴,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間丐巫,已是汗流浹背谈况。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評(píng)論 1 268
  • 我被黑心中介騙來泰國打工勺美, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人碑韵。 一個(gè)月前我還...
    沈念sama閱讀 47,685評(píng)論 2 368
  • 正文 我出身青樓赡茸,卻偏偏與公主長得像,于是被迫代替她去往敵國和親祝闻。 傳聞我的和親對(duì)象是個(gè)殘疾皇子占卧,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,573評(píng)論 2 353

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

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn)联喘,斷路器华蜒,智...
    卡卡羅2017閱讀 134,651評(píng)論 18 139
  • 前言 ThreadLocal很多同學(xué)都搞不懂是什么東西,可以用來干嘛豁遭。但面試時(shí)卻又經(jīng)常問到叭喜,所以這次我和大家一起學(xué)...
    liangzzz閱讀 12,445評(píng)論 14 228
  • 從三月份找實(shí)習(xí)到現(xiàn)在,面了一些公司蓖谢,掛了不少捂蕴,但最終還是拿到小米、百度闪幽、阿里啥辨、京東、新浪盯腌、CVTE溉知、樂視家的研發(fā)崗...
    時(shí)芥藍(lán)閱讀 42,239評(píng)論 11 349
  • Spring Boot 參考指南 介紹 轉(zhuǎn)載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 46,803評(píng)論 6 342
  • 為了安撫不吃早飯胃會(huì)給予我的煎熬,在辦公桌落座后腕够,糾結(jié)了一下就走到了樓下面館着倾,饞蟲又如實(shí)來報(bào)道,偷偷的給自己要了一...
    AA晨曉曦閱讀 120評(píng)論 0 0