本文作者:王一飛,叩丁狼高級講師。原創文章,轉載請註明出處。
前面幾篇講完了併發環境下各種集合的使用, 今天來聊一聊多線程設計模式。進入正題前顯示說下,多線程的設計準則:
多線程的設計準則
1:安全性(safety)
程序正常運行的必要條件之一,對象進行邏輯操作時,對象結果狀態要和設計者原意一致。舉個簡單例子: 多線程對ArrayList對象操作時,會得到意想不到的的結果,因爲ArrayList是線程不安全的集合類,單純的多線程操作時,不具有安全性。
2:生存性/活性(liveness)
程序正常運行的必要條件之一,無論是什麼時候,必要執行的處理一定能夠執行,如果死活執行不到,這個代碼有問題。比如說,程序運行過程中,突然停止了,又不死機,後續代碼不執行了。常見的死循環,死鎖等。這種情況表示代碼失去活性,多線程代碼要必須強調生存性/活性。
3:可複用性(reusablility)
屬於代碼優化範疇,類設計成爲組件,可以重複使用。編寫多線程程序時,如果能巧妙的將線程的互斥機制/資源競爭機制隱藏到類中,這就是一個可複用性高的程序。
4:性能(performance)
也屬於代碼優化範疇,如果代碼能設計成可以快速,大批量的執行,這個代碼就是高性能代碼。這時候就得考慮,吞吐量: 單位時間處理數量, 響應時間:開始到結束的花費時間,容量:同時進行處理的數量。
總結: 安全性, 生存性/活性是多線程設計模式的必須條件, 在滿足這兩個前提下,再考慮如何提高線程的複用性跟性能。
Single Threaded Execution模式
居於上面準則, 來看下本篇的主角, Single Threaded Execution模式. 這個模式是多線程設計模式中最簡單,最基礎的一個. 按字面意思解釋 : 同一時刻某個資源只允許一個線程操作.
假設一個需求: 多個線程共同操作一個資源Resoure,先執行setData, 再執行showData方法
//競爭資源
public class Resource {
private String data;
public void setData(String data){
this.data = data;
try {
Thread.sleep(1000); //放大問題
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void showData(){
System.out.println(Thread.currentThread().getName() + "--:: data:" + data);
}
}
public class App {
public static void main(String[] args) {
//共用同一個資源對象
final Resource resource = new Resource();
new Thread(new Runnable() {
public void run() {
resource.setData("A");
//期望:t1--:: data:A
//結果:t1--:: data:B
resource.showData();
}
}, "t1").start();
new Thread(new Runnable() {
public void run() {
resource.setData("B");
//期望:t2--:: data:B
//結果:t2--:: data:B
resource.showData();
}
}, "t2").start();
}
}
t1--:: data:B
t2--:: data:B
看執行結果, t1線程打印出來並不是它setData進去的A, 原因: 當線程t1 通過setData方法改data=A 然後休眠1s. 緊接着線程t2進入setData方法修改data=B,並休眠. 這是t1線程休眠結束, 馬上執行showData, 此時的data已經被t2改爲data=B, 所以出現上面t1–:: data:B 的結果.
上述案例對resource資源不進行安全防護,就讓多個線程進行操作, 最終得到非期望的結果.這就是我們常說的線程不安全的操作. 要解決這問題最簡單做法就是將resource資源保護起來即可, 保證同一時刻只允許一個線程操作.
方案: synchronized 將 setData 跟 showData 方法包裹起來
public class App {
public static void main(String[] args) {
final Resource resource = new Resource();
new Thread(new Runnable() {
public void run() {
synchronized (resource) {
resource.setData("A");
resource.showData();
}
}
}, "t1").start();
new Thread(new Runnable() {
public void run() {
synchronized (resource) {
resource.setData("B");
resource.showData();
}
}
}, "t2").start();
}
}
線程t1操作前, 先獲取resource資源對象鎖,持有鎖之後,可以執行setData/showData方法, 線程t2則必須等待線程t1釋放資源對象鎖纔可以參與執行.
這種類型的多線程操作模式,我們稱之爲:Single Threaded Execution模式. 其核心點就是保證同一時刻某個資源只允許一個線程操作.
何時使用(使用場景)
1>多線程編程環境
2>多個線程同時訪問同一個共享資源;
3>共享資源的狀態會因線程的訪問而改變;
使用注意
Single Thread Execution 模式使用不當時,可能引起死鎖問題:
比如, 如果resource資源對象不是1個對象,而是多個對象呢?
假設場景: t1 先持有resource1, 再持有resource2 2個對象鎖之後,纔可以執行showData方法, t2更好相反.
public class App {
public static void main(String[] args) {
final Resource resource1 = new Resource();
final Resource resource2 = new Resource();
new Thread(new Runnable() {
public void run() {
synchronized (resource1) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resource2) {
resource2.setData("A");
resource2.showData();
}
}
}
}, "t1").start();
new Thread(new Runnable() {
public void run() {
synchronized (resource2) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resource1) {
resource1.setData("B");
resource1.showData();
}
}
}
}, "t2").start();
}
}
所以, 在使用Single Threaded Execution模式一定要看清楚使用場合.