新聞中心
大家好,歡迎來到Tlog4J課堂,我是Jensen,今天咱們來簡單聊聊ThreadLocal。

成都創(chuàng)新互聯專注于企業(yè)成都營銷網站建設、網站重做改版、太倉網站定制設計、自適應品牌網站建設、成都h5網站建設、商城網站制作、集團公司官網建設、成都外貿網站制作、高端網站制作、響應式網頁設計等建站業(yè)務,價格優(yōu)惠性價比高,為太倉等各大城市提供網站開發(fā)制作服務。
ThreadLocal是通過線程隔離的方式防止任務在共享資源上產生沖突, 線程本地存儲是一種自動化機制,可以為使用相同變量的每個不同線程都創(chuàng)建不同的存儲。
ThreadLocal簡介
線程安全(是指廣義上的共享資源訪問安全性,因為線程隔離是通過副本保證本線程訪問資源安全性,它不保證線程之間還存在共享關系的狹義上的安全性)的解決思路:
- 互斥同步:synchronized 和 ReentrantLock。
- 非阻塞同步:CAS, AtomicXxx。
- 無同步方案:棧封閉,本地存儲(Thread Local),可重入代碼。
本地存儲(Thread Local),在官網的解釋是這樣的:
This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its {@code get} or {@code set} method) has its own, independently initialized copy of the variable. {@code ThreadLocal} instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID)
該類提供了線程局部 (thread-local) 變量。這些變量不同于它們的普通對應物,因為訪問某個變量(通過其 get 或 set 方法)的每個線程都有自己的局部變量,它獨立于變量的初始化副本。ThreadLocal 實例通常是類中的 private static 字段,它們希望將狀態(tài)與某一個線程(例如,用戶 ID 或事務 ID)相關聯。
總結而言:ThreadLocal是一個將在多線程中為每一個線程創(chuàng)建單獨的變量副本的類;當使用ThreadLocal來維護變量時, ThreadLocal會為每個線程創(chuàng)建單獨的變量副本, 避免因多線程操作共享變量而導致的數據不一致的情況。
ThreadLocal理解
提到ThreadLocal被提到應用最多的是session管理和數據庫鏈接管理,這里以數據訪問為例幫助你理解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方法中會多次創(chuàng)建connect。
第二,由于connect是共享變量,那么必然在調用connect的地方需要使用到同步來保障線程安全,因為很可能一個線程在使用connect進行數據庫操作,而另外一個線程調用closeConnection關閉鏈接。
為了解決上述線程安全的問題,第一考慮:互斥同步
你可能會說,將這段代碼的兩個方法進行同步處理,并且在調用connect的地方需要進行同步處理,比如用Synchronized或者ReentrantLock互斥鎖。
這里再拋出一個問題:這地方到底需不需要將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();
}
}
這樣處理確實也沒有任何問題,由于每次都是在方法內部創(chuàng)建的連接,那么線程之間自然不存在線程安全問題。
但是這樣會有一個致命的影響:導致服務器壓力非常大,并且嚴重影響程序的執(zhí)行性能。
由于在方法中需要頻繁地開啟和關閉數據庫連接,這樣不僅嚴重影響程序執(zhí)行效率,還可能導致服務器壓力巨大。
ThreadLocal登場
那么這種情況下使用ThreadLocal是再適合不過的了,因為ThreadLocal在每個線程中對該變量會創(chuàng)建一個副本,即每個線程內部都會有一個該變量,且在線程內部任何地方都可以使用,線程之間互不影響,這樣一來就不存在線程安全問題,也不會嚴重影響程序執(zhí)行性能。
下面就是網上出現最多的例子:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class ConnectionManager {
private static final ThreadLocaldbConnectionLocal = new ThreadLocal () {
@Override
protected Connection initialValue() {
try {
return DriverManager.getConnection("", "", "");
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
};
public Connection getConnection() {
return dbConnectionLocal.get();
}
}
再注意下ThreadLocal的修飾符
ThreaLocal的JDK文檔中說明:如果我們希望通過某個類將狀態(tài)(例如用戶ID、事務ID)與線程關聯起來,那么通常在這個類中定義private static類型的ThreadLocal實例。
但是要注意,雖然ThreadLocal能夠解決上面說的問題,但是由于在每個線程中都創(chuàng)建了副本,所以要考慮它對資源的消耗,比如內存的占用會比不使用ThreadLocal要大。
ThreadLocal原理
如何實現線程隔離
主要是用到了Thread對象中的一個ThreadLocalMap類型的變量threadLocals, 負責存儲當前線程的關于Connection的對象, dbConnectionLocal(以上述例子中為例) 這個變量為Key, 以新建的Connection對象為Value。
這樣的話, 線程第一次讀取的時候如果不存在就會調用ThreadLocal的initialValue方法創(chuàng)建一個Connection對象并且返回。
具體關于為線程分配變量副本的代碼如下:
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();
}
- 首先獲取當前線程對象t,然后從線程t中獲取到ThreadLocalMap的成員屬性threadLocals。
- 如果當前線程的threadLocals已經初始化(即不為null) 并且存在以當前ThreadLocal對象為Key的值,則直接返回當前線程要獲取的對象(本例中為Connection)。
- 如果當前線程的threadLocals已經初始化(即不為null)但是不存在以當前ThreadLocal對象為Key的的對象,那么重新創(chuàng)建一個Connection對象,并且添加到當前線程的threadLocals Map中,并返回。
- 如果當前線程的threadLocals屬性還沒有被初始化, 則重新創(chuàng)建一個ThreadLocalMap對象, 并且創(chuàng)建一個Connection對象并添加到ThreadLocalMap對象中并返回。
如果存在則直接返回很好理解, 那么對于初始化的代碼又是怎樣的呢?
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
- 首先調用我們上面寫的重載過后的initialValue方法,產生一個Connection對象。
- 繼續(xù)查看當前線程的threadLocals是不是空的, 如果ThreadLocalMap已被初始化, 那么直接將產生的對象添加到ThreadLocalMap中, 如果沒有初始化, 則創(chuàng)建并添加對象到其中。
同時, ThreadLocal還提供了直接操作Thread對象中的threadLocals的方法。
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
這樣也可以不實現initialValue, 將初始化工作放到DBConnectionFactory的getConnection方法中:
public Connection getConnection() {
Connection connection = dbConnectionLocal.get();
if (connection == null) {
try {
connection = DriverManager.getConnection("", "", "");
dbConnectionLocal.set(connection);
} catch (SQLException e) {
e.printStackTrace();
}
}
return connection;
}
那么看過代碼之后就很清晰的知道了為什么ThreadLocal能夠實現變量的多線程隔離了; 其實就是用了Map的數據結構給當前線程緩存了, 要使用的時候就從本線程的threadLocals對象中獲取就可以了, key就是當前線程。
當然了,在當前線程下獲取當前線程里面的Map里面的對象并操作肯定沒有線程并發(fā)問題了, 當然能做到變量的線程間隔離了。
現在知道了ThreadLocal到底是什么了, 又知道了如何使用ThreadLocal以及其基本實現原理了是不是就可以結束了呢? 其實還有一個問題就是ThreadLocalMap是個什么對象, 為什么要用這個對象呢?
ThreadLocalMap對象是什么
本質上來講,它就是一個Map, 但是這個ThreadLocalMap與我們平時見到的Map有點不一樣
- 它沒有實現Map接口。
- 它沒有public的方法, 最多有一個default的構造方法, 因為這個ThreadLocalMap的方法僅僅在ThreadLocal類中調用, 屬于靜態(tài)內部類。
- ThreadLocalMap的Entry實現繼承了WeakReference
>。 - 該方法僅僅用了一個Entry數組來存儲Key, Value; Entry并不是鏈表形式, 而是每個bucket里面僅僅放一個Entry。
要了解ThreadLocalMap的實現, 我們先從入口開始, 就是往該Map中添加一個值:
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();
}
先進行簡單的分析, 對該代碼表層意思進行解讀:
- 看下當前threadLocal的在數組中的索引位置 比如: i = 2, 看 i = 2 位置上面的元素(Entry)的Key是否等于threadLocal 這個 Key, 如果等于就很好說了, 直接將該位置上面的Entry的Value替換成最新的就可以了。
- 如果當前位置上面的 Entry 的 Key為空, 說明ThreadLocal對象已經被回收了, 那么就調用replaceStaleEntry。
- 如果清理完無用條目(ThreadLocal被回收的條目)、并且數組中的數據大小 > 閾值的時候對當前的Table進行重新哈希 所以, 該HashMap是處理沖突檢測的機制是向后移位, 清除過期條目 最終找到合適的位置。
了解完Set方法, 后面就是Get方法了:
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);
}
先找到ThreadLocal的索引位置, 如果索引位置處的entry不為空并且鍵與threadLocal是同一個對象, 則直接返回; 否則去后面的索引位置繼續(xù)查找。
ThreadLocal造成內存泄露的問題
網上有這樣一個例子:
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadLocalDemo {
static class LocalVariable {
private Long[] a = new Long[1024 * 1024];
}
// (1)
final static ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES,new LinkedBlockingQueue<>());
// (2)
final static ThreadLocallocalVariable = new ThreadLocal ();
public static void main(String[] args) throws InterruptedException {
// (3)
Thread.sleep(5000 * 4);
for (int i = 0; i < 50; ++i) {
poolExecutor.execute(new Runnable() {
public void run() {
// (4)
localVariable.set(new LocalVariable());
// (5)
System.out.println("use local varaible" + localVariable.get());
localVariable.remove();
}
});
}
// (6)
System.out.println("pool execute over");
}
}
如果用線程池來操作ThreadLocal 對象確實會造成內存泄露, 因為對于線程池里面不會銷毀的線程, 里面總會存在著的強引用。
這是因為final static 修飾的 ThreadLocal 并不會釋放, 而ThreadLocalMap 對于 Key 雖然是弱引用, 但是強引用不會釋放, 弱引用當然也會一直有值, 同時創(chuàng)建的LocalVariable對象也不會釋放, 就造成了內存泄露。
如果LocalVariable對象不是一個大對象的話, 其實泄露的并不嚴重, 泄露的內存 = 核心線程數 * LocalVariable對象的大小。
所以, 為了避免出現內存泄露的情況, ThreadLocal提供了一個清除線程中對象的方法, 即 remove, 其實內部實現就是調用 ThreadLocalMap 的remove方法:
private void remove(ThreadLocal> key) {
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)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
找到Key對應的Entry, 并且清除Entry的Key(ThreadLocal)置空, 隨后清除過期的Entry即可避免內存泄露。
ThreadLocal應用場景
每個線程維護了一個“序列號”
再回想上文說的,如果我們希望通過某個類將狀態(tài)(例如用戶ID、事務ID)與線程關聯起來,那么通常在這個類中定義private static類型的ThreadLocal 實例。
public class SerialNum {
// The next serial number to be assigned
private static int nextSerialNum = 0;
private static ThreadLocal serialNum = new ThreadLocal() {
protected synchronized Object initialValue() {
return new Integer(nextSerialNum++);
}
};
public static int get() {
return ((Integer) (serialNum.get())).intValue();
}
}
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;
}
在線程內部創(chuàng)建ThreadLocal
還有一種用法是在線程類內部創(chuàng)建ThreadLocal,基本步驟如下:
- 在多線程的類(如ThreadDemo類)中,創(chuàng)建一個ThreadLocal對象threadXxx,用來保存線程間需要隔離處理的對象xxx。
- 在ThreadDemo類中,創(chuàng)建一個獲取要隔離訪問的數據的方法getXxx(),在方法中判斷,若ThreadLocal對象為null時候,應該new()一個隔離訪問類型的對象,并強制轉換為要應用的類型。
- 在ThreadDemo類的run()方法中,通過調用getXxx()方法獲取要操作的數據,這樣可以保證每個線程對應一個數據對象,在任何時刻都操作的是這個對象。
public class ThreadLocalTest implements Runnable{
ThreadLocal StudentThreadLocal = new ThreadLocal();
@Override
public void run() {
String currentThreadName = Thread.currentThread().getName();
System.out.println(currentThreadName + " is running...");
Random random = new Random();
int age = random.nextInt(100);
System.out.println(currentThreadName + " is set age: " + age);
Student Student = getStudentt(); //通過這個方法,為每個線程都獨立的new一個Studentt對象,每個線程的的Studentt對象都可以設置不同的值
Student.setAge(age);
System.out.println(currentThreadName + " is first get age: " + Student.getAge());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println( currentThreadName + " is second get age: " + Student.getAge());
}
private Student getStudentt() {
Student Student = StudentThreadLocal.get();
if (null == Student) {
Student = new Student();
StudentThreadLocal.set(Student);
}
return Student;
}
public static void main(String[] args) {
ThreadLocalTest t = new ThreadLocalTest();
Thread t1 = new Thread(t,"Thread A");
Thread t2 = new Thread(t,"Thread B");
t1.start();
t2.start();
}
}
class Student{
int age;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
java 開發(fā)手冊中推薦的 ThreadLocal
看看阿里巴巴 java 開發(fā)手冊中推薦的 ThreadLocal 的用法:
import java.text.DateFormat;
import java.text.SimpleDateFormat;
public class DateUtils {
public static final ThreadLocalthreadLocal = new ThreadLocal (){
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};
}
然后我們再要用到 DateFormat 對象的地方,這樣調用:
DateUtils.df.get().format(new Date());
文章名稱:搞懂Java并發(fā)—ThreadLocal
文章來源:http://fisionsoft.com.cn/article/dhijgsc.html


咨詢
建站咨詢
