我們有時貌似熟悉異步、多線程、任務和並行,但有時又不一定特別清楚它們之前的本質區別,甚至在很多複雜的場景下亂用一氣。下面我就結合場景來說明在什麼情況下該採用什麼。同時,還講解下如何優雅地控制線程,處理任務和並行中的異常。
一、在什麼場景可以使用多線程
- 想要同時處理多件事:單線程處理不了的,必須使用多線程。(類似於分身術)
- 多個線程分解大任務:用單線程可以做,但是使用多線程可以更快。(類似於左右開弓)
例1:同時處理2件事
場景描述: 執行實際工作20秒鐘,20秒時間到結束執行。
public class CalculatePrimes extends Thread{
//使用兩個線程,一個用於計時(線程會休眠20秒然後設置一個主線程要檢查的標誌finished),一個用於執行實際工作。在執行實際工作的線程啓動前啓動計時線程。
//達到20秒鐘主線程將停止。
public static final int SECONDS = 20000;
public volatile boolean finished = false;
public void run(){
System.out.println("開始執行實際工作啦");
for (long i = 0l; i < Long.MAX_VALUE; i++) {
if(finished){
break;
}
if(i%100000000==0){
System.out.println("i:"+i);
}
}
System.out.println("結束執行實際工作啦");
}
public static void main(String[] args){
//通過實例化CalculatePrimes類型的對象來創建線程。
CalculatePrimes calculator = new CalculatePrimes();
calculator.start();
try{
System.out.println("執行Thread.sleep前,時間爲:"+System.currentTimeMillis());
Thread.sleep(SECONDS);
System.out.println("執行Thread.sleep後,時間爲:"+System.currentTimeMillis());
}catch(InterruptedException e){
}
calculator.finished = true;
System.out.println("執行完main()");
}
}
執行結果如下:
執行Thread.sleep前,時間爲:1325663362824
開始執行實際工作啦
i:0
i:100000000
i:200000000
i:300000000
i:400000000
i:500000000
i:600000000
i:700000000
i:800000000
i:900000000
i:1000000000
i:1100000000
i:1200000000
i:1300000000
執行Thread.sleep後,時間爲:1325663382824
結束執行實際工作啦
執行完main()
或者用TimerTask來實現:
public class CalculatePrimes extends Thread{
//使用兩個線程,一個用於計時(線程會休眠10秒然後設置一個主線程要檢查的標誌finished),一個用於執行實際工作。在執行實際工作的線程啓動之前,啓動計時線程。達到10秒鐘主線程將停止。
public static final int SECONDS = 20000;
public volatile boolean finished = false;
public void run(){
System.out.println("開始執行實際工作啦");
for (long i = 0l; i < Long.MAX_VALUE; i++) {
if(finished){
break;
}
if(i%100000000==0){
System.out.println("i:"+i);
}
}
System.out.println("結束執行實際工作啦");
}
public static void main(String[] args){
final CalculatePrimes calculator = new CalculatePrimes();
calculator.start();
Timer timer = new Timer();
timer.schedule(
new TimerTask(){
public void run(){
calculator.finished = true;
}
}, SECONDS);
}
}
例2:計算密集型的一件事
應用場景:從一個非常大的數組中查找值最大的元素。
public class TenThreads {
private static class WorkerThread extends Thread{
int max = Integer.MIN_VALUE;
int[] ourArray;
public int getMax(){
return max;
}
public WorkerThread(int[] ourArray){
this.ourArray = ourArray;
for (int i = 0; i < ourArray.length; i++) {
System.out.print(ourArray[i]+" ");
}
System.out.println();
}
public void run(){
for (int i = 0; i < ourArray.length; i++) {
max = Math.max(max,ourArray[i]);
}
}
}
public static void main(String[] args){
WorkerThread[] threads = new WorkerThread[9];
int[][] bigMatrix = {{1,10,100},{2,20,200},{3,33,333},{4,40,444},{5,50,500},{6,66,666},{7,77,777},{8,88,888},{9,99,999}};
int max = Integer.MIN_VALUE;
for (int i = 0; i < 9; i++) {
threads[i] = new WorkerThread(bigMatrix[i]);
threads[i].start();
}
try{
for (int i = 0; i < 9; i++) {
threads[i].join();
max = Math.max(max,threads[i].getMax());
}
}catch(InterruptedException e){
}
System.out.println("Maximum value is:"+max);
}
}
執行結果:
1 10 100
2 20 200
3 33 333
4 40 444
5 50 500
6 66 666
7 77 777
8 88 888
9 99 999
Maximum value is:999
當然這個示例程序的數據量不大,只起到示例說明的作用,只要大家明白意圖就好,大任務在實際工作中還是會遇到比較多的。
二、區分異步和多線程應用場景
舉個場景--要獲取某個網頁的內容並顯示出來。來看下應該選擇異步還是多線程更適合。
可以預見,如果該網頁的內容很多,或者當前的網絡狀況不太好,獲取網頁的過程會持續較長時間。
於是,我們可能會想到用 新起工作線程的方法來完成這項工作,這樣在等待網頁內容返回的過程中界面就不會被阻滯了。是的,上面的程序解決了界面阻滯的問題,但是,它高效嗎?答案是:不。因爲這樣是爲了獲取網頁,新起了一個工作線程,然後在讀取網頁的整個過程中,該工作線程始終被阻滯,直到獲取網頁完畢爲止。在整個過程中,工作線程被佔用着,這意味着系統的資源始終被消耗着、等待着。
如果使用異步模式去實現,它使用線程池進行管理。新起異步操作後,會將工作丟給線程池中的某個工作線程來完成。當開始I/O操作的時候,異步會將工作線程還給線程池,這意味着獲取網頁的工作不會再佔用任何CPU資源了。直到異步完成,即獲取網頁完畢,異步纔會通過回調的方式通知線程池。可見,異步模式藉助於線程池,極大地節約了CPU的資源。
注:直接內存訪問,是一種不經過CPU而直接進行內存數據存儲的數據交換模式。通過直接內存訪問的數據交換幾乎可以不損耗CPU的資源。在硬件中,硬盤、網卡、聲卡、顯卡等都有D直接內存訪問功能。異步編程模型就是讓我們充分利用硬件的直接內存訪問功能來釋放CPU的壓力。
明白了異步和多線程的區別後,我們來總結下兩者的應用場景:
- 計算密集型工作,採用多線程。
- IO密集型工作,採用異步機制。
三、線程在現有其他技術中的應用
在使用下面這些技術是,我們必須始終假設可以在多個線程中併發地執行的情境,這就要求我們必須適當同步共享數據。
- Servlet和JSP技術
Servlet容器創建多個線程,在這些線程中執行servlet請求。在你寫servlet程序時,你不需要知道你的servlet請求是在什麼線程中執行的。但是你要知道,如果同時有多個對相同URL的請求入棧,那麼同一個servlet可能會同時在多個線程中是活動的。這要求我們再編寫servlet或jsp時,必須始終假設可以在多個線程中併發地執行同一個servlet或jsp的情境,這就要求我們必須適當同步servlet或jsp文件訪問的任何共享數據,包括servlet對象本身的字段。
2. 現實RMI對象
RMI讓你可以調用在其他JVM中運行的對象並對其進行操作。當調用遠程方法時,RMI編譯器創建的RMI存根會打包方法參數,並通過網絡警它們發送到遠程系統,然後遠程系統會將它們解包並調用遠程方法。
假設你創建了一個RMI對象,並將它註冊到RMI註冊表或者JNDI名稱空間(Java Naming and Directory Interface即Java命名和目錄接口)。當遠程客戶機調用其中的一個方法是,該方法會在什麼線程中執行呢?假設RMI對象的常用方法是繼承UnicastRemoteObject,在構造UnicastRemoteObject時,會初始化用於分派遠程方法調用的基礎結構,這包括用於接收遠程調用請求的套接字偵聽器和一個或多個執行遠程請求的線程。所以,當接收到執行RMI方法的請求時,這些方法將在RMI管理的線程中執行。
3.Collection集合類
Collection集合類在自身實現時並沒有使用同步,這就意味着程序員在使用集合類時,在多線程的情境下如果沒有進行同步,是有可能出問題的,即不能在不進行同步的情況下在多線程場景下使用集合類。通過每次訪問共享集合中的方法時使用同步,可以在多線程應用程序中使用Collection類。對於任何給定的集合,每次必須用同一個鎖進行同步。通常可以選擇集合對象本身作爲鎖。另一種方法是使用Collections類提供的一組List、Map、Set的封裝器。如可以用Collections.synchronizedMap封裝Map,它將確保所有對該映射的訪問都被正確同步了。