1、ThreadLocal
1.1 定義
用來提供線程內部的局部變量,這種變量在多線程環境下訪問(通過get或者set方法訪問)時能保證各個線程的變量對應相對獨立於其他線程內的變量。
作用是:提供了線程內部的局部變量,不同的線程之間不會相互干擾,這種變量在線程的生命週期內起作用,減少同一個線程內多個函數或組件之間一些公共變量傳遞的複雜度。
1.2 示例
需求:線程隔離
在多線程併發的場景下,每個線程中變量都是相互隔離的
- 線程A:設置變量1,獲取變量1
- 線程B:設置變量2,獲取變量2
1.2.1 錯誤實現
public class ThreadLockDemo {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public static void main(String[] args) {
ThreadLockDemo demo = new ThreadLockDemo();
for (int i = 1; i <= 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
demo.setName(Thread.currentThread().getName() + "的數據");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "--->" + demo.getName());
}
}, "線程" + i).start();
}
}
}
運行結果
線程4--->線程10的數據
線程3--->線程10的數據
線程8--->線程10的數據
線程10--->線程10的數據
線程2--->線程10的數據
線程9--->線程10的數據
線程7--->線程10的數據
線程5--->線程10的數據
線程6--->線程10的數據
線程1--->線程10的數據
1.2.2 使用 ThreadLocal 解決
ThreadLocal
1、set() 將變量綁定到當前線程中
2、get() 獲取當前線程綁定的變量
public class ThreadLockDemo {
ThreadLocal<String> threadLocal = new ThreadLocal<>();
private String name;
// public String getName() {
// return name;
// }
//
// public void setName(String name) {
// this.name = name;
// }
public String getName() {
return threadLocal.get();
}
public void setName(String name) {
threadLocal.set(name);
}
public static void main(String[] args) {
ThreadLockDemo demo = new ThreadLockDemo();
for (int i = 1; i <= 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
demo.setName(Thread.currentThread().getName() + "的數據");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "--->" + demo.getName());
}
}, "線程" + i).start();
}
}
}
運行結果
線程8--->線程8的數據
線程1--->線程1的數據
線程5--->線程5的數據
線程6--->線程6的數據
線程7--->線程7的數據
線程10--->線程10的數據
線程3--->線程3的數據
線程9--->線程9的數據
線程2--->線程2的數據
線程4--->線程4的數據
1.3 使用 synchronize 來處理
1.3.1 使用synchronize 加鎖來實現
使用 synchronize 加鎖也能解決這個問題
public class ThreadLockDemo {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public static void main(String[] args) {
ThreadLockDemo demo = new ThreadLockDemo();
for (int i = 1; i <= 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
synchronized (ThreadLockDemo.class){
demo.setName(Thread.currentThread().getName() + "的數據");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "--->" + demo.getName());
}
}
}, "線程" + i).start();
}
}
}
運行也沒有問題,但是加鎖後,各個線程需要排隊進入運行,性能會降低,造成該操作不是併發運行。
1.3.2 ThreadLocal 與 synchronize 的區別
都用於處理多線程併發訪問變量的問題,不過兩者處理問題的角度和思路不同。
synchronize | ThreadLocal | |
---|---|---|
原理 | 同步機制採用 以時間換空間的方式,只提供了一份變量,讓不容的線程排隊訪問 | ThreadLocal採用以空間換時間的方式,爲每一個線程都提供了一份變量的副本,從而實現同時訪問而相不干擾 |
側重點 | 多個線程之間訪問資源的同步 | 多線程中讓每個線程之間的數據互相隔離 |
2 在數據庫連接案例中使用到了 ThreadLocal
- 傳遞數據:保存每個線程綁定的數據,在需要的地方直接獲取,避免參數直接傳遞帶來的代碼耦合問題
- 線程隔離:各線程之間的數據相互隔離卻又具備併發性,避免同步方式帶來的性能損失
3 ThreadLocal 的內部結構
JDK8 中 ThreadLocal 的設計是:
- 每一個Thread線程內部都有一個Map(ThreadLocalMap)
- Map 裏邊存儲 ThreadLocal 對象(key)和線程的變量副本(value)
- Thread 內部的Map是由ThreadLocal維護的,由ThreadLocal負責向 map 獲取和設置線程的變量值
- 對於不同的線程,每次獲取副本值時,別的線程並不能獲取到當前線程的副本值,形成了副本的隔離,互不干擾
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
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();
}
/**
* Variant of set() to establish initialValue. Used instead
* of set() in case user has overridden the set() method.
*
* @return the initial value
*/
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;
}
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
4 爲什麼要使用弱引用
無論 ThreadLocalMap 中的 key 使用哪種類型引用都無法完全避免出現內存泄漏。
要避免內存泄漏有兩種方式:
- 使用完 ThreadLocal ,調用其 remove 方法刪除對應的 Entry
- 使用完 ThreadLocal,當前Thread也隨之運行結束
第二種方式是不好控制,特別是使用線程池的時候,線程結束不會銷燬的。
也就說,在使用完後,記得調用 remove,無論是強引用還是弱引用都不會有問題,那爲什麼要使用弱引用呢?
在 ThreadLocalMap 中的 set/getEntry 的方法中,會對 key 爲 null(即 ThreadLocal 爲 null)進行判斷,如果爲 null 的話,那麼會對 value 設置爲 null。
即在使用完 ThreadLocal ,CurrentThread 依然運行的情況下,就算忘記調用 remove 方法,弱引用比強引用多一層保障:弱引用的 ThreadLocal 會被回收,對應的 value 在下一次 ThreadLocalMap 調用 set/get/remove中的任意方法時都會被清除,從而避免內存泄漏。
總結:ThreadLocal內存泄漏的根源是:由於ThreadLocalMap 的生命週期跟 Thread 一樣長,如果沒有手動刪除掉對應的 key 就會導致內存泄漏。