1. 你知道ThreadLocal是什麼嗎?
簡單地說,就是用來隔離數據的。用ThreadLocal來保存的數據,只對當前線程生效,當前線程對該數據做的任何操作,對別的線程是不生效的。舉個栗子一看便知:
public class TestThreadLocal {
private static User user = new User("jerry", "123"));
public void fun(User user){
System.out.println(Thread.currentThread().getName() + "開始執行,修改前的username[" + user.getUsername() + "]");
user.setUsername("tom");
System.out.println(Thread.currentThread().getName() + "執行完畢,修改後的username[" + user.getUsername() + "]");
}
public static void main(String[] args) {
TestThreadLocal testThreadLocal = new TestThreadLocal();
new Thread(() -> {
testThreadLocal.fun(user);
}, "線程A").start();
new Thread(() -> {
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
testThreadLocal.fun(user);
}, "線程B").start();
}
}
這段代碼很簡單,就是有個user,初始的username爲jerry,然後線程A將其改名爲tom,3秒鐘後,線程B再去讀取user的username。因爲線程A將起改爲tom了,所以線程B開始執行時讀取到的應該是tom。看執行結果:
分析得沒錯,線程B開始執行時讀取到的確實是tom,說明線程A的修改對線程B生效了。假如用ThreadLocal,結果就不是這樣了。
怎麼用呢?首先將上述代碼中的:
private static User user = new User("jerry", "123"));
改成:
private static ThreadLocal<User> user = ThreadLocal.withInitial(() -> new User("jerry", "123"));
然後將testThreadLocal.fun(user);
改成testThreadLocal.fun(user.get());
,這樣就可以了,再次執行,結果如下:
2. 說說ThreadLocal的常用方法
set(T t):將值綁定到當前線程中。注意是當前線程,比如你在main線程中set值,再另起兩個線程去get,是取不到的;
get():當前線程從ThreadLocal中取出自己的副本;
假如你定義了一個靜態變量:
private static ThreadLocal<User> threadLocal = new ThreadLocal<>();
那麼你必須在線程A和線程B中分別進行set,而不能在主線程中進行set,如下:
new Thread(() -> {
threadLocal.set(new User("jerry", "123"));
testThreadLocal.fun(threadLocal.get());
}, "線程A").start();
new Thread(() -> {
threadLocal.set(new User("jerry", "123"));
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
testThreadLocal.fun(threadLocal.get());
}, "線程B").start();
jdk8提供了新的構造方式,即開篇中用到的那種方式:
private static ThreadLocal<User> user = ThreadLocal.withInitial(() -> new User("jerry", "123"));
這樣就不用在每個線程中進行set了,在這裏初始化一次,每個線程get時都會取出屬於自己的副本。
3. 對ThreadLocal的應用場景有了解嗎?
很多框架中用到了ThreadLocal,比如Spring中,用ThreadLocal來保存數據庫連接,這樣可以保證單個線程的操作使用的是同一個數據庫連接;
可以用ThreadLocal來做session、cookie的隔離;
最經典的一個,SimpleDataFormat調用parse格式化時間的時候,parse方法會先調用Calendar.clear(),再調用Calendar.add(),如果一個線程調用了調用完add,在準備繼續parse的時候,另一個線程clear掉了,這就出問題了,所以可以用ThreadLocal;
還有一個就是可以用來傳參數,比如你多個方法都要對user對象進行操作,並且有些方法是第三方jar的,不能把user當成參數傳過去,那麼就可以將user裝到ThreadLocal中,要用的時候get出來即可。
4. ThreadLocal的底層原理你造不?
來看看ThreadLocal的set和get方法就秒懂了:
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();
}
看到這裏是不是就一目瞭然呢,其實ThreadLocal裏面有個ThreadLocalMap,把當前線程作爲key,就可以拿到這個線程專屬的ThreadLocalMap,所以說每個線程都有一個ThreadLocalMap,拿到了這個ThreadLocalMap後(如果拿到的不爲空,就用這個,爲空就創建一個,保證每個線程只有一個),將當前ThreadLocal綁定的值設置到map中;get的時候就將map的entry.value返回,就是我們存入的對象啦。
5. 那你對ThreadLocalMap瞭解過嗎?
上面說了,每個線程只對應一個ThreadLocalMap,但是一個線程可以有很多個ThreadLocal來保存不同的對象,那麼ThreadLocalMap怎麼來保存這多個ThreadLocal呢?那就是用你數組!源碼中有這麼一個數組:
private Entry[] table;
這個就是用來保存ThreadLocal的。怎麼判斷當前新加入的ThreadLocal放在數組的哪個位置呢?索引怎麼計算出來?ThreadLocalMap會利用usafe類計算出一個threadLocalHashCode,然後再根據:
int i = key.threadLocalHashCode & (len-1)
計算出來的i就是存放當前ThreadLocal的索引。
6. 你用ThreadLocal遇到過什麼問題嗎?
首先來看看ThreadLocalMap中的這段代碼:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
ThreadLocalMap中的key,也就是ThreadLocal對象,被設計成弱引用了,所以在外部沒有強引用key的時候,key會被垃圾回收清理掉,ThreadLocalMap中就會出現key爲null的Entry,永遠無法被GC回收,從而造成內存泄漏。爲了避免這個問題,用完ThreadLocal最好調用一下remove方法,手動清除掉。