1. 程式人生 > 實用技巧 >織夢TAG標籤偽靜態處理教程

織夢TAG標籤偽靜態處理教程

技術標籤:Java併發

一、ThreadLocal簡介

多執行緒訪問同一個共享變數的時候容易出現併發問題,特別是多個執行緒對一個變數進行寫入的時候,為了保證執行緒安全,一般使用者在訪問共享變數的時候需要進行額外的同步措施才能保證執行緒安全性。ThreadLocal是除了加鎖這種同步方式之外的一種保證一種規避多執行緒訪問出現執行緒不安全的方法,當我們在建立一個變數後,如果每個執行緒對其進行訪問的時候訪問的都是執行緒自己的變數這樣就不會存線上程不安全問題。

  ThreadLocal是JDK包提供的,它提供執行緒本地變數,如果建立一個ThreadLocal變數,那麼訪問這個變數的每個執行緒都會有這個變數的一個副本,在實際多執行緒操作的時候,操作的是自己本地記憶體中的變數,從而規避了執行緒安全問題。如下圖所示:

ThreadLocal 提供了執行緒本地的例項。它與普通變數的區別在於,每個使用該變數的執行緒都會初始化一個完全獨立的例項副本。ThreadLocal 變數通常被private static修飾。當一個執行緒結束時,它所使用的所有 ThreadLocal 相對的例項副本都可被回收。

總的來說,ThreadLocal 適用於每個執行緒需要自己獨立的例項且該例項需要在多個方法中被使用,也即變數線上程間隔離而在方法或類間共享的場景。

二、ThreadLocal的簡單使用

內部方法如下:

作用域型別方法描述
publicTget()返回此執行緒區域性變數的當前執行緒副本中的值
protectedTinitialValue()返回此執行緒區域性變數的當前執行緒的“初始值”
publicvoidremove()移除此執行緒區域性變數當前執行緒的值
publicvoidset(T value)將此執行緒區域性變數的當前執行緒副本中的值設定為指定值

示例:

package test;

public class ThreadLocalTest {

    static ThreadLocal<String> localVar = new ThreadLocal<>();

    static void print(String str) {
        //列印當前執行緒中本地記憶體中本地變數的值
        System.out.println(str + " :" + localVar.get());
        //清除本地記憶體中的本地變數
        localVar.remove();
    }

    public static void main(String[] args) {
        Thread t1  = new Thread(new Runnable() {
            @Override
            public void run() {
                //設定執行緒1中本地變數的值
                localVar.set("localVar1");
                //呼叫列印方法
                print("thread1");
                //列印本地變數
                System.out.println("after remove : " + localVar.get());
            }
        });

        Thread t2  = new Thread(new Runnable() {
            @Override
            public void run() {
                //設定執行緒1中本地變數的值
                localVar.set("localVar2");
                //呼叫列印方法
                print("thread2");
                //列印本地變數
                System.out.println("after remove : " + localVar.get());
            }
        });

        t1.start();
        t2.start();
    }
}

三、ThreadLocal的實現原理

首先 ThreadLocal 是一個泛型類,保證可以接受任何型別的物件。

因為一個執行緒內可以存在多個 ThreadLocal 物件,所以其實是 ThreadLocal 內部維護了一個 Map ,這個 Map 不是直接使用的 HashMap ,而是 ThreadLocal 實現的一個叫做ThreadLocalMap的靜態內部類。而我們使用的 get()、set() 方法其實都是呼叫了這個ThreadLocalMap類對應的 get()、set() 方法。例如下面的 set 方法:

public void set(T value) {
	Thread t = Thread.currentThread();
	ThreadLocalMap map = getMap(t);
	if (map != null)
		map.set(this, value);
	else
		createMap(t, value);
}

get方法:

public T get() {   
	Thread t = Thread.currentThread();   
	ThreadLocalMap map = getMap(t);   
	if (map != null)   
		return (T)map.get(this);   

	// Maps are constructed lazily.  if the map for this thread   
	// doesn't exist, create it, with this ThreadLocal and its   
	// initial value as its only entry.   
	T value = initialValue();   
	createMap(t, value);   
	return value;   
}

createMap方法:

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

ThreadLocalMap是個靜態的內部類:

static class ThreadLocalMap {   
	........   
}  

最終的變數是放在了當前執行緒的 ThreadLocalMap 中,並不是存在 ThreadLocal 上,ThreadLocal 可以理解為只是ThreadLocalMap的封裝,傳遞了變數值。

ThreadLocalMap

ThreadLocalMap可以看成一個HashMap,但是它本身具體的實現並沒有實現繼承HashMap甚至跟java.util.Map都沾不上一點關係。只是內部的實現跟HashMap類似(通過雜湊表的方式儲存)。
ThreadLocalMap.Entry可以看成是儲存鍵值對的物件,其本質上是一個WeakReference<ThreadLocal>物件。

ThreadLocalMap中儲存的是ThreadLocalMap.Entry。

首先ThreadLocalMap需要一個“容器”來儲存這些Entry物件,ThreadLocalMap中定義了Entry陣列例項table,用於儲存Entry。

  private Entry[] table;

也就是說,ThreadLocalMap維護一張雜湊表(一個數組),表裡面儲存Entry。既然是雜湊表,那肯定就會涉及到載入因子,即當表裡面儲存的物件達到容量的多少百分比的時候需要擴容。ThreadLocalMap中定義了threshold屬性,當表裡儲存的物件數量超過threshold就會擴容。如下所示:

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

從上面程式碼看出,載入因子設定為2/3。即每次容量超過設定的len的2/3時,需要擴容。

儲存Entry物件

首先看看資料是如何被放入到雜湊表裡面:

/**
 * Set the value associated with key.
 *
 * @param key the thread local object
 * @param value the value to be 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);

    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal k = e.get();

        if (k == key) {
            e.value = value;
            return;
        }

        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

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

從上面程式碼中看出,通過key(ThreadLocal型別)的hashcode來計算儲存的索引位置i。如果i位置已經儲存了物件,那麼就往後挪一個位置依次類推,直到找到空的位置,再將物件存放。另外,在最後還需要判斷一下當前的儲存的物件個數是否已經超出了閾值(threshold的值)大小,如果超出了,需要重新擴充並將所有的物件重新計算位置(rehash函式來實現)。那麼我們看看rehash函式如何實現的:

private void rehash() {
    expungeStaleEntries();

    // Use lower threshold for doubling to avoid hysteresis
    if (size >= threshold - threshold / 4)
        resize();
}

看到,rehash函式裡面先呼叫了expungeStaleEntries函式,然後再判斷當前儲存物件的大小是否超出了閾值的3/4。如果超出了,再擴容。看的有點混亂。為什麼不直接擴容並重新擺放物件?為啥要搞成這麼複雜?

其實,ThreadLocalMap裡面儲存的Entry物件本質上是一個WeakReference<ThreadLocal>。也就是說,ThreadLocalMap裡面儲存的物件本質是一個對ThreadLocal物件的弱引用,該ThreadLocal隨時可能會被回收!即導致ThreadLocalMap裡面對應的Value的Key是null。我們需要把這樣的Entry給清除掉,不要讓它們佔坑。

expungeStaleEntries函式就是做這樣的清理工作,清理完後,實際儲存的物件數量自然會減少,這也不難理解後面的判斷的約束條件為閾值的3/4,而不是閾值的大小。

那麼如何判斷哪些Entry是需要清理的呢?其實很簡單,只需把ThreadLocalMap裡面的key值遍歷一遍,為null的直接刪了即可。可是,前面我們說過,ThreadLocalMap並沒有實現java.util.Map介面,即無法得到keySet。其實,不難發現,如果Key值為null,此時呼叫ThreadLocalMap的getEntry(ThreadLocal)相當於getEntry(null),getEntry(null)返回的是null,這也就很好的解決了判斷問題。也就是說,無需判斷,直接根據getEntry函式的返回值是不是null來判定需不需要將該Entry刪除掉。注意,getEntry返回null也有可能是key的值不為null,但是對於getEntry返回為null的Entry,也沒有佔坑的必要,同樣需要刪掉,這麼一來,就一舉兩得了。

獲取Entry物件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函式很簡單,直接通過雜湊碼計算位置i,然後把雜湊表中對應i位置的Entry物件拿出來。如果對應位置的值為null,這就存在如下幾種可能:

  • key對應的值確實為null
  • 由於位置衝突,key對應的值儲存的位置並不在i位置上,即i位置上的null並不屬於key的值。

因此,需要一個函式再次去確認key對應的value的值,即getEntryAfterMiss函式:

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

ThreadLocalMap.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>。即Entry 本質上就是WeakReference<ThreadLocal>,換言之,Entry就是一個弱引用,具體講,Entry例項就是對ThreadLocal某個例項的弱引用。只不過,Entry同時還儲存了value。

四、ThreadLocal不支援繼承性

同一個ThreadLocal變數在父執行緒中被設定值後,在子執行緒中是獲取不到的。(threadLocals中為當前呼叫執行緒對應的本地變數,所以二者自然是不能共享的)

public class ThreadLocalTest2 {

    //(1)建立ThreadLocal變數
    public static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        //在main執行緒中新增main執行緒的本地變數
        threadLocal.set("mainVal");
        //新建立一個子執行緒
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("子執行緒中的本地變數值:"+threadLocal.get());
            }
        });
        thread.start();
        //輸出main執行緒中的本地變數值
        System.out.println("mainx執行緒中的本地變數值:"+threadLocal.get());
    }
}

五、InheritableThreadLocal類

ThreadLocal類是不能提供子執行緒訪問父執行緒的本地變數的,而InheritableThreadLocal類則可以做到這個功能,下面是該類的原始碼:

public class InheritableThreadLocal<T> extends ThreadLocal<T> {

    protected T childValue(T parentValue) {
        return parentValue;
    }

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

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

從上面程式碼可以看出,InheritableThreadLocal類繼承了ThreadLocal類,並重寫了childValue、getMap、createMap三個方法。其中createMap方法在被呼叫(當前執行緒呼叫set方法時得到的map為null的時候需要呼叫該方法)的時候,建立的是inheritableThreadLocal而不是threadLocals。同理,getMap方法在當前呼叫者執行緒呼叫get方法的時候返回的也不是threadLocals而是inheritableThreadLocal。

下面我們看看重寫的childValue方法在什麼時候執行,怎樣讓子執行緒訪問父執行緒的本地變數值。我們首先從Thread類開始說起:

private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize) {
    init(g, target, name, stackSize, null, true);
}
private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize, AccessControlContext acc,
                  boolean inheritThreadLocals) {
    //判斷名字的合法性
    if (name == null) {
        throw new NullPointerException("name cannot be null");
    }

    this.name = name;
    //(1)獲取當前執行緒(父執行緒)
    Thread parent = currentThread();
    //安全校驗
    SecurityManager security = System.getSecurityManager();
    if (g == null) { //g:當前執行緒組
        if (security != null) {
            g = security.getThreadGroup();
        }
        if (g == null) {
            g = parent.getThreadGroup();
        }
    }
    g.checkAccess();
    if (security != null) {
        if (isCCLOverridden(getClass())) {
            security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
        }
    }

    g.addUnstarted();

    this.group = g; //設定為當前執行緒組
    this.daemon = parent.isDaemon();//守護執行緒與否(同父執行緒)
    this.priority = parent.getPriority();//優先順序同父執行緒
    if (security == null || isCCLOverridden(parent.getClass()))
        this.contextClassLoader = parent.getContextClassLoader();
    else
        this.contextClassLoader = parent.contextClassLoader;
    this.inheritedAccessControlContext =
            acc != null ? acc : AccessController.getContext();
    this.target = target;
    setPriority(priority);
    //(2)如果父執行緒的inheritableThreadLocal不為null
    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        //(3)設定子執行緒中的inheritableThreadLocals為父執行緒的inheritableThreadLocals
        this.inheritableThreadLocals =
            ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    this.stackSize = stackSize;

    tid = nextThreadID();
}

在init方法中,首先(1)處獲取了當前執行緒(父執行緒),然後(2)處判斷當前父執行緒的inheritableThreadLocals是否為null,然後呼叫createInheritedMap將父執行緒的inheritableThreadLocals作為建構函式引數建立了一個新的ThreadLocalMap變數,然後賦值給子執行緒。下面是createInheritedMap方法和ThreadLocalMap的構造方法

static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
    return new ThreadLocalMap(parentMap);
}

private ThreadLocalMap(ThreadLocalMap parentMap) {
    Entry[] parentTable = parentMap.table;
    int len = parentTable.length;
    setThreshold(len);
    table = new Entry[len];

    for (int j = 0; j < len; j++) {
        Entry e = parentTable[j];
        if (e != null) {
            @SuppressWarnings("unchecked")
            ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
            if (key != null) {
                //呼叫重寫的方法
                Object value = key.childValue(e.value);
                Entry c = new Entry(key, value);
                int h = key.threadLocalHashCode & (len - 1);
                while (table[h] != null)
                    h = nextIndex(h, len);
                table[h] = c;
                size++;
            }
        }
    }
}

在建構函式中將父執行緒的inheritableThreadLocals成員變數的值賦值到新的ThreadLocalMap物件中。返回之後賦值給子執行緒的inheritableThreadLocals。總之,InheritableThreadLocals類通過重寫getMap和createMap兩個方法將本地變數儲存到了具體執行緒的inheritableThreadLocals變數中,當執行緒通過InheritableThreadLocals例項的set或者get方法設定變數的時候,就會建立當前執行緒的inheritableThreadLocals變數。而父執行緒建立子執行緒的時候,ThreadLocalMap中的建構函式會將父執行緒的inheritableThreadLocals中的變數複製一份到子執行緒的inheritableThreadLocals變數中。

六、從ThreadLocalMap看ThreadLocal使用不當的記憶體洩漏問題

實際上ThreadLocalMap中使用的 key 為 ThreadLocal 的弱引用,弱引用的特點是,如果這個物件只存在弱引用,那麼在下一次垃圾回收的時候必然會被清理掉。

所以如果 ThreadLocal 沒有被外部強引用的情況下,在垃圾回收的時候會被清理掉的,這樣一來ThreadLocalMap中使用這個 ThreadLocal 的 key 也會被清理掉。但是,value 是強引用,不會被清理,這樣一來就會出現 key 為 null 的 value。

ThreadLocalMap實現中已經考慮了這種情況,在呼叫 set()、get()、remove() 方法的時候,會清理掉 key 為 null 的記錄。如果說會出現記憶體洩漏,那只有在出現了 key 為 null 的記錄後,沒有手動呼叫 remove() 方法,並且之後也不再呼叫 get()、set()、remove() 方法的情況下。

建議回收自定義的ThreadLocal變數,尤其線上程池場景下,執行緒經常會被複用,如果不清理自定義的 ThreadLocal變數,可能會影響後續業務邏輯和造成記憶體洩露等問題。 儘量在代理中使用try-finally塊進行回收:

objectThreadLocal.set(userInfo); 
try {
    // ... 
} 
finally {
    objectThreadLocal.remove(); 
}

七、ThreadLocal的使用場景

ThreadLocal 適用於如下兩種場景:

  • 每個執行緒需要有自己單獨的例項
  • 例項需要在多個方法中共享,但不希望被多執行緒共享

對於第一點,每個執行緒擁有自己例項,實現它的方式很多。例如可以線上程內部構建一個單獨的例項。ThreadLocal可以以非常方便的形式滿足該需求。

對於第二點,可以在滿足第一點(每個執行緒有自己的例項)的條件下,通過方法間引用傳遞的形式實現。ThreadLocal 使得程式碼耦合度更低,且實現更優雅。

儲存使用者Session

private static final ThreadLocal threadSession = new ThreadLocal();

    public static Session getSession() throws InfrastructureException {
        Session s = (Session) threadSession.get();
        try {
            if (s == null) {
                s = getSessionFactory().openSession();
                threadSession.set(s);
            }
        } catch (HibernateException ex) {
            throw new InfrastructureException(ex);
        }
        return s;
    }
}

解決執行緒安全的問題

比如Java7中的SimpleDateFormat不是執行緒安全的,可以用ThreadLocal來解決這個問題:

public class DateUtil {
    private static ThreadLocal<SimpleDateFormat> format1 = new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };

    public static String formatDate(Date date) {
        return format1.get().format(date);
    }
}

這裡的DateUtil.formatDate()就是執行緒安全的了。(Java8裡的 java.time.format.DateTimeFormatter是執行緒安全的,Joda time裡的DateTimeFormat也是執行緒安全的)。

八、ThreadLocalRandom

ThreadLocalRandom使用ThreadLocal的原理,讓每個執行緒內持有一個本地的種子變數,該種子變數只有在使用隨機數時候才會被初始化,多執行緒下計算新種子時候是根據自己執行緒內維護的種子變數進行更新,從而避免了競爭。

用法:

ThreadLocalRandom.current().nextInt(100)