1. 程式人生 > >深入理解 ThreadLocal

深入理解 ThreadLocal

前言

上篇文章 深入理解 Handler 訊息機制 中提到了獲取執行緒的 Looper 是通過 ThreadLocal 來實現的:

public static @Nullable Looper myLooper() {
    return sThreadLocal.get();
}

每個執行緒都有自己的 Looper,它們之間不應該有任何交集,互不干擾,我們把這種變數稱為 執行緒區域性變數 。而 ThreadLocal 的作用正是儲存執行緒區域性變數,每個執行緒中儲存的都是獨立存在的資料副本。如果你還是不太理解,看一下下面這個簡單的例子:

public static void main(String[] args) throws InterruptedException {

    ThreadLocal<Boolean> threadLocal = new ThreadLocal<Boolean>();
    threadLocal.set(true);

    Thread t1 = new Thread(() -> {
        threadLocal.set(false);
        System.out.println(threadLocal.get());
    });

    Thread t2 = new Thread(() -> {
        System.out.println(threadLocal.get());
    });

    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(threadLocal.get());
}

執行結果是:

false
null
true

可以看到,我們在不同的執行緒中呼叫同一個 ThreadLocal 的 get() 方法,獲得的值是不同的,看起來就像 ThreadLocal 為每個執行緒分別儲存了不同的值。那麼這到底是如何實現的呢?一起來看看原始碼吧。

以下原始碼基於 JDK 1.8 , 相關檔案:

Thread.java

ThreadLocal.java

ThreadLocal

首先 ThreadLocal 是一個泛型類,public class ThreadLocal<T>,支援儲存各種資料型別。它對外暴露的方法很少,基本就 get()set()remove()

這三個。下面依次來看一下。

set()

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t); // 獲取當前執行緒的 ThreadLocalMap
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value); // 建立 ThreadLocalMap
}

這裡出現了一個新東西 ThreadLocalMap,暫且就把他當做一個普通的 Map。從 map.set(this, value)

可以看出來這個 map 的鍵是 ThreadLocal 物件,值是要儲存的 value 物件。其實看到這,ThreadLocal 的原理你應該基本都明白了。

每一個 Thread 都有一個 ThreadLocalMap ,這個 Map 以 ThreadLocal 物件為鍵,以要儲存的執行緒區域性變數為值。這樣就做到了為每個執行緒儲存不同的副本。

首先通過 getMap() 函式獲取當前執行緒的 ThreadLocalMap :

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

原來 Thread 還有這麼一個變數 threadLocals

/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class.
*
* 儲存執行緒私有變數,由 ThreadLocal 進行管理
*/
ThreadLocal.ThreadLocalMap threadLocals = null;

預設為 null,所以第一次呼叫時返回 null ,呼叫 createMap(t, value) 進行初始化:

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

get()

set() 方法是向 ThreadLocalMap 中插值,那麼 get() 就是在 ThreadLocalMap 中取值了。

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t); // 獲取當前執行緒的 ThreadLocalMap
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result; // 找到值,直接返回
        }
    }
    return setInitialValue(); // 設定初始值
}

首先獲取 ThreadLocalMap,在 Map 中尋找當前 ThreadLocal 對應的 value 值。如果 Map 為空,或者沒有找到 value,則通過 setInitialValue() 函式設定初始值。

private T setInitialValue() {
    T value = initialValue(); // 為 null
    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;
}

setInitialValue()set() 邏輯基本一致,只不過 value 是 null 而已。這也解釋了文章開頭的例子會輸出 null。當然,在 ThreadLocal 的子類中,我們可以通過重寫 setInitialValue() 來提供其他預設值。

remove()

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

remove() 就更簡單了,根據鍵直接移除對應條目。

看到這裡,ThreadLocal 的原理好像就說完了,其實不然。ThreadLocalMap 是什麼樣的一個雜湊表呢?它是如何解決雜湊衝突的?它是如何新增,獲取和刪除元素的?可能會導致記憶體洩露嗎?

其實 ThreadLocalMap 才是 ThreadLocal 的核心。ThreadLocal 僅僅只是提供給開發者的一個工具而已,就像 Handler 一樣。帶著上面的問題,來閱讀 ThreadLocalMap 的原始碼,體會 JDK 工程師的鬼斧神工。

ThreadLocalMap

Entry

ThreadLocalMap 是 ThreadLocal 的靜態內部類,它沒有直接使用 HashMap,而是一個自定義的雜湊表,使用陣列實現,陣列元素是 Entry

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

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

Entry 類繼承了 WeakReference<ThreadLocal<?>>,我們可以把它看成是一個鍵值對。鍵是當前的 ThreadLocal 物件,值是儲存的物件。注意 ThreadLocal 物件的引用是弱引用,值物件 value 的引用是強引用。ThreadLocal 使用弱引用其實很好理解,原始碼註釋中也告訴了我們答案:

To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys

Thread 持有 ThreadLocalMap 的強引用,ThreadLocalMap 中的 Entry 的鍵是 ThreadLocal 引用。如果執行緒長期存活或者使用了執行緒池,而 ThreadLocal 在外部又沒有任何強引用了,這種情況下如果 ThreadLocalMap 的鍵仍然使用強引用 ThreadLocal,就會導致 ThreadLocal 永遠無法被垃圾回收,造成記憶體洩露。

圖片來源:https://www.jianshu.com/p/a1cd61fa22da

那麼,使用弱引用是不是就萬無一失了呢?答案也是否定的。同樣是上面說到使用情況,執行緒長期存活,由於 Entry 的 key 使用了弱引用,當 ThreadLocal 不存在外部強引用時,可以在 GC 中被回收。但是根據可達性分析演算法,仍然存在著這麼一個引用鏈:

Current Thread -> ThreadLocalMap -> Entry -> value

key 已經被回收了,此時 key == null。那麼,value 呢?如果執行緒長期存在,這個針對 value 的強引用也會一直存在,外部是否對 value 指向的物件還存在其他強引用也不得而知。所以這裡還是有機率發生記憶體洩漏的。就算我們不知道外部的引用情況,但至少在這裡應該是可以切斷 value 引用的。

所以,為了解決可能存在的記憶體洩露問題,我們有必要對於這種 key 已經被 GC 的過期 Entry 進行處理,手動釋放 value 引用。當然,JDK 中已經為我們處理了,而且處理的十分巧妙。下面就來看看 ThreadLocalMap 的原始碼。

建構函式

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

table 是儲存 Entry 的陣列,初始容量 INITIAL_CAPACITY 是 16。

firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1) 是 ThreadLocalMap 計算雜湊的方式。&(2^n-1) 其實等同於 % 2^n,位運算效率更高。

threadLocalHashCode 是如何計算的呢?看下面的程式碼:

private static final int HASH_INCREMENT = 0x61c88647;

private static AtomicInteger nextHashCode = new AtomicInteger();

private final int threadLocalHashCode = nextHashCode();

private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

0x61c88647 是一個增量,每次取雜湊都要再加上這個數字。又是一個神奇的數字,讓我想到了 Integer 原始碼中的 52429 這個數字,見 走進 JDK 之 Integer 。0x61c88647 背後肯定也有它的數學原理,總之肯定是為了效率。

原理就不去探究了,其實我也不知道是啥原理。不過我們可以試用一下,看看效果如何。按照上面的方式來計算連續幾個元素的雜湊值,也就是在 Entry 陣列中的位置。程式碼如下:

public class Test {

    private static final int INITIAL_CAPACITY = 16;
    private static final int HASH_INCREMENT = 0x61c88647;
    private static AtomicInteger nextHashCode = new AtomicInteger();

    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

    private static int hash() {
        return nextHashCode() & (INITIAL_CAPACITY - 1);
    }

    public static void main(String[] args) {

        for (int i = 0; i < 8; i++) {
            System.out.println(hash());
        }
    }
}

運算結果如下:

0
7
14
5
12
3
10
1

計算結果分佈還是比較均勻的。既然是雜湊表,肯定就會存在雜湊衝突的情況。那麼,ThreadLocalMap 是如何解決雜湊衝突呢?很簡單,看一下 nextIndex() 方法。

private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}

在不超過 len 的情況下直接加 1,否則置 0。其實這樣又可以看成一個環形陣列。

接下來看看 ThreadLocalMap 的資料是如何儲存的。

set()

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); // 當前 key 的雜湊,即在陣列 table 中的位置

    for (Entry e = tab[i];
        e != null; // 迴圈直到碰到空 Entry
        e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

        if (k == key) { // 更新 key 對應的值
            e.value = value;
            return;
        }

        if (k == null) { // 替代過期 entry
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}
  1. 通過 key.threadLocalHashCode & (len-1) 計算出初始的雜湊值
  2. 不斷呼叫 nextIndex() 直到找到空 Entry
  3. 在第二步遍歷過程中的每個元素,要處理兩種情況:

    (1). k == key,說明當前 key 已存在,直接更新值即可,直接返回

    (2). k == null, 注意這裡的前置條件是 entry != null。說明遇到過期 Entry,直接替換
  4. 不屬於 3 中的兩種情況,則將引數中的鍵值對插入空 Entry 處
  5. cleanSomeSlots()/rehash()

先來看看第三步中的第二種特殊情況。Entry 不為空,但其中的 key 為空,什麼時候會發生這種情況呢?對,就是前面說到記憶體洩漏時提到的 過期 Entry。我們都知道 Entry 的 key 是弱引用的 ThreadLocal,當外部沒有它的任何強引用時,下次 GC 時就會將其回收。所以這時候的 Entry 理論上也是無效的了。

由於這裡是在 set() 方法插入元素的過程中發現了過期 Entry,所以只要將要插入的 Entry 直接替換這個 key==null 的 Entry 就可以了,這就是 replaceStaleEntry() 的核心邏輯。

replaceStaleEntry()

private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    Entry e;

    // Back up to check for prior stale entry in current run.
    // We clean out whole runs at a time to avoid continual
    // incremental rehashing due to garbage collector freeing
    // up refs in bunches (i.e., whenever the collector runs).
    // 向前找到第一個過期條目
    int slotToExpunge = staleSlot;
    for (int i = prevIndex(staleSlot, len);
        (e = tab[i]) != null;
        i = prevIndex(i, len))
        if (e.get() == null)
            slotToExpunge = i; // 記錄前一個過期條目的位置

    // Find either the key or trailing null slot of run, whichever occurs first
    // 向後查詢,直到找到 key 或者 空 Entry
    for (int i = nextIndex(staleSlot, len);
        (e = tab[i]) != null;
        i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();

        // If we find key, then we need to swap it
        // with the stale entry to maintain hash table order.
        // The newly stale slot, or any other stale slot
        // encountered above it, can then be sent to expungeStaleEntry
        // to remove or rehash all of the other entries in run.
        if (k == key) {

            // 如果在向後查詢過程中發現 key 相同的 entry 就覆蓋並且和過期 entry 進行交換
            e.value = value;

            tab[i] = tab[staleSlot];
            tab[staleSlot] = e;

            // Start expunge at preceding stale entry if it exists
            // 如果在查詢過程中還未發現過期 entry,那麼就以當前位置作為 cleanSomeSlots 的起點
            if (slotToExpunge == staleSlot)
                slotToExpunge = i;
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }

        // If we didn't find stale entry on backward scan, the
        // first stale entry seen while scanning for key is the
        // first still present in the run.
        // 如果向前未搜尋到過期 entry,而在向後查詢過程遇到過期 entry 的話,後面就以此時這個位置
        // 作為起點執行 cleanSomeSlots
        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    }

    // If key not found, put new entry in stale slot
    // 如果在查詢過程中沒有找到可以覆蓋的 entry,則將新的 entry 插入在過期 entry
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);

    // If there are any other stale entries in run, expunge them
    // 在上面的程式碼執行過程中,找到了其他的過期條目
    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

看起來挺累人的。在我理解,replaceStaleEntry 只是做一個標記的作用,在各種情況下最後都會呼叫 cleanSomeSlots 來真正的清理過期條目。

你可以看到 ``

cleanSomeSlots()

private boolean cleanSomeSlots(int i, int n) {
    boolean removed = false;
    Entry[] tab = table;
    int len = tab.length;
    do {
        i = nextIndex(i, len);
        Entry e = tab[i];
        if (e != null && e.get() == null) {
            n = len;
            removed = true;
            i = expungeStaleEntry(i); // 需要清理的 Entry
        }
    } while ( (n >>>= 1) != 0);
    return removed;
}

引數 n 表示掃描控制。初始情況下掃描 log2(n) 次,如果遇到過期條目,會再掃描 log2(table.length)-1 次。在 set() 方法中呼叫,引數 n 表示元素的個數。在 replaceStaleEntry 中呼叫,引數 n 表示的是陣列 table 的長度。

注意 do 迴圈裡面的判斷條件:e != null && e.get() == null ,還是那些 Entry 不為空,key 為空的過期條目。發現過期條目之後,呼叫 expungeStaleEntry() 去清理。

expungeStaleEntry()

private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    // expunge entry at staleSlot
    // 清空 staleSlot 處的 過期 entry
    // 將 value 置空,保證不會因為這裡的強引用造成 memory leak
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    // Rehash until we encounter null
    // 繼續搜尋直到遇到 tab 中的空 entry
    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 {
            // key 還沒有被回收
            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; // 此時從 staleSlot 到 i 之間不存在過期條目
}

直接將 entry.valueentry 都置空,消除記憶體洩露的隱患。注意這裡僅僅只是置空,並不是回收物件。因為你不知道 value 在外部的引用情況,只需要管好自己的引用就可以了。

除此之外,不甘寂寞的 expungeStaleEntry() 又發起了一次掃描,直到碰到空 Entry未知。期間遇到的過期 Entry 要置空。

整個 set() 方法就看完了,原理很簡單,但是其中關於記憶體洩漏的預防處理十分複雜,看的我一度放棄了,也讓我對原始碼閱讀產生了一些疑問。有些時候是不是沒有必要逐行去玩去完全理解?比如這一系列關於記憶體洩露的處理,核心思想就是清理 Entry 不為 null 但 key 為 null 的過期條目。理解了核心思想,對於其中複雜的細節處理是不是沒有必要去深究?不知道你怎麼看,歡迎在評論區寫下你的看法。

下面來看一看 getgetEntry 方法。

getEntry()

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() 比較粗暴,上來直接根據雜湊值查詢 table 陣列,如果直接命中,就返回。未直接命中,呼叫 getEntryAfterMiss() 繼續查詢。

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    // 向後查詢直到遇到空 entry
    while (e != null) {
        ThreadLocal<?> k = e.get();
        if (k == key) // get it
            return e;
        if (k == null) // key 等於 null,清理過期 entry
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len); // 繼續向後查詢
        e = tab[i];
    }
    return null;
}

呼叫 nextIndex() 向後查詢,直到遇到 空 Entry,也就是隊尾:

  • k==key,說明找到了對應 Entry
  • k==null,說明遇到了過期 Entry,呼叫 expungeStaleEntry() 處理

對過期 Entry 的處理真的是無處不在,就是為了最大程度的降低記憶體洩漏發生的機率。那麼有沒有什麼一勞永逸的辦法呢?那就是 ThreadLocalMapremove() 方法。

remove()

private void remove(ThreadLocal<?> key) {
    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)]) {
        if (e.get() == key) {
            e.clear();
            expungeStaleEntry(i);
            return;
        }
    }
}

直接清除當前 ThreadLocal 對應的 Entry,根本上避免了發生記憶體洩露。所以,當我們不再需要使用 ThreadLocal 中的相應資料時,呼叫一下 remove() 方法肯定是個好習慣。

雖然在長期存活的執行緒(例如執行緒池)中使用 ThreadLocal 併發生記憶體洩漏是一個小概率事件,但 JDK 開發者卻為此多寫了很多程式碼。我們在使用中也要多加註意,仔細考慮是否會涉及到記憶體洩露的問題。

End

最後說說在網上看到的一個觀點,ThreadLocal 比 Synchronized 更適合解決執行緒同步問題。

首先這個問題本身就不是那麼嚴謹。ThreadLocal 是用來解決執行緒同步問題的嗎?表面上看,ThreadLocal 的機制的確是執行緒安全的,但它並不是為了解決多執行緒訪問同一個變數的競爭問題,而是給每一個執行緒都提供單獨的變數,有些文章稱之為 資料備份,但它們並不是備份,每一個都是獨立存在的,互不干擾,並不存在什麼同步問題。

ThreadLocalSynchronized 的應用場景也是千差萬別的。例如銀行的轉賬場景,涉及多個賬戶同時轉賬的多執行緒同步問題,ThreadLocal 根本就沒法解決,即使每個執行緒都單獨儲存著使用者的餘額也沒法解決併發問題。ThreadLocal 在 Android 中的典型應用就是 Looper,每個執行緒都有自己的 Looper 物件,它們都是獨立工作,互不干擾的。

關於 ThreadLocal 就說到這裡了。後續分享的方向主要集中在兩塊,一方面是 AOSP 原始碼的閱讀和解析,另一方面是 Kotlin 和 Java 相關特性的對比,敬請期待!

文章首發微信公眾號: 秉心說 , 專注 Java 、 Android 原創知識分享,LeetCode 題解。

更多最新原創文章,掃碼關注我吧!

相關推薦

深入理解ThreadLocal

hibernate text 請求 nag rev ger 希望 har thread類 摘要   ThreadLocal 又名線程局部變量,是 Java 中一種較為特殊的線程綁定機制,用於保證變量在不同線程間的隔離性,以方便每個線程處理自己的狀態。

Java併發-深入理解ThreadLocal

一、ThreadLocal是什麼? ThreadLocal與執行緒同步機制不同,執行緒同步機制是多個執行緒共享同一個變數,而ThreadLocal是為每一個執行緒建立一個單獨的變數副本,故而每個執行緒都可以獨立地改變自己所擁有的變數副本,而不會影響其他執行緒所對應的副本。可以

[轉]Java 併發:深入理解 ThreadLocal

版權宣告: 摘要:   ThreadLocal 又名執行緒區域性變數,是 Java 中一種較為特殊的執行緒繫結機制,用於保證變數在不同執行緒間的隔離性,以方便每個執行緒處理自己的狀態。進一步地,本文以ThreadLocal類的原始碼為切入點,深入分析了T

深入理解ThreadLocal應用

ThreadLocal ThreadLocal類用來提供執行緒內部的區域性變數,不同的執行緒之間不會相互干擾,這種變數線上程的生命週期內起作用,減少同一個執行緒內多個函式或元件之間一些公共變數的傳遞的複雜度。說白了,ThreadLocal就是建立了能夠拿到以自

java多執行緒--深入理解threadlocal以及適用場景

/** * This class provides thread-local variables. These variables differ from * their normal counterparts in that each thread that accesses one (via its

深入理解 ThreadLocal

前言 上篇文章 深入理解 Handler 訊息機制 中提到了獲取執行緒的 Looper 是通過 ThreadLocal 來實現的: public static @Nullable Looper myLooper() { return sThreadLocal.get(); } 每個執行緒都有自己的 L

ThreadLocal深入理解與內存泄露分析

fonts statistic rac jdk 占用內存 得出 銷毀 prev other ThreadLocal 當使用ThreadLocal維護變量時,ThreadLocal為每個使用該變量的線程提供獨立的變量副本

深入理解java:執行緒本地變數 java.lang.ThreadLocal

ThreadLocal,很多人都叫它做執行緒本地變數,也有些地方叫做執行緒本地儲存,其實意思差不多。 可能很多朋友都知道ThreadLocal為變數在每個執行緒中都建立了一個副本,那樣每個執行緒可以訪問自己內部的副本變數。 這句話從表面上看起來理解正確,但實際上這種理解是不太正確的。下面我們

ThreadLocal深入理解與記憶體洩露分析

ThreadLocal的介面方法 public T get() { } public void set(T value) { } public void remove() { } protected T initialValue() { } get()用來獲取

java jdbc深入理解(connection與threadlocal與資料庫連線池和事務實)

1.jdbc連線資料庫,就這樣子Class.forName("com.mysql.jdbc.Driver"); java.sql.Connection conn = DriverManager.getConnection(jdbcUrl);2.通過傳入jdbc url用Drivermanager.getC

(轉存 作者未知)深入理解HTML協議

期望 intern 屬於 公告欄 機制 被拒 定向 圖片 工具欄 深入理解HTML協議 http協議學 習系列 1. 基礎概念篇 1.1 介紹 HTTP是Hyper Text Transfer Protocol(超文本傳輸協議)的縮寫。它的發展是萬維網協會(World

深入理解hostname

network 配置文件 release linux 知識點 當我覺得對Linux系統下修改hostname已經非常熟悉的時候,今天碰到了幾個個問題,這幾個問題給我好好上了一課,很多知識點,當你覺得你已經掌握的時候,其實你了解的還只是皮毛。技術活,切勿淺嘗則止!實驗環境:Red Hat E

徹底理解ThreadLocal

pass 進行 tps rem spec 區別 tablet dea 覆蓋 ThreadLocal是什麽   早在JDK 1.2的版本中就提供Java.lang.ThreadLocal,ThreadLocal為解決多線程程序的並發問題提供了一種新的思路。使用這個工具類可以很

javascript深入理解js閉包

bag 思考 2個 表達式 proto window對象 來看 連接 第一次 閉包(closure)是Javascript語言的一個難點,也是它的特色,很多高級應用都要依靠閉包實現。 一、變量的作用域 要理解閉包,首先必須理解Javascript特殊的變量作用域。

JSON 的深入理解

jos 數據轉換 不同 宋體 ges 分隔 blog 結構 集合 JSON 知識 JSON(JavaScript Object Notataion)javascript的對象表示形式,但是目前已經發展為一種輕量級的數據交互格式。 特點:完全獨立於語言的文本格式,跨平臺,有結

深入理解 CSS3 彈性盒布局模型

分辨率 top 應用 時間 控制 用戶 lock fire 應用開發 彈性盒布局模型(Flexible Box Layout)是 CSS3 規範中提出的一種新的布局方式。該布局模型的目的是提供一種更加高效的方式來對容器中的條目進行布局、對齊和分配空間。這種布局

深入理解javascript之設計模式

rip 是我 解決問題 不想 接受 button move center 常識 設計模式 設計模式是命名、抽象和識別對可重用的面向對象設計實用的的通用設計結構。設計模式確定類和他們的實體、他們的角色和協作、還有他們的責任分配。 每個設計模式都聚焦於一個面向對象的設計難題

(轉)final關鍵字的深入理解

多線程 body error app nds ann this tar order 轉自http://www.importnew.com/7553.html Java中的final關鍵字非常重要,它可以應用於類、方法以及變量。這篇文章中我將帶你看看什麽是final關鍵字?將

深入理解計算機系統-作業2.10

oid 位置 pla borde 作業2 nbsp body 開始 width 1 void inplace_swap(int *x, int *y){ 2 *y = *x ^ *y;/*step1*/ 3 *x = *x ^ *y;/*step2*/ 4

深入理解Activity啟動流程(二)–Activity啟動相關類的類圖

b- ive ava ani affinity server 組織 詳細 pac 本文原創作者:Cloud Chou. 歡迎轉載,請註明出處和本文鏈接 本系列博客將詳細闡述Activity的啟動流程,這些博客基於Cm 10.1源碼研究。 在介紹Activity的詳細啟動流程