ThreadLocal使用原理、注意問題、使用場景

想必不少朋友對ThreadLocal並不陌生,今天咱們就來一塊兒探討下ThreadLocal的使用方法和實現原理。首先,本文先談一下對ThreadLocal的理解,而後根據ThreadLocal類的源碼分析了其實現原理和使用須要注意的地方,最後給出了兩個應用場景。java

一.對ThreadLocal的理解數據庫

        ThreadLocal,不少地方叫作線程本地變量,也有些地方叫作線程本地存儲,其實意思差很少。可能不少朋友都知道ThreadLocal爲變量在每一個線程中都建立了一個副本,那麼每一個線程能夠訪問本身內部的副本變量。安全

        這句話從字面上看起來很容易理解,可是真正理解並非那麼容易。服務器

        咱們仍是先來看一個例子:多線程

class ConnectionManager {  
   
 private static Connection connect = null;  
   
 public static Connection openConnection() {  
 if(connect == null){  
 connect = DriverManager.getConnection();  
 }  
 return connect;  
 }  
   
 public static void closeConnection() {  
 if(connect!=null)  
 connect.close();  
 }  
}  

  

 假設有這樣一個數據庫連接管理類,這段代碼在單線程中使用是沒有任何問題的,可是若是在多線程中使用呢?很顯然,在多線程中使用會存在線程安全問題:第一,這裏面的2個方法都沒有進行同步,極可能在openConnection方法中會屢次建立connect;第二,因爲connect是共享變量,那麼必然在調用connect的地方須要使用到同步來保障線程安全,由於極可能一個線程在使用connect進行數據庫操做,而另一個線程調用closeConnection關閉連接。源碼分析

        因此出於線程安全的考慮,必須將這段代碼的兩個方法進行同步處理,而且在調用connect的地方須要進行同步處理。性能

        這樣將會大大影響程序執行效率,由於一個線程在使用connect進行數據庫操做的時候,其餘線程只有等待。this

        那麼你們來仔細分析一下這個問題,這地方到底需不須要將connect變量進行共享?事實上,是不須要的。假如每一個線程中都有一個connect變量,各個線程之間對connect變量的訪問其實是沒有依賴關係的,即一個線程不須要關心其餘線程是否對這個connect進行了修改的。spa

        到這裏,可能會有朋友想到,既然不須要在線程之間共享這個變量,能夠直接這樣處理,在每一個須要使用數據庫鏈接的方法中具體使用時才建立數據庫連接,而後在方法調用完畢再釋放這個鏈接。好比下面這樣:線程

class ConnectionManager {  
   
 private Connection connect = null;  
   
 public Connection openConnection() {  
 if(connect == null){  
 connect = DriverManager.getConnection();  
 }  
 return connect;  
 }  
   
 public void closeConnection() {  
 if(connect!=null)  
 connect.close();  
 }  
}  
   
class Dao{  
 public void insert() {  
 ConnectionManager connectionManager = new ConnectionManager();  
 Connection connection = connectionManager.openConnection();  
   
 //使用connection進行操做  
   
 connectionManager.closeConnection();  
 }  
}  

  

 這樣處理確實也沒有任何問題,因爲每次都是在方法內部建立的鏈接,那麼線程之間天然不存在線程安全問題。可是這樣會有一個致命的影響:致使服務器壓力很是大,而且嚴重影響程序執行性能。因爲在方法中須要頻繁地開啓和關閉數據庫鏈接,這樣不只嚴重影響程序執行效率,還可能致使服務器壓力巨大。

        那麼這種狀況下使用ThreadLocal是再適合不過的了,由於ThreadLocal在每一個線程中對該變量會建立一個副本,即每一個線程內部都會有一個該變量,且在線程內部任何地方均可以使用,線程之間互不影響,這樣一來就不存在線程安全問題,也不會嚴重影響程序執行性能。

        可是要注意,雖然ThreadLocal可以解決上面說的問題,可是因爲在每一個線程中都建立了副本,因此要考慮它對資源的消耗,好比內存的佔用會比不使用ThreadLocal要大。

 

二.深刻解析ThreadLocal類

        在上面談到了對ThreadLocal的一些理解,那咱們下面來看一下具體ThreadLocal是如何實現的。

        先了解一下ThreadLocal類提供的幾個方法:

public T get() { }  
public void set(T value) { }  
public void remove() { }  
protected T initialValue() { }  

get()方法是用來獲取ThreadLocal在當前線程中保存的變量副本,set()用來設置當前線程中變量的副本,remove()用來移除當前線程中變量的副本,initialValue()是一個protected方法,通常是用來在使用時進行重寫的,它是一個延遲加載方法,下面會詳細說明。

        首先咱們來看一下ThreadLocal類是如何爲每一個線程建立一個變量的副本的。

        先看下get方法的實現:


        第一句是取得當前線程,而後經過getMap(t)方法獲取到一個map,map的類型爲ThreadLocalMap。而後接着下面獲取到<key,value>鍵值對,注意這裏獲取鍵值對傳進去的是 this,而不是當前線程t。

        若是獲取成功,則返回value值。

        若是map爲空,則調用setInitialValue方法返回value。

        咱們上面的每一句來仔細分析:

        首先看一下getMap方法中作了什麼:


        可能你們沒有想到的是,在getMap中,是調用當期線程t,返回當前線程t中的一個成員變量threadLocals。

        那麼咱們繼續取Thread類中取看一下成員變量threadLocals是什麼:


        實際上就是一個ThreadLocalMap,這個類型是ThreadLocal類的一個內部類,咱們繼續取看ThreadLocalMap的實現:


        能夠看到ThreadLocalMap的Entry繼承了WeakReference,而且使用ThreadLocal做爲鍵值。

        而後再繼續看setInitialValue方法的具體實現:


        很容易瞭解,就是若是map不爲空,就設置鍵值對,爲空,再建立Map,看一下createMap的實現:


        至此,可能大部分朋友已經明白了ThreadLocal是如何爲每一個線程建立變量的副本的:

        首先,在每一個線程Thread內部有一個ThreadLocal.ThreadLocalMap類型的成員變量threadLocals,這個threadLocals就是用來存儲實際的變量副本的,鍵值爲當前ThreadLocal變量,value爲變量副本(即T類型的變量)。

        初始時,在Thread裏面,threadLocals爲空,當經過ThreadLocal變量調用get()方法或者set()方法,就會對Thread類中的threadLocals進行初始化,而且以當前ThreadLocal變量爲鍵值,以ThreadLocal要保存的副本變量爲value,存到threadLocals。

        而後在當前線程裏面,若是要使用副本變量,就能夠經過get方法在threadLocals裏面查找。

        下面經過一個例子來證實經過ThreadLocal能達到在每一個線程中建立變量副本的效果:

package com.bijian.study;  
  
public class Test {  
      
    ThreadLocal<Long> longLocal = new ThreadLocal<Long>();  
    ThreadLocal<String> stringLocal = new ThreadLocal<String>();  
  
    public void set() {  
        longLocal.set(Thread.currentThread().getId());  
        stringLocal.set(Thread.currentThread().getName());  
    }  
  
    public long getLong() {  
        return longLocal.get();  
    }  
  
    public String getString() {  
        return stringLocal.get();  
    }  
  
    public static void main(String[] args) throws InterruptedException {  
        final Test test = new Test();  
  
        test.set();  
        System.out.println(test.getLong());  
        System.out.println(test.getString());  
  
        Thread thread1 = new Thread() {  
            public void run() {  
                test.set();  
                System.out.println(test.getLong());  
                System.out.println(test.getString());  
            };  
        };  
        thread1.start();  
        thread1.join();  
  
        System.out.println(test.getLong());  
        System.out.println(test.getString());  
    }  
}  

運行結果:

1  
main  
11  
Thread-0  
1  
main  

從這段代碼的輸出結果能夠看出,在main線程中和thread1線程中,longLocal保存的副本值和stringLocal保存的副本值都不同。最後一次在main線程再次打印副本值是爲了證實在main線程中和thread1線程中的副本值確實是不一樣的。

        總結一下:

        1)實際的經過ThreadLocal建立的副本是存儲在每一個線程本身的threadLocals中的;

        2)爲什麼threadLocals的類型ThreadLocalMap的鍵值爲ThreadLocal對象,由於每一個線程中可有多個threadLocal變量,就像上面代碼中的longLocal和stringLocal;

        3)在進行get以前,必須先set,不然會報空指針異常;

        若是想在get以前不須要調用set就能正常訪問的話,必須重寫initialValue()方法。

        由於在上面的代碼分析過程當中,咱們發現若是沒有先set的話,即在map中查找不到對應的存儲,則會經過調用setInitialValue方法返回i,而在setInitialValue方法中,有一個語句是T value = initialValue(), 而默認狀況下,initialValue方法返回的是null。

3、ThreadLocal 存在的坑:

一、看下面這個例子:
 

Java代碼  收藏代碼
package com.bijian.study;  
  
public class Test02 {  
  
    ThreadLocal<Long> longLocal = new ThreadLocal<Long>();  
    ThreadLocal<String> stringLocal = new ThreadLocal<String>();  
  
    public void set() {  
        longLocal.set(Thread.currentThread().getId());  
        stringLocal.set(Thread.currentThread().getName());  
    }  
  
    public long getLong() {  
        return longLocal.get();  
    }  
  
    public String getString() {  
        return stringLocal.get();  
    }  
  
    public static void main(String[] args) throws InterruptedException {  
        final Test02 test = new Test02();  
  
        System.out.println(test.getLong());  
        System.out.println(test.getString());  
  
        Thread thread1 = new Thread() {  
            public void run() {  
                test.set();  
                System.out.println(test.getLong());  
                System.out.println(test.getString());  
            };  
        };  
        thread1.start();  
        thread1.join();  
  
        System.out.println(test.getLong());  
        System.out.println(test.getString());  
    }  
}  

運行結果:

Exception in thread "main" java.lang.NullPointerException  
    at com.bijian.study.Test02.getLong(Test02.java:14)  
    at com.bijian.study.Test02.main(Test02.java:24) 

在main線程中,沒有先set,直接get的話,運行時會報空指針異常。

        可是若是改爲下面這段代碼,即重寫了initialValue方法:

package com.bijian.study;  
  
public class Test03 {  
  
    ThreadLocal<Long> longLocal = new ThreadLocal<Long>() {  
        protected Long initialValue() {  
            return Thread.currentThread().getId();  
        };  
    };  
      
    ThreadLocal<String> stringLocal = new ThreadLocal<String>() {  
        protected String initialValue() {  
            return Thread.currentThread().getName();  
        };  
    };  
  
    public void set() {  
        longLocal.set(Thread.currentThread().getId());  
        stringLocal.set(Thread.currentThread().getName());  
    }  
  
    public long getLong() {  
        return longLocal.get();  
    }  
  
    public String getString() {  
        return stringLocal.get();  
    }  
  
    public static void main(String[] args) throws InterruptedException {  
        final Test03 test = new Test03();  
  
        //test.set();  
        System.out.println(test.getLong());  
        System.out.println(test.getString());  
  
        Thread thread1 = new Thread() {  
            public void run() {  
                //test.set();  
                System.out.println(test.getLong());  
                System.out.println(test.getString());  
            };  
        };  
        thread1.start();  
        thread1.join();  
  
        System.out.println(test.getLong());  
        System.out.println(test.getString());  
    }  
}  

運行結果:

1  
main  
8  
Thread-0  
1  
main 

二、

ThreadLocal<T>變量通常要聲名成static類型,即當前線程中只有一個T類型變量的實例,線程內可共享該實例數據且不會出問題,如將其聲名成非static,則一個線程內就存儲多個T類型變量的實例,有點存儲空間的浪費,通常不多有這樣的應用場景。另外根據實際狀況,ThreadLocal變量聲名時也多加上private final關鍵詞代表它時類內私有、引用不可修改,
在線程池環境下,因爲線程是一直運行且複用的,使用ThreadLocal<T>時會出現這個任務看到上個任務ThreadLocal變量值以及內存泄露等問題,解決方法就是在當前任務執行完後將ThreadLocal變量remove或設置爲初始值
經過上面的分析。咱們可以認識到ThreadLocal事實上是與線程綁定的一個變量,如此就會出現一個問題:假設沒有將ThreadLocal內的變量刪除(remove)或替換,它的生命週期將會與線程共存,若是不remove掉,極可能會出現內存泄漏的問題。

四.ThreadLocal的應用場景

        最多見的ThreadLocal使用場景爲 用來解決數據庫鏈接、Session管理等。如:

        數據庫鏈接:

 

private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {  
    public Connection initialValue() {  
        return DriverManager.getConnection(DB_URL);  
    }  
};  
  
public static Connection getConnection() {  
    return connectionHolder.get();  
}  

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;  
}  
相關文章
相關標籤/搜索