在JDK中,解決線程沖突問題,有兩種解決方案:l 給臨界區加鎖;l 本地化臨界區。
第一種解決方案的典型代表是Synchonized。第二種的典型代表是ThreadLocal。而CopyOnWrite是這兩種方案的融合。
ThreadLocal為每個線程的并發通路資料建立一個副本,通過對副本的操作來隔離臨界區的污染。雖然增加了記憶體空間的消耗,但大大減少了線程同步所帶來性能消耗,也減少了線程并發控制的複雜度。
當然ThreadLocal不能完全解決并發的問題,因為它隻是隔離了臨界區,抛開了臨界區的同步問題,導緻多個副本的存在。是以使用的時候要因地制宜,符合自身的邏輯。
Demo
ThreadLocal的使用很簡單,如下所示,先聲明一個ThreadLocal的私有變量。然後線上程啟動的時候指派set(),在使用的時候再get()出來。
private ThreadLocal local = new ThreadLocal();
public void run() {
local.set(objLocal);
// ......
local.get();
也可以通過覆寫初始值的方法initialValue()來實作指派。
private ThreadLocal local = new ThreadLocal() {
@Override
protected ObjectinitialValue() {
// ......
return XXX;
}
};
Code
那ThreadLocal是如何能實作本地化臨界區的功能呢?我們到JDK一看究竟。
線上程類Thread中,通過一個Map屬性threadLocals來存放臨界區的資料。我暫且稱之為本地資料區。 這個Map的key為ThreadLocal執行個體,value為臨界區的資料值。
這個ThreadLocalMap沒有實作Map接口,但是也用到了散清單來組織資料。
ThreadLocal.ThreadLocalMap threadLocals = null;
接着,我們看到ThreadLocal取值get()時候,需要傳入目前線程t來擷取本地資料區t.threadLocals。擷取到資料區後,使用ThreadLocal執行個體作為key來取值。
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
return setInitialValue();
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
同樣,指派set()也是差不多的邏輯。如果t.threadLocals不存在就建立一個ThreadLocalMap對象。
這裡有個需要注意的點,擷取本地資料區是要傳入目前線程的,因為threadLocals是挂在具體的Thread對象上。是以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);
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
最後,我們發現ThreadLocal指派到本地資料區的時候,會建立一個Entry來存放:key為ThreadLocal執行個體,value為副本值。這裡其實隻是把value的值賦過來,沒有做clone的處理。
是以需要加倍注意的是,此處隻是值拷貝,沒做clone(深度拷貝)。如果傳進來的是個引用,就不能做到隔離了。
static class Entry extendsWeakReference<ThreadLocal> {
Object value;
Entry(ThreadLocal k, Object v) {
super(k);
value = v;
}
}
P.S 隔離失效
從上面ThreadLocal源碼可以看出:
1) ThreadLocal賦初始值的時候,需要線上程運作中,即run()中,否則ThreadLocalMap會挂錯線程;
2) 使用ThreadLocal隔離的值不能是引用,否則隔離的隻是引用,而引用所指向的對象則隔離失敗;
3) 本地資料區ThreadLocalMap是挂在Thread對象上的,是以要注意線程複用(線程池)所帶來的污染。
set引用
下面程式先建立一個SHARE_LIST作為臨界區,然後建立兩個線程同時去做修改。
給每個線程引入ThreadLocal屬性試圖本地化SHARE_LIST來隔離多線程的沖突。
根據ThreadLocal的設計初衷,應該是在各個Thread建立自己的本地資料區,互不影響。後來卻發現SHARE_LIST被污染了,所有線程的修改都寫入了同一個SHARE_LIST。最後,主線程和另外兩個線程輸出的結果都是一樣的。
這裡ThreadLocal隻是本地化(隔離)了引用值,而沒有本地化引用的對象本身,是以出現了這種現象。
public static List<String>SHARE_LIST = new CopyOnWriteArrayList<String>();
public static void main(String[]args) throws Exception {
SHARE_LIST.add("Int");
System.out.println("Change before : " + SHARE_LIST);
new MyThread2("Tom").start();
new MyThread2("Jack").start();
System.out.println("Change after :");
Thread.currentThread().sleep(500);
System.out.println(Thread.currentThread() + ":" + SHARE_LIST);
}
static class MyThread2 extends Thread {
privateThreadLocal<List<String>> local = new ThreadLocal<List<String>>();
private String name;
public MyThread2(Stringname) {
this.name = name;
}
@Override
public void run() {
local.set(SHARE_LIST);
local.get().add(name); // Change the parameter locally.
try {
Thread.currentThread().sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + ":" + local.get());
}
}
挂錯線程
把上面的代碼稍作修改,把ThreadLocal的賦初始值放在建立線程執行個體的構造函數中。執行程式後,發現擷取本地資料區local.get()的時候抛出了NullPointerException。
這是因為線程建立的時候還在主線程main中,這個時候指派set()就會把資料放到了main的本地資料區;而到了子線程run()的時候,擷取本地資料區get()取的是子線程的,是以就會抛空指針。
public static void main(String[]args) throws InterruptedException {
SHARE_LIST.add("Int");
System.out.println("Change before : " + SHARE_LIST);
new MyThread3("Tom", SHARE_LIST).start();
new MyThread3("Jack", SHARE_LIST).start();
System.out.println("Change after :");
Thread.currentThread().sleep(500);
System.out.println(Thread.currentThread() + ":" + SHARE_LIST);
}
static class MyThread3 extends Thread {
privateThreadLocal<List<String>> local = new ThreadLocal<List<String>>();
private String name;
public MyThread3(String name, List<String>list) {
this.name = name;
local.set(list);
}
@Override
public void run() {
local.get().add(name); // Changethe parameter locally.
try {
Thread.currentThread().sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + ":" + local.get());
}
}