一、關于線程本地存儲
線程本地存儲是一種自動化機制,可以為使用相同變量的每個不同的線程都創(chuàng)建不同的存儲,通過根除對變量的共享來防止任務在共享資源時產生沖突。
因此,如果你有5個線程都要使用變量x所表示的對象,那么線程本地存儲就會生成5個用于x的不同的存儲塊,并且使得你可以將狀態(tài)與線程關聯起來。
二、ThreadLocal是什么?
ThreadLocal是線程本地存儲的一種實現方案。它并不是一個Thread,我們也可以稱之為線程局部變量,在多線程并發(fā)訪問時,ThreadLocal類為每個使用該變量的線程都創(chuàng)建一個變量值的副本,每一個線程都可以獨立地改變自己的副本,而不會和其他線程的副本發(fā)生沖突。從線程的角度來看,就感覺像是每個線程都完全擁有該變量一樣。
三、什么情況下使用ThreadLocal?
ThreadLocal的使用場合主要用來解決多線程情況下對數據的讀取因線程并發(fā)而產生數據不一致的問題。ThreadLocal為每個線程中并發(fā)訪問的數據提供一個本地副本,然后通過對這個本地副本的訪問來執(zhí)行具體的業(yè)務邏輯操作,這樣就可以大大減少線程并發(fā)控制的復雜度;然而這樣做也需要付出一定的代價,需要耗費一部分內存資源,但是相比于線程同步所帶來的性能消耗還是要好上那么一點點。
四、ThreadLocal的應用場景
最常見的ThreadLocal的使用場景是用來解決數據庫連接、Session管理等等。如:
4.1、 數據庫連接管理:
同一事務多DAO共享同一Connection,必須在一個共同的外部類中使用threadLocal保存Connection。
public class ConnectionManager {
private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {
@Override
protected Connection initialValue() {
Connection conn = null;
try {
conn = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/test", "username",
"password");
} catch (SQLException e) {
e.printStackTrace();
}
return conn;
}
};
public static Connection getConnection() {
return connectionHolder.get();
}
public static void setConnection(Connection conn) {
connectionHolder.set(conn);
}
}
通過上面這種方式就保證了一個線程對應一個數據庫連接,保證了事務。因為一般事務都是依賴一個個數據庫連接來控制的,如commit,rollback等都是需要獲取數據庫連接來操作的。
4.2、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
直接看代碼:
package com.feizi.java.concurrency.tool;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
* Created by feizi on 2018/5/29.
*/
public class ThreadLocalHolder {
private static ThreadLocal<Integer> holder = new ThreadLocal<Integer>(){
private Random rand = new Random(10);
protected synchronized Integer initialValue(){
return rand.nextInt(100);
}
};
public static void increment(){
holder.set(holder.get() + 1);
}
public static Integer get(){
return holder.get();
}
public static void main(String[] args) throws InterruptedException {
ExecutorService threadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++){
threadPool.execute(new Accessor(i));
}
threadPool.shutdown();
}
}
class Accessor implements Runnable{
private final int id;
public Accessor(int id) {
this.id = id;
}
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()){
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
ThreadLocalHolder.increment();
System.out.println(this);
Thread.yield();
}
}
@Override
public String toString() {
return "#" + id + " : " + ThreadLocalHolder.get();
}
}
控制臺輸出結果:
#線程#3 : 14
#線程#0 : 81
#線程#4 : 94
#線程#2 : 91
#線程#1 : 47
#線程#1 : 48
#線程#4 : 95
#線程#3 : 15
#線程#2 : 92
#線程#0 : 82
#線程#3 : 16
#線程#0 : 83
#線程#4 : 96
#線程#2 : 93
#線程#1 : 49
#線程#2 : 94
#線程#3 : 17
從上面輸出結果,我們看到:每個線程的輸出的結果都是隔離的,相互并不影響,#線程#3首次輸出14,到了下次再輸出的時候變成15,#線程#0首次輸出81,再次輸出82,其他類似。
因為每個單獨的線程都被分配了自己的存儲,因為它們每個都需要跟蹤自己的計數值,即便只有一個ThreadLocalHolder對象。
六、ThreadLocal的實現
ThreadLocal中主要提供的方法:
1、public T get(){}
主要用于獲取ThreadLocal在當前線程中保存的變量副本
2、public void set(T value) {}
主要用于設置當前線程中變量的副本
3、public void remove() {}
主要用于移除當前線程中變量的副本
4、protected T initialValue() {}
它是一個被protected修飾的方法,主要用于在實例化時進行重載的,是一個延時加載方法
6.1、get()方法的實現:
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調用getMap(t)方法獲取一個ThreadLocalMap類型的map,之后判斷這個map是否為空,如果不為空,則根據this(表示當前ThreadLocal對象)獲取一個<key,value>鍵值對的的Entry,需要注意的是,這里傳入的this,而不是當前線程t,如果map為空,則調用setInitialValue()初始化一個value,默認是返回null。
然后,我們跟一下getMap(t)中做了什么操作:
ThreadLocalMap getMap(Thread t) {
//返回當前線程t中的一個成員變量threadLocals
return t.threadLocals;
}
從上面可以看到,getMap(t)中返回了當前線程t中的一個成員變量threadLocals,接著再繼續(xù)往下跟,看一下threadLocals是什么東西:
ThreadLocal.ThreadLocalMap threadLocals = null;
可以看到,threadLocals實際就是一個ThreadLocalMap,這個類是ThreadLocald的一個內部類,然后我們再看一下ThreadLocalMapd的定義:
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
/**
* The initial capacity -- MUST be a power of two.
*/
private static final int INITIAL_CAPACITY = 16;
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;
/**
* The number of entries in the table.
*/
private int size = 0;
/**
* The next size value at which to resize.
*/
private int threshold; // Default to 0
}
從上面的定義我們大致可以看出,ThreadLocalMap的鍵值對Entry類繼承了WeakReference,并且使用ThreadLocal<?>作為key值進行存儲。
6.2、setInitialValue()方法的實現
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()進行初始化value值,我們跟一下這個initialValue()方法:
protected T initialValue() {
return null;
}
從上述initialValue()方法中,我們可以看到直接return返回了一個null,獲取當前線程t,根據當前線程t獲取ThreadLocalMap類型的map,此時再判斷map是否為空,不為空則直接設置<key,value>鍵值對,注意此處的key仍然還是this(表示當前threadLocal對象),為空則調用createMap(Thread t, T firstValue)方法:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
從上面代碼我們可以看出,直接new了一個ThreadLocalMap對象,以this(當前threadLocal對象)作為key,傳入的value設置為值,并且賦給當前線程t的成員變量threadLocals。
6.3、set()方法的實現:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
首先獲取當前線程t,然后根據當前線程t獲取ThreadLocalMap類型的map,判斷map不為空就設置鍵值對,為空就調用createMap初始化一個新的map。
6.4、remove()方法的實現:
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
首先獲取當前線程t,根據當前線程t獲取ThreadLocalMap,判斷不為空,就根據this(當前threadLocal對象)移除相對應的value。
通過上面的分析,我們就大致明白了ThreadLocal的基本工作原理:
首先,每個線程Thread內部都擁有一個threadLocals變量(這個是在Thread類中定義的),這個threadLocals是ThreadLocal.ThreadLocalMap類型的,也就是一個Map,這個map是整個threadLocal得以實現的核心,它用于存儲變量的副本。key值為this(即當前threadLocal對象),value為變量副本(T類型的變量)。
當我們new一個ThreadLocal對象時,即初始化ThreadLocal),這個Thread類的threadLocals為null,然后進行get()或者set()的時候,都需要對這個Thread類的threadLocals進行初始化操作(步驟都是先獲取當前線程t,然后根據t獲取ThreadLocalMap,判斷如果為空,就初始化new一個),然后以this(當前ThreadLocal變量)為key,以threadLocal需要保存的副本變量為value,存到Thread類的threadLocals中。之后,在線程里面,如果需要使用變量副本,就可以通過get()方法,根據當前線程t去獲取threadLocals中對應存儲的value副本值。
ok,上面還是有些啰嗦,我們再總結一下:
- Thread類中定義了一個ThreadLocal.ThreadLocalMap類型的成員變量threadLocals,用于保存變量的副本
- ThreadLocal類中定義了一個ThreadLocalMap的靜態(tài)內部類
- ThreadLocalMap類中定義了一個繼承WeakReference類的Entry鍵值對,并且這個Entry鍵值對有些特殊,特殊之處就在于它的key必須是ThreadLocal類型的
- threadLocals在保存變量副本的時候,以this(當前ThreadLocal變量)為key,以傳入需要保存的變量副本為value進行存儲
- ThreadLocal在get()的時候,會先獲取當前線程t,然后根據t去獲取ThreadLocalMap,之后對這個ThreadLocalMap進行判空,如果不為空,則根據this(當前ThreadLocal變量)獲取ThreadLocalMap類的Entry鍵值對,再對Entry鍵值對進行判空,如果不為空就取出變量副本value進行return,如果ThreadLocalMap為空,就調用setInitialValue()方法就行初始化,并且返回一個null的value默認值。
- ThreadLocal在set()的時候,也會先獲取當前線程t,然后根據t去獲取一個ThreadLocalMap,之后對這個ThreadLocalMap進行判空,如果不為空,就以this(當前ThreadLocal變量)為key,需要保存的變量副本為value設置鍵值對,否則就調用createMap初始化一個ThreadLocalMap。
- ThreadLocal在setInitialValue()的時候,同上面的set()過程類似,唯一的區(qū)別是setInitialValue()方法會返回一個默認值為null的value(需要對value初始化)
- ThreadLocal在remove()的時候,同樣也會先獲取當前線程t,然后根據t獲取一個ThreadLocalMap,之后再對這個ThreadLocalMap進行判空,如果不為空,則根據this(當前ThreadLocal變量)移除對應存儲的變量副本value。
其實簡單來說,大致就是每個線程都維護了一個map,而這個map的key就是當前threadLocal變量,而值則是我們需要set的那個變量的value,之后每次線程在get取值的時候都是從自己的變量中取值,既然是從自己的變量中取值,那么當然也就不存在線程安全的問題了。ThreadLocal只是充當一個key的角色,然后順帶給每個線程提供一個初始值。
多線程安全性解決方案
- 采用synchronized進行同步控制,但是效率略低,使得并發(fā)變同步(串行)
- 采用ThreadLocal線程本地存儲,為每個使用該變量的線程都存儲一個本地變量副本(線程互不相干)
兩種線程安全方案的區(qū)別
- synchronized同步機制采用了“以時間換空間”的方式,僅僅只提供一份變量,讓參與的多個不同的線程排隊進行訪問
- ThreadLocal采用“以空間換時間”的方式,為參與的每個線程都各自提供一份本地副本,因此可以做到同時訪問而互不影響。
綜上所述,ThreadLocal通常占用內存較大,但是速度快;而synchronized則占用內存小,速度相對而言比較慢。如果在內存比較充足的情況,對并發(fā)部分的執(zhí)行效率要求很高的話,那么就是ThreadLocal派上用場的時候了,一般情況下還是synchronized用的居多。