高併發程式設計:執行緒安全和ThreadLocal
執行緒安全的概念:當多個執行緒訪問某一個類(物件或方法)時,這個類始終都能表現出正確的行為,那麼這個類(物件或方法)就是執行緒安全的。
執行緒安全
說的可能比較抽象,下面就以一個簡單的例子來看看什麼是執行緒安全問題。
public class MyThread implements Runnable { private int number = 5; @Override public void run() { number--; System.out.println("執行緒 : " + Thread.currentThread().getName() + "獲取到了公共資源,number = " + number); } public static void main(String[] args) { MyThread mt = new MyThread(); Thread t1 = new Thread(mt, "t1"); Thread t2 = new Thread(mt, "t2"); Thread t3 = new Thread(mt, "t3"); Thread t4 = new Thread(mt, "t4"); Thread t5 = new Thread(mt, "t5"); t1.start(); t2.start(); t3.start(); t4.start(); t5.start(); } }
Java中定一個執行緒有兩種方式:一是繼承Thread方法,二是實現Runnable介面,MyThread使用的是實現Runnable的方式來定義一個執行緒類。該類中有一個類變數number,初始值是5。在我new出的5個執行緒開啟start()方法的時候,執行緒執行到run方法就把number減一次。程式碼在控制檯的輸出結果如下:
執行緒 : t1獲取到了公共資源,number = 3 執行緒 : t3獲取到了公共資源,number = 2 執行緒 : t2獲取到了公共資源,number = 3 執行緒 : t4獲取到了公共資源,number = 1 執行緒 : t5獲取到了公共資源,number = 0
再次執行,得到以下結果:
執行緒 : t2獲取到了公共資源,number = 3
執行緒 : t1獲取到了公共資源,number = 3
執行緒 : t3獲取到了公共資源,number = 2
執行緒 : t4獲取到了公共資源,number = 1
執行緒 : t5獲取到了公共資源,number = 0
從上面兩個輸出結果可以看出,先執行到那個執行緒是不確定的,而number的值更為奇怪,並不是按照5到0依次遞減的。已第一次執行結果為例子,究竟是什麼原因導致了程式出現數據不一致問題的可能性?下面給出了一個可能的情景,如圖所示:
程式碼中建立了5個執行緒,t1執行緒啟動做number–操作時,這時候t3執行緒搶佔到CPU的執行權,t1中斷,t3啟動,這時候number的值等於4,t3執行緒在number等於4的基礎上做number–操作,當t3執行完number–操作時,t1又搶到了CPU的執行權,於是對number進行輸出,此時的number等於3,輸出結束之後t3搶到了CPU執行權,於是t3也對number進行列印輸出,於是t3執行緒輸出的結果也是等於3。
這是多執行緒程式中的一個普遍問題,稱為競爭狀態,如果一個類的物件在多執行緒程式中沒有導致競爭狀態,則稱這樣的類為執行緒安全的。上訴的MyThread類不是執行緒安全的。解決的辦法是給程式碼加鎖,加鎖的關鍵字為synchronized,synchronized可以在任意物件及方法上加鎖,而加鎖的這段程式碼稱為“互斥區”或“臨界區”
public class MyThread implements Runnable {
private int number = 5;
@Override
public synchronized void run() {
number--;
System.out.println("執行緒 : " + Thread.currentThread().getName() + "獲取到了公共資源,number = " + number);
}
public static void main(String[] args) {
MyThread mt = new MyThread();
Thread t1 = new Thread(mt, "t1");
Thread t2 = new Thread(mt, "t2");
Thread t3 = new Thread(mt, "t3");
Thread t4 = new Thread(mt, "t4");
Thread t5 = new Thread(mt, "t5");
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
}
}
本例為一個執行緒安全的執行緒類,無論執行多少次,都是同樣的輸出結果:
執行緒 : t1獲取到了公共資源,number = 4
執行緒 : t2獲取到了公共資源,number = 3
執行緒 : t4獲取到了公共資源,number = 2
執行緒 : t3獲取到了公共資源,number = 1
執行緒 : t5獲取到了公共資源,number = 0
當多個執行緒訪問myThread的run方法時,以排隊的方式進行處理,這裡的排隊是按照CPU分配的先後順序給定的,而不是按照程式碼的先後順序或者執行緒的啟動先後順序來執行的。一個執行緒想要執行synchronized修改的方法裡面的程式碼,首先是嘗試獲得鎖,如果拿到鎖,執行synchronized程式碼體內容;拿不到鎖,這個執行緒就會不斷的嘗試獲得這把鎖,直到拿到為止。而且多個執行緒會同時去競爭這把鎖,也就是會有鎖競爭問題。
ThreadLocal
ThreadLocal是執行緒區域性變數,是一種多執行緒間併發訪問量的解決方案。與其synchronized等加鎖的方式不同,ThreadLocal完全不提供鎖,而使用以空間換時間的手段,為每個執行緒提供變數的獨立副本,以保障執行緒安全。
public class UseThreadLocal {
public static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public void setThreadLocal(String value) {
threadLocal.set(value);
}
public String getThreadLocal(){
return threadLocal.get();
}
public static void main(String[] args) {
UseThreadLocal utl = new UseThreadLocal();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
utl.setThreadLocal("張三");
System.err.println("當前t1執行緒拿到的值 : " + utl.getThreadLocal());
}
}, "t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
utl.setThreadLocal("李四");
System.err.println("當前t2執行緒拿到的值 : " + utl.getThreadLocal());
}
}, "t2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
t1.start();
t2.start();
System.err.println("主執行緒拿到的值 : " + utl.getThreadLocal());
}
}
上述程式碼建立了3個執行緒,執行緒1向ThreadLocal裡面設定值"張三",執行緒2向ThreadLocal裡面設定值"李四"。程式的程式碼輸出如下:
當前t1執行緒拿到的值 : 張三
當前t2執行緒拿到的值 : 李四
主執行緒拿到的值 : null
從程式的輸出可以看出,每個執行緒只能打印出本執行緒設定的變數值。該程式存在一個共享變數threadLocal,當t1向threadLocal設定“張三”之後,取出的值自然是“張三”,接下來t2執行緒向threadLocal設定值“李四”之後,取出來的值自然是“李四”。有的同學可能會有疑問,說t2也許將t1之前設定的值覆蓋掉了,那麼請看主執行緒的輸出,其結果為null,主執行緒取出的結果為空。這說明了用了ThreadLocal裡面的值只存在與執行緒的區域性變數,對其他執行緒具有不可見性。
那麼ThreadLocal是如何實現其功能的?閱讀其原始碼發現它用到了ThreadLocalMap,該類和HashMap一樣是鍵值對的一種資料結構,值得注意的是雖然該類和HashMap功能類似,當時該類並沒有繼續自Map。
private Entry[] table;
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
ThreadLocal的set方法原始碼
public void set(T value) {
Thread t = Thread.currentThread(); //獲取當前執行緒
ThreadLocalMap map = getMap(t); /以當前執行緒作為key獲得map容器
if (map != null)//判斷map是否為空
map.set(this, value); //非空則把當前執行緒作為key,當前value作為值放進map裡面
else
createMap(t, value);//為空則建立map
}
下面我們再來看ThreadLocal應用場景的另一個例子,任務的同時提交。
public class MessageHolder {
private List<String> messages = new ArrayList<>();
private static final ThreadLocal<MessageHolder> holder = new ThreadLocal<MessageHolder>(){
@Override
protected MessageHolder initialValue() {
return new MessageHolder();
}
};
public static void add(String value) {
holder.get().messages.add(value);
}
/**
* 清空list,並返回刪掉的list裡面的值
* @return
*/
public static List<String> clear() {
List<String> list = holder.get().messages;
holder.remove();
return list;
}
public static void main(String[] args) {
MessageHolder.add("A");
MessageHolder.add("B");
List<String> cleared = MessageHolder.clear(); //已經被清除的list
System.out.println("被清空掉的元素:" + cleared);
}
}
MessageHolder類定義了add和clear方法,add方法是新增元素,clear是清空元素的方法,並返回被清楚的list集合。應用場景如下圖,funtion1可能return 1,2,function2可能返回3,4,function3返回5,6,而之前的做法可能是對這三個function()累加的程式碼段進行加鎖,這樣造成A執行緒在訪問的時候B執行緒只能處於等待,只有當這三個方法都執行完畢,向前端返回1,2,3,4,5,6的時候,A執行緒釋放索,B執行緒才能繼續使用,這樣系統解決併發性就很低。
從效能上說,ThreadLocal不具有絕對的優勢,在併發不是很高的時候,加鎖的效能會更好,但作為一套與鎖完全無關的執行緒安全解決方案,在高併發量或者競爭激烈的場景,使用ThreadLocal可以在一定程度上減少鎖競爭。