線程安全的概念:當多個線程訪問某一個類(對象或方法)時,這個類始終都能表現出正確的行爲,那麼這個類(對象或方法)就是線程安全的。
線程安全
說的可能比較抽象,下面就以一個簡單的例子來看看什麼是線程安全問題。
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可以在一定程度上減少鎖競爭。