ThreadLocal使用原理、註意問題、使用場景
想必很多朋友對ThreadLocal並不陌生,今天我們就來一起探討下ThreadLocal的使用方法和實現原理。首先,本文先談一下對ThreadLocal的理解,然後根據ThreadLocal類的源碼分析了其實現原理和使用需要註意的地方,最後給出了兩個應用場景。
一.對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進行數據庫操作的時候,其他線程只有等待。
那麽大家來仔細分析一下這個問題,這地方到底需不需要將connect變量進行共享?事實上,是不需要的。假如每個線程中都有一個connect變量,各個線程之間對connect變量的訪問實際上是沒有依賴關系的,即一個線程不需要關心其他線程是否對這個connect進行了修改的。
到這裏,可能會有朋友想到,既然不需要在線程之間共享這個變量,可以直接這樣處理,在每個需要使用數據庫連接的方法中具體使用時才創建數據庫鏈接,然後在方法調用完畢再釋放這個連接。比如下面這樣:
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。
三、ThreadLocal 存在的坑:
1、看下面這個例子:
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
2、
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; }
ThreadLocal使用原理、註意問題、使用場景