ThreadLocal類

public class ThreadLocalTest1 implements Runnable{
	private static ThreadLocal<Integer> threadLocalA = new ThreadLocal<Integer>();
	private static ThreadLocal<Integer> threadLocalB = new ThreadLocal<Integer>();
	
	public static void main(String[] args) {
		Thread t1 = new Thread(new ThreadLocalTest1(), "A");
		Thread t2 = new Thread(new ThreadLocalTest1(), "B");
		t1.start();
		t2.start();
	}

	@Override
	public void run() {
		threadLocalA.set((int) (Math.random() * 100D));
		System.out.println("當前線程:" + Thread.currentThread().getName() + ": " + threadLocalA.get());
		
		threadLocalB.set((int) (Math.random() * 100D));
		System.out.println("當前線程:" + Thread.currentThread().getName() + ": " + threadLocalB.get());
	}

}

 輸出:java

  當前線程:A: 98
  當前線程:A: 25
  當前線程:B: 28
  當前線程:B: 27mysql

其中: 98, 25 這兩個值都會放到本身所屬的線程對線A當中web

   28, 27這兩個值都會放到本身所屬的線程對線A當中sql

                                   

1、對ThreadLocal理解

  ThreadLocal提供一個方便的方式,能夠根據不一樣的線程存放一些不一樣的特徵屬性,能夠方便的在線程中進行存取。  編程

  歸納起來講,對於多線程資源共享的問題,同步機制採用了「以時間換空間」的方式,而 ThreadLocal 採用了「以空間換時間」的方式。前者僅提供一份變量,讓不一樣的線程排隊訪問;後者爲每個線程都提供了一份變量,所以能夠同時訪問而互不影響。數組

  ThreadLocal 類主要有以下方法:緩存

    protected T initialValue():設置初始值,默認爲null
    public void set(T value):設置一個要保存的值,並會覆蓋原來的值
    public T get():得到保存的值,若是沒有用過set方法,會獲取到初始值
    public void remove():移除保存的值session

ThreadLocal是什麼

  首先,它是一個數據結構,有點像HashMap,能夠保存"key : value"鍵值對,可是一個ThreadLocal只能保存一個,而且各個線程的數據互不干擾。數據結構

ThreadLocal<String> localName = new ThreadLocal();
localName.set("佔小狼");
String name = localName.get();

  在線程1中初始化了一個ThreadLocal對象localName,並經過set方法,保存了一個值 佔小狼,同時在線程1中經過 localName.get()能夠拿到以前設置的值,可是若是在線程2中,拿到的將是一個null。多線程

  這是爲何,如何實現?不過以前也說了,ThreadLocal保證了各個線程的數據互不干擾。

  看看 set(T value)和 get()方法的源碼

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

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

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

  能夠發現,每一個線程中都有一個 ThreadLocalMap數據結構,當執行set方法時,其值是保存在當前線程的 threadLocals變量中,當執行set方法中,是從當前線程的 threadLocals變量獲取。

  因此在線程1中set的值,對線程2來講是摸不到的,並且在線程2中從新set的話,也不會影響到線程1中的值,保證了線程之間不會相互干擾。

  那每一個線程中的 ThreadLoalMap到底是什麼?

ThreadLoalMap

本文分析的是1.7的源碼。

  從名字上看,能夠猜到它也是一個相似HashMap的數據結構,可是在ThreadLocal中,並沒實現Map接口。

  在ThreadLoalMap中,也是初始化一個大小16的Entry數組,Entry對象用來保存每個key-value鍵值對,只不過這裏的key永遠都是ThreadLocal對象,是否是很神奇,經過ThreadLocal對象的set方法,結果把ThreadLocal對象本身當作key,放進了ThreadLoalMap中。

  這裏須要注意的是,ThreadLoalMap的Entry是繼承WeakReference,和HashMap很大的區別是,Entry中沒有next字段,因此就不存在鏈表的狀況了。

hash衝突

  沒有鏈表結構,那發生hash衝突了怎麼辦?

  先看看ThreadLoalMap中插入一個key-value的實現

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;    
    if (!cleanSomeSlots(i, sz) && sz >= threshold)        
        rehash();
}

  每一個ThreadLocal對象都有一個hash值 threadLocalHashCode,每初始化一個ThreadLocal對象,hash值就增長一個固定的大小 0x61c88647

  在插入過程當中,根據ThreadLocal對象的hash值,定位到table中的位置i,過程以下:一、若是當前位置是空的,那麼正好,就初始化一個Entry對象放在位置i上;二、不巧,位置i已經有Entry對象了,若是這個Entry對象的key正好是即將設置的key,那麼從新設置Entry中的value;三、很不巧,位置i的Entry對象,和即將設置的key不要緊,那麼只能找下一個空位置;

  這樣的話,在get的時候,也會根據ThreadLocal對象的hash值,定位到table中的位置,而後判斷該位置Entry對象中的key是否和get的key一致,若是不一致,就判斷下一個位置

  能夠發現,set和get若是衝突嚴重的話,效率很低,由於ThreadLoalMap是Thread的一個屬性,因此即便在本身的代碼中控制了設置的元素個數,但仍是不能控制其它代碼的行爲。

內存泄露

  ThreadLocal可能致使內存泄漏,爲何?先看看Entry的實現:

static class Entry extends WeakReference<ThreadLocal<?>> {    
    /** The value associated with this ThreadLocal. */    
    Object value;
    Entry(ThreadLocal<?> k, Object v) {        
        super(k);        
        value = v;    
    }
}

  經過以前的分析已經知道,當使用ThreadLocal保存一個value時,會在ThreadLocalMap中的數組插入一個Entry對象,按理說key-value都應該以強引用保存在Entry對象中,但在ThreadLocalMap的實現中,key被保存到了WeakReference對象中。

  這就致使了一個問題,ThreadLocal在沒有外部強引用時,發生GC時會被回收,若是建立ThreadLocal的線程一直持續運行,那麼這個Entry對象中的value就有可能一直得不到回收,發生內存泄露。

如何避免內存泄露

  既然已經發現有內存泄露的隱患,天然有應對的策略,在調用ThreadLocal的get()、set()可能會清除ThreadLocalMap中key爲null的Entry對象,這樣對應的value就沒有GC Roots可達了,下次GC的時候就能夠被回收,固然若是調用remove方法,確定會刪除對應的Entry對象。

  若是使用ThreadLocal的set方法以後,沒有顯示的調用remove方法,就有可能發生內存泄露,因此養成良好的編程習慣十分重要,使用完ThreadLocal以後,記得調用remove方法。

ThreadLocal<String> localName = new ThreadLocal();
try {    
    localName.set("佔小狼");    
    // 其它業務邏輯
} finally {    
    localName.remove();
}

2、以session爲例來理解ThreadLocal  

  在web開發的session中,不一樣的線程對應不一樣的session,那麼如何針對不一樣的線程獲取對應的session呢?

  咱們能夠設想了以下兩種方式:

    1.在action中建立session,而後傳遞給Service,Service再傳遞給Dao,很明顯,這種方式將使代碼變得臃腫複雜。

    2.建立一個靜態的map,鍵對應咱們的線程,值對應session,當咱們想獲取session時,只須要獲取map,而後根據當前的線程就能夠獲取對應的值。

  咱們看看Hibernate中是如何實現這種狀況的:

 

  在Hibernate中是經過使用ThreadLocal來實現的。在getSession方法中,若是ThreadLocal存在session,則返回session,不然建立一個session放入ThreadLocal中。

  總結一下就是在ThreadLocal中存放了一個session。

爲何咱們在ThreadLocal存放一個session,這個session就會與一個線程對應呢? 

  實際上ThreadLocal中並無存聽任何的對象或引用,在上面的的代碼中ThreadLocal的實例threadSession只至關於一個標記的做用。而存放對象的真正位置是正在運行的Thread線程對象,每一個Thread對象中都存放着一個ThreadLocalMap類型threadLocals對象,這是一個映射表map,這個map的鍵是一個ThreadLocal對象,值就是咱們想存的局部對象。

  咱們以上面的代碼爲例分析一下:

  當咱們往ThreadLocal中存放變量的時候發生了什麼?

  即這行代碼時。

  咱們看下ThreadLocal的源碼中set()方法的實現。

  若是把這些代碼簡化的話就一句

    Thread.currentThread().threadLocals.set(this,value);

    Thread.currentThread()獲取當前的線程

    threadLocals就是咱們上面說的每一個線程對象中用於存放局部對象的map

  因此set()就是獲取到當前線程的map而後把值放進去,咱們發現鍵是this,也就是當前的ThreadLocal對象,能夠發現ThreadLocal對象就是一個標記的做用,咱們根據這個標記找到對應的局部對象。

  若是對比get()方法,能夠發現原理都差很少,都是對線程中的threadLocals這個map的操做,我就不解釋了。

  ThreadLocal就是一個標記的做用,當咱們在線程中使用ThreadLocal的set()或者get()方法時,實際上是在操做咱們線程自帶的threadLocals這個map,多個線程的時候天然就有多個map,這些map互相獨立,可是,這些map都是根據一個ThreadLocal對象(由於它是靜態的)來做爲鍵存放。

  這樣能夠在多個線程中,每一個線程存放不同的變量,咱們經過一個ThreadLocal對象,在不一樣的線程(經過Thread.currentThread()獲取當前線程)中獲得不一樣的值(不一樣線程的threadLocals不同)。

  爲何threadLocals要是一個map呢?

  由於咱們可能會在一個類中聲明多個ThreadLocal的實例,這樣就有多個標記,因此要使用map對應。

原理:

 線程共享變量緩存以下:

  Thread.ThreadLocalMap<ThreadLocalObject>;

    一、Thread: 當前線程,能夠經過Thread.currentThread()獲取

    二、ThreadLocal:咱們的static ThreadLocal變量。

    三、Object: 當前線程共享變量

 咱們調用ThreadLocal.get方法時,其實是從當前線程中獲取ThreadLocalMap<ThreadLocal, Object>,而後根據當前ThreadLocal獲取當前線程共享變量Object

 ThreadLocal.set,ThreadLocal.remove其實是一樣的道理。

 

 這種存儲結構的好處:

  一、線程死去的時候,線程共享變量ThreadLocalMap則銷燬。

  二、ThreadLocalMap<ThreadLocal,Object>鍵值對數量爲ThreadLocal的數量,通常來講ThreadLocal數量不多,相比在ThreadLocal中用Map<Thread, Object>鍵值對存儲線程共享變量(Thread數量通常來講比ThreadLocal數量多),性能提升不少。

 

 關於ThreadLocalMap<ThreadLocalObject>弱引用問題:

  當線程沒有結束,可是ThreadLocal已經被回收,則可能致使線程中存在ThreadLocalMap<nullObject>的鍵值對,形成內存泄露。(ThreadLocal被回收,ThreadLocal關聯的線程共享變量還存在)。

  雖然ThreadLocal的get,set方法能夠清除ThreadLocalMap中key爲null的value,可是get,set方法在內存泄露後並不會必然調用,因此爲了防止此類狀況的出現,咱們有兩種手段。

    一、使用完線程共享變量後,顯示調用ThreadLocalMap.remove方法清除線程共享變量;

    二、JDK建議ThreadLocal定義爲private static,這樣ThreadLocal的弱引用問題則不存在了。

應用場景:

  (1)在線程中存放一些就像session的這種特徵變量,會針對不一樣的線程,有不一樣的值。

  (2)通常 ThreadLocal 做爲全局變量使用,示例以下:

public class ConnectionManager {

    // 線程內共享Connection,ThreadLocal一般是全局的,支持泛型
    private static ThreadLocal<Connection> threadLocal = new ThreadLocal<Connection>();

    public static Connection getCurrentConnection() {
        Connection conn = threadLocal.get();
        try {
            if(conn == null || conn.isClosed()) {
                String url = "jdbc:mysql://localhost:3306/test" ;
                conn = DriverManager.getConnection(url , "root" , "root") ;
                threadLocal.set(conn);
            }
        } catch (SQLException e) {
        }
        return conn;
    }

    public static void close() {
        Connection conn = threadLocal.get();
        try {
            if(conn != null && !conn.isClosed()) {
                conn.close();
                threadLocal.remove();
                conn = null;
            }
        } catch (SQLException e) {
        }
    }
}

  (3)ThreadLocal解決共享參數 

  ThreadLocal 也經常使用在多線程環境中,某個方法處理一個業務,須要遞歸依賴其餘方法時,而要在這些方法中共享參數的問題。

  例若有方法 a(),在該方法中調用了方法b(),而在方法 b() 中又調用了方法 c(),即 a–>b—>c。若是 a,b,c 如今都須要使用一個字符串參數 args。

  經常使用的作法是 a(String args)–>b(String args)—c(String args)。可是使用ThreadLocal,能夠用另一種方式解決:

    在某個接口中定義一個ThreadLocal 對象
    方法 a()、b()、c() 所在的類實現該接口
    在方法 a()中,使用 threadLocal.set(String args) 把 args 參數放入 ThreadLocal 中
    方法 b()、c() 能夠在不用傳參數的前提下,在方法體中使用 threadLocal.get() 方法就能夠獲得 args 參數
示例以下:

interface ThreadArgs {
    ThreadLocal threadLocal = new ThreadLocal();
}

class A implements ThreadArgs {
    public static void a(String args) {
        threadLocal.set(args);
    }
}

class B implements ThreadArgs {
    public static void b() {
        System.out.println(threadLocal.get());
    }
}

class C implements ThreadArgs {
    public static void c() {
        System.out.println(threadLocal.get());
    }
}

public class ThreadLocalDemo {
    public static void main(String[] args) {
        A.a(「hello」);
        B.b();
        C.c();
    }
}

輸出結果:

hello
hello

關於 InheritableThreadLocal
  InheritableThreadLocal 類是 ThreadLocal 類的子類。ThreadLocal 中每一個線程擁有它本身的值,與 ThreadLocal 不一樣的是,InheritableThreadLocal 容許一個線程以及該線程建立的全部子線程均可以訪問它保存的值。

 

跟詳細的關於ThreadLocal的分析見這一篇:  ThreadLocal的實現原理 

 

總結:

  ThreadLocal就是用來在類中聲明的一個標記,而後經過這個標記就根據不一樣Thread對象存取值。

相關文章
相關標籤/搜索