在 Java 多線程中仲锄,為了保證多個線程對變量的安全訪問拓型,可以將變量放到 ThreadLocal
類型的對象中如失,使變量在每個線程中都有獨立值。ThreadLocal
類通常被翻譯為線程本地變量或線程局部變量策彤。
ThreadLocal 的基本使用
ThreadLocal
中主要包含如下方法:
-
set(T value)
,設置當前線程在線程本地變量實例中綁定的值匣摘。 -
T get()
店诗,獲取當前線程在線程本地變量實例中綁定的值。 -
remove()
音榜,移除當前線程在線程本地變量實例中綁定的值必搞。
看個簡單的例子:
public class ThreadLocalDemo {
static class Foo {
static final AtomicInteger AMOUNT = new AtomicInteger(0);
int index = 0;
int bar = 10;
public Foo() {
index = AMOUNT.incrementAndGet();
}
public int getBar() {
return bar;
}
public void setBar(int bar) {
this.bar = bar;
}
@Override
public String toString() {
return index + "@Foo{bar=" + bar + "}";
}
}
private static final ThreadLocal<Foo> LOCAL_FOO = new ThreadLocal<>();
public static void main(String[] args) {
ExecutorService pool = Executors.newCachedThreadPool();
for (int i = 0; i < 3; i++) {
pool.execute(new Runnable() {
@Override
public void run() {
if (LOCAL_FOO.get() == null) {
LOCAL_FOO.set(new Foo());
}
System.out.println("線程的初始本地值: " + LOCAL_FOO.get());
try {
for (int i = 0; i < 3; i++) {
Foo foo = LOCAL_FOO.get();
foo.setBar(foo.getBar() + 1);
Thread.sleep(10);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("最終線程值: " + LOCAL_FOO.get());
LOCAL_FOO.remove();
}
});
}
}
}
運行結果如下:
如果第一次不想判斷是否為空,還可以使用如下方式在定義的時候設置初始值囊咏。
ThreadLocal<Foo> LOCAL_FOO = ThreadLocal.withInitial(() -> new Foo());
ThreadLocal的使用場景
ThreadLocal
是解決線程安全問題一個較好的方案恕洲,它通過為每個線程提供一個獨立的值去解決并發(fā)的沖突問題。
線程隔離
ThreadLocal
主要用于線程隔離梅割,防止自己的變量被其它線程修改霜第,而且可以避免同步鎖帶來的性能損失。
ThreadLocal
線程隔離的使用案例包括為每個線程綁定一個用戶會話信息户辞、數(shù)據(jù)庫連接泌类、HTTP請求等。常見使用場景為數(shù)據(jù)庫連接獨享底燎、Session數(shù)據(jù)管理等刃榨。
看一段 Hibernate 中的代碼:
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;
}
跨函數(shù)傳遞數(shù)據(jù)
通常用于一個線程內(nèi),跨類双仍、跨方法傳遞數(shù)據(jù)時枢希,在某一個地方進行了設置,在隨后同一個線程的任意地方都可以獲取朱沃。
典型應用包括:
- 用來傳遞請求過程中的用戶ID苞轿。
- 用來傳遞請求過程中的用戶會話茅诱。
- 用來傳遞HTTP用戶的請求實例的
HttpRequest
。
下面是一段參考代碼:
public class SessionHolder {
private static final ThreadLocal<String> sidLocal = new ThreadLocal<>("sidLocal");
private static final ThreadLocal<UserDTO> sessionUserLocal = new ThreadLocal<>("sessionUserLocal");
private static final ThreadLocal<HttpSession> sessionLocal = new ThreadLocal<>("sessionLocal");
...
public static void setSession(HttpSession session) {
sessionLocal.set(session);
}
public static HttpSession getSession() {
HttpSession session = sessionLocal.get();
Assert.notNull(session, "session not set");
return session;
}
}
ThreadLocal 源碼分析
本部分看下 ThreadLocal
源碼搬卒。
set方法
set(T value)
方法用于設置本地線程變量瑟俭,核心源碼如下:
public void set(T value) {
// 獲取當前線程
Thread t = Thread.currentThread();
// 獲取當前線程的 ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
大致流程:
- 獲取當前線程,并獲取當前線程的
ThreadLocalMap
成員傳入map
契邀。 - 如果
map
不為空摆寄,就將value
設置到map
中,當前的ThreadLocal
作為鍵坯门。 - 如果
map
為空椭迎,就位該線程創(chuàng)建map
,然后將value
加入其中田盈。
get方法
get()
方法源碼如下:
public T get() {
// 獲取當前線程
Thread t = Thread.currentThread();
// 獲取線程的 ThreadLocalMap
ThreadLocalMap map = getMap(t);
// 如果map不為空
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 如果map為空
return setInitialValue();
}
// 設置 ThreadLocal 關聯(lián)的初始值并返回
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;
}
大致流程如下:
- 獲取當前線程畜号,并獲取當前線程的
ThreadLocalMap
成員,暫存于map
變量允瞧。 - 如果獲得的
map
不為空简软,那么以當前的ThreadLocal
實例為鍵獲取map
中的記錄。 - 如果記錄的值不為空述暂,就返回該值痹升。
- 如果記錄為空,就利用
initialValue()
初始化鉤子函數(shù)獲取初始值畦韭,被設置在map
中疼蛾。
remove 方法
remove()
方法用于移除線程本地變量中的值,源碼如下:
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
initialValue 方法
initialValue()
方法用于獲取初始值艺配,源碼如下:
protected T initialValue() {
return null;
}
默認返回 null察郁,一般使用 ThreadLocal.withInitial(...)
靜態(tài)工廠方法來在定義 ThreadLocal
實例時設置初始值的回調(diào)函數(shù)。
靜態(tài)工廠方法源碼如下:
static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
private final Supplier<? extends T> supplier;
SuppliedThreadLocal(Supplier<? extends T> supplier) {
this.supplier = Objects.requireNonNull(supplier);
}
@Override
protected T initialValue() {
return supplier.get();
}
}
使用示例:
ThreadLocal<Foo> LOCAL_FOO = ThreadLocal.withInitial(() -> new Foo());
ThreadLocalMap 源碼分析
ThreadLocal
的操作都是基于 ThreadLocalMap
展開的转唉,而 ThreadLocalMap
是 ThreadLocal
的一個內(nèi)部靜態(tài)類皮钠,實現(xiàn)了一套簡單的 Map
結構。
ThreadLocalMap
的成員變量如下:
static class ThreadLocalMap {
// 條目類型赠法,一個靜態(tài)內(nèi)部類
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
// 初始容量
private static final int INITIAL_CAPACITY = 16;
// 條目數(shù)組麦轰,作為哈希表使用
private Entry[] table;
// 當前條目數(shù)量
private int size = 0;
// 擴容因子
private int threshold; // Default to 0
...
然后看一下它的 set
方法。
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
// 根據(jù) key 的 HashCode 找到插槽
int i = key.threadLocalHashCode & (len-1);
// 從i開始向后循環(huán)搜索
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;
}
}
// 沒有找到插槽款侵,增加新的 Entry
tab[i] = new Entry(key, value);
// 數(shù)量增加
int sz = ++size;
// 清理 key 為 null 的無效 Entry
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
另外,需要注意 Entry
使用了弱引用(WeakReference
)侧纯。
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
這里簡單介紹下原因新锈。首先介紹下弱引用的特點,僅有弱引用指向的對象只能生存到下一次垃圾回收之前茂蚓。因此壕鹉,在下次 GC 發(fā)生時剃幌,就可以使用那些沒有被其他強引用指向聋涨、僅被 Entry
的 Key
所指向的 ThreadLocal
實例能被順利回收晾浴。回收之后牍白,Entry
的 Key
變?yōu)榭占够耍罄m(xù)調(diào)用 get()
、set()
或 remove()
的時候會清空這些 Entry
茂腥。
ThreadLocal 使用規(guī)范
在如下條件下狸涌,ThreadLocal
有可能發(fā)生內(nèi)存泄露:
- 線程長時間運行而沒有被銷毀,線程池中的
Thread
實例很容易滿足此條件最岗。 -
ThreadLocal
引用內(nèi)設置為null帕胆,且后續(xù)在同一Thread
實例執(zhí)行期間,沒有發(fā)生其它ThreadLocal
實例的get()
般渡、set()
或remove()
操作懒豹。
建議使用 ThreadLocal
時遵循以下原則:
- 盡量使用
private static final
修飾ThreadLocal
實例,private
和final
修飾符保證盡可能不讓他人修改驯用,static
修飾符保證實例全局唯一脸秽。 -
ThreadLocal
使用完成之后務必調(diào)用remove()
方法。