1. 程式人生 > >Java併發——ThreadLocal、ThreadGroup類解析

Java併發——ThreadLocal、ThreadGroup類解析

ThreadLocal

    上節在討論Thread類的時候,丟擲了一個問題,即執行緒範圍之間如何實現資料的共享。其實很簡單,利用一個Map來存貯,鍵存貯執行緒的名字、id等資料,而值則存貯著該執行緒對應共享的資料,將該Map傳進對應的執行緒就可以實現資料的共享了,但是得注意同步。防止出現"髒資料"。而ThreadLocal類的存貯策略與上述相似,但是它只儲存著每個執行緒的對應的本地資料,一個執行緒並不能訪問ThreadLocal裡另外一個執行緒儲存的資料。說了這麼多,還沒正式的介紹ThreadLocal類,中文名是本地執行緒類,該類是用來儲存執行緒的本地資料的,如示例程式碼:

public class ThreadLocalTest extends Thread {
    private ThreadLocal<String> threadLocal;
    public ThreadLocalTest(ThreadLocal<String> threadLocal){
        this.threadLocal=threadLocal;
    }
    public void run(){
        System.out.println("蕾姆"+threadLocal.get());
    }
}

public
class Test { static ThreadLocal<String> threadLocal=new ThreadLocal<>(); public static void main(String args[]) throws UnsupportedEncodingException, InterruptedException { threadLocal.set("拉姆"); System.out.println(threadLocal.get()); ThreadLocalTest threadLocalTest=
new ThreadLocalTest(threadLocal); threadLocalTest.start(); } //列印: //拉姆 //蕾姆null

    與普通的Map存貯不同,ThreadLocal類的存貯並不需要指明鍵,因為它預設當前執行緒為它的鍵,所以只需要直接set與get即可。從程式碼中看出,將ThreadLocal類的示例傳進另外一個執行緒,並進行獲取,得到的是null值,也就是說ThreadLocal類會儲存當前執行緒的資料,因為ThreadLocalTest 執行緒沒有set進資料,所以在ThreadLocalTest 執行緒內get不到資料。從這個例子看出,它實現了執行緒之間的資料的分離,讓每個執行緒都獨自管理它的資料,從而不會混淆。它的應用場景是:

  1. 方便同一個執行緒使用某一物件,避免不必要的引數傳遞;
  2. 執行緒間資料隔離(每個執行緒在自己執行緒裡使用自己的區域性變數,各執行緒間的ThreadLocal物件互不影響);

    關於ThreadLocal類的關鍵,是其內部的ThreadLocalMap靜態內部類,ThreadLocalMap類存貯的節點定義如下:

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

    從程式碼中可以看出,ThreadLocalMap儲存的節點繼承了WeakReference類,也就是弱引用類,當GC掃描到了該部分時,所有的節點都會被GC回收,但是如果存貯的是較小的單位,那麼GC便很小的可能性會掃到,所以ThreadLocal類裡面不能存貯較大的資料以及比較重要的資料。每個節點儲存著ThreadLocal類以及當前執行緒的資料。ThreadLocalMap類與HashMap類的主要區別有以下幾點:

  1. HashMap存貯的是強引用,而ThreadLocalMap類存貯的是弱引用;
  2. HashMap預設的負載因子是0.75,且可以從外部改變該值,而ThreadLocalMap類的預設是2/3,且不能更改,HashMap類的threshold 大於桶長度,而ThreadLocalMap類小於桶長度;
private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }
  1. HashMap解決鍵衝突是採用連結串列法解決,而ThreadLocalMap類則是採取開放地址法來解決:
private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }

    開放地址法是指,當經過計算,某個鍵的索引值在雜湊桶中已經有了節點時,它會將鍵值對放置那個節點的下一個桶中,如果下一個桶中還有,那麼放在下一個桶的下一個桶中,直到存入或者超出桶的上限。如插入的程式碼:

private void set(ThreadLocal<?> key, Object value) {
            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;
            //resize桶的大小
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

    而為了記憶體的回收,ThreadLocalMap類裡的set、remove、rehash等方法都直接或者間接的呼叫了expungeStaleEntry方法,該方法是用來將失去鍵引用的值引用置為null,方便記憶體回收的:

private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;
            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;
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

    而ThreadLocal底層呼叫的如下:

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

    其中ThreadLocalMap 例項是從Thread中得到的,一個執行緒也只允許一個ThreadLocalMap來儲存本地資料,一個ThreadLocalMap類裡儲存著很多相同的ThreadLocal(this)引用,但是ThreadLocal類對應著很多個ThreadLocalMap,因為每個執行緒的ThreadLocalMap都不一樣,也就是說ThreadLocal類只是個空殼,內部的ThreadLocalMap都是有不同的執行緒對應的ThreadLocalMap實現的。

ThreadGroup

    關於java.lang包裡的與執行緒相關的類就是ThreadGroup類,ThreadGroup類是執行緒組類,執行緒組不同於執行緒池前者是為了統一管理多個執行緒的屬性,比如設定是否是守護執行緒,執行緒的優先順序等等。而後者是為了減少連線帶來的開銷。當你在main方法裡建立執行緒時,那麼該執行緒會自動成為主執行緒的執行緒組中的一員。而每一個ThreadGroup都可以包含一組的子執行緒和一組子執行緒組,先介紹ThreadGroup的內部成員變數:

public
class ThreadGroup implements Thread.UncaughtExceptionHandler {
	當前執行緒組的父執行緒組
    private final ThreadGroup parent;
    //執行緒組的名字
    String name;
    //當前執行緒組最大優先順序
    int maxPriority;
    //是否被銷燬
    boolean destroyed;
    //是否守護執行緒
    boolean daemon;
    //是否可以中斷
    boolean vmAllowSuspension;
    //當前執行緒組的執行緒數量
    int nthreads;
    //存貯當前的執行緒
    Thread threads[];
    //當前執行緒組的數量
    int ngroups;
    //存貯多個執行緒組
    ThreadGroup groups[];
}

    如何讓執行緒加入指定的執行緒組呢?其實很簡單,在建立此執行緒的時候,指定對應的執行緒組就行了。通常情況下我們建立執行緒時可能不設定執行緒組,這時候建立的執行緒會和建立該執行緒的執行緒所在組在一個組裡面。一般來說,在main方法中建立的執行緒,它的父執行緒組是main執行緒組,而main執行緒組的父執行緒組是system執行緒組,如下程式碼:

 System.out.println(Thread.currentThread().getThreadGroup().getName());
 System.out.println(Thread.currentThread().getThreadGroup().getParent().getName());
 System.out.println(Thread.currentThread().getThreadGroup().getParent().getParent().getName();
 //列印:main
 //system
 //報異常

    由此可見,system是最高級別的執行緒組了。因為是對執行緒組裡所有執行緒的操作,所以ThreadGroup類的操作大多是批處理操作,具體有如下:

//設定是否守護執行緒
public final void setDaemon(boolean daemon);
//設定最高優先順序
public final void setMaxPriority(int pri);
//執行緒組的內的活躍執行緒數
public int activeCount();
//將執行緒組內的執行緒複製給list執行緒陣列
public int enumerate(Thread list[]);
//中斷處於阻塞的執行緒
public final void interrupt();

    還需注意的是ThreadGroup 類實現了Thread.UncaughtExceptionHandler介面,也就是說ThreadGroup 可以設定未處理異常的處理方法,當執行緒組中某個執行緒發生Unchecked exception異常時,由執行環境呼叫此方法進行相關處理,如果有必要,可以重新定義此方法,如下面的示例:

public class GroupTest extends ThreadGroup {
    public GroupTest(String name) {
        super(name);
    }
    public void uncaughtException(Thread t, Throwable e){
    		//異常在這裡處理
            System.out.println("蕾姆在這裡處理異常啦");
    }
}

GroupTest groupTest=new GroupTest("蕾姆");
        Thread thread=new Thread(groupTest, new Runnable() {
            @Override
            public void run() {
                throw new NullPointerException();
            }
        });
thread.start();

    ThreadGroup 類預設的處理未捕獲異常的方式是,先判斷是否有父執行緒組,如果有,交給父執行緒組處理,若沒有就採取自身預設的處理方式處理:

public void uncaughtException(Thread t, Throwable e) {
        if (parent != null) {
            parent.uncaughtException(t, e);
        } else {
            Thread.UncaughtExceptionHandler ueh =
                Thread.getDefaultUncaughtExceptionHandler();
            if (ueh != null) {
                ueh.uncaughtException(t, e);
            } else if (!(e instanceof ThreadDeath)) {
                System.err.print("Exception in thread \""
                                 + t.getName() + "\" ");
                e.printStackTrace(System.err);
            }
        }
    }