Java多線程編程--(8)學習Java5.0 併發編程包--線程池、Callable & Future 簡介

線程池是程序設計領域池化技術的一種應用(數據庫連接池也是一個典型的池化技術),池化技術解決了大量的短請求帶來的系統頻繁創建對象對性能的影響。我們可以實現自己的線程池,但往往因爲考慮不周全如容錯性、自動擴容與縮容等導致性能不佳!Java5.0 內置了對線程池的支持,提供了性能比較優越的線程池相關的類!我們就來簡單介紹一下如何使用這個線程池!

【ExecutorService & Executors

在Java5.0中,接口Executor代表一個任務執行器,我們往這個執行器中提交任務,這個執行器負責執行,方式可以爲“當前提交線程執行”,“另起線程執行”或“線程池執行”,由具體不同實現決定。這樣設計,Java將線程控制相關的操作封裝在這裏面。ExecutorService是這個接口的子接口,代表“線程池執行”這種方式!同時也提供了一些實現類,但我們不用自己去創建實現類對象,Executors提供了大量工廠方法去爲我們做這個,我們先看一個例子:

package cn.test;  
  
import java.util.concurrent.ExecutorService;  
import java.util.concurrent.Executors;  
  
public class ThreadPoolTest1 {  
  
    public static void main(String[] args) {  
          
        // 創建線程數量爲固定5個的線程池   
        ExecutorService threadPool = Executors.newFixedThreadPool(5);  
        // 向線程池提交任務即可   
        for(int i=0; i<5; i++){  
              
            threadPool.execute(new MyInnerTask());  
        }  
    }  
      
    static class MyInnerTask implements Runnable {  
  
        @Override  
        public void run() {  
              
            int i = 0;  
            while(i<10){  
                  
                System.out.println(Thread.currentThread().getName() + " 執行第 " + i + " 任務循環!" );  
                i++;  
            }  
        }  
    }  
  
}  

部分輸出爲:

[java] view plaincopyprint?pool-1-thread-1 執行第 3 任務循環!  
pool-1-thread-3 執行第 7 任務循環!  
pool-1-thread-5 執行第 6 任務循環!  
pool-1-thread-5 執行第 7 任務循環!  
pool-1-thread-5 執行第 8 任務循環!  
pool-1-thread-4 執行第 8 任務循環!  
pool-1-thread-4 執行第 9 任務循環!  
pool-1-thread-2 執行第 4 任務循環!  
pool-1-thread-5 執行第 9 任務循環!  
pool-1-thread-2 執行第 5 任務循環!  
pool-1-thread-3 執行第 8 任務循環!  
pool-1-thread-1 執行第 4 任務循環!  
pool-1-thread-3 執行第 9 任務循環!  
pool-1-thread-2 執行第 6 任務循環!  
pool-1-thread-1 執行第 5 任務循環!  

pool-1-thread-1 執行第 3 任務循環!
pool-1-thread-3 執行第 7 任務循環!
pool-1-thread-5 執行第 6 任務循環!
pool-1-thread-5 執行第 7 任務循環!
pool-1-thread-5 執行第 8 任務循環!
pool-1-thread-4 執行第 8 任務循環!
pool-1-thread-4 執行第 9 任務循環!
pool-1-thread-2 執行第 4 任務循環!
pool-1-thread-5 執行第 9 任務循環!
pool-1-thread-2 執行第 5 任務循環!
pool-1-thread-3 執行第 8 任務循環!
pool-1-thread-1 執行第 4 任務循環!
pool-1-thread-3 執行第 9 任務循環!
pool-1-thread-2 執行第 6 任務循環!
pool-1-thread-1 執行第 5 任務循環!


上面是創建了一個固定數量爲5的線程池,然後我們就往其中提交任務即可,提交後,線程池就會執行。我們看到這個多線程例子中卻沒有任何線程相關的代碼,所有這些代碼都被封裝到了ExecutorService中!固定數量的線程池就是線程池中總是維持固定數量的線程,沒有任務了,線程數量不變,任務多了,線程數量也不變。這個適用的場景是:任務數量比較固定,且任務執行時間較長。

我們再看一種創建線程池的工廠方法:

  1. ExecutorService threadPool = Executors.newCachedThreadPool();  
ExecutorService threadPool = Executors.newCachedThreadPool();

這會創建一個有彈性的線程池,當任務較多並且此時所有線程都在執行任務,則會創建額外線程去執行!沒有任務時,當線程的空閒時長達到60秒,則將線程殺死!這種線程池適合的場景是:任務數量不定,並且任務執行時間較短!

我們再看一個十分特別的線程池:

  1. ExecutorService threadPool = Executors.newSingleThreadExecutor();  
ExecutorService threadPool = Executors.newSingleThreadExecutor();

這個線程池中只會有一個線程,那這個和單線程有啥區別呢?首先線程創建不由我們去處理,其次線程執行因異常退出後,這個線程池會再起一個線程,即自動維持一個線程的存在!這種線程池適合的場景就是:固定需要一個線程去執行某種任務!

ExecutorService接口還有一個子接口ScheduledExecutorService,這是一種特殊的線程池,用於延遲執行任務或定期定時執行任務(可以認爲是定時器池),我們同樣是通過Executors的工廠方法去創建這種線程池,我們直接看用法了:

  1. package cn.test;  
  2.   
  3. import java.util.concurrent.Executors;  
  4. import java.util.concurrent.ScheduledExecutorService;  
  5. import java.util.concurrent.TimeUnit;  
  6.   
  7. public class ThreadPoolTest2 {  
  8.   
  9.     public static void main(String[] args) {  
  10.           
  11.         // 創建線程數量爲固定5個的線程池   
  12.         ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(2);  
  13.         // 向線程池提交任務即可   
  14.         for(int i = 0; i<2; i++){  
  15.               
  16.             scheduledThreadPool.schedule(new MyInnerTask(), 5, TimeUnit.SECONDS);  
  17.         }  
  18.         // 主線程給出計時   
  19.         for(int i=1; i<=5; i++){  
  20.               
  21.             System.out.println("等待 " + i + " 秒!");  
  22.             try {  
  23.                 Thread.sleep(1000);  
  24.             } catch (InterruptedException e) {  
  25.                 e.printStackTrace();  
  26.             }  
  27.         }  
  28.     }  
  29.       
  30.     static class MyInnerTask implements Runnable {  
  31.   
  32.         @Override  
  33.         public void run() {  
  34.               
  35.             System.out.println(Thread.currentThread().getName() + " come !!!");  
  36.             try {  
  37.                 Thread.sleep(3000);  
  38.             } catch (InterruptedException e) {  
  39.                 e.printStackTrace();  
  40.             }  
  41.         }  
  42.     }  
  43.   
  44. }  
package cn.test;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class ThreadPoolTest2 {

	public static void main(String[] args) {
		
		// 創建線程數量爲固定5個的線程池
		ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(2);
		// 向線程池提交任務即可
		for(int i = 0; i<2; i++){
			
			scheduledThreadPool.schedule(new MyInnerTask(), 5, TimeUnit.SECONDS);
		}
		// 主線程給出計時
		for(int i=1; i<=5; i++){
			
			System.out.println("等待 " + i + " 秒!");
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
	
	static class MyInnerTask implements Runnable {

		@Override
		public void run() {
			
			System.out.println(Thread.currentThread().getName() + " come !!!");
			try {
				Thread.sleep(3000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}

}

看結果:

  1. 等待 1 秒!  
  2. 等待 2 秒!  
  3. 等待 3 秒!  
  4. 等待 4 秒!  
  5. 等待 5 秒!  
  6. pool-1-thread-1 come !!!  
  7. pool-1-thread-2 come !!!  
等待 1 秒!
等待 2 秒!
等待 3 秒!
等待 4 秒!
等待 5 秒!
pool-1-thread-1 come !!!
pool-1-thread-2 come !!!

上例,我們創建一個定時執行任務的線程池,含有兩個固定的線程,主線程主要負責計時!調用線程池的schedule方法向其中部署任務。第二個參數5表示提交的任務會延時5秒後被執行!

ScheduledExecutorService類型的線程池在提交任務時,還有其他重載方法可以使用:

  1. scheduledThreadPool.scheduleAtFixedRate(new MyInnerTask(), 52, TimeUnit.SECONDS);  
scheduledThreadPool.scheduleAtFixedRate(new MyInnerTask(), 5, 2, TimeUnit.SECONDS);

上述提交任務的方法,第一個5表示該任務延遲5秒被執行,第二個2表示,這個任務每隔2秒會被重新執行一次(具體的操作應該是該任務上的已被執行標示會在2秒後被清除)。這裏有個原則是:如果任務本身的執行時長>2秒,則該任務會在被執行完畢後,纔可以再次被執行(立即)!同一個任務不會出現多線程併發執行的情況!

另一個重載形式爲:

  1. scheduledThreadPool.scheduleWithFixedDelay(new MyInnerTask(), 52, TimeUnit.SECONDS);  
scheduledThreadPool.scheduleWithFixedDelay(new MyInnerTask(), 5, 2, TimeUnit.SECONDS);

上述提交任務的方法,數字參數5和2的意思和上面的方法一致,但和上面方法有所區別的是:無論該任務執行多長時間,任務執行後,會再延遲2秒纔可以被執行!即如果任務執行本身需要3秒鐘,則該任務第一次執行和第二次可以被執行的間隔爲5秒(被執行的3+延遲的2)。

【Callable & Futrue】

以前我們提到的所有線程相關的操作,線程執行完任務後都不會有返回值也不會拋出異常(Thread的run方法本身就沒有返回值和異常聲名)。有時,爲了處理這種問題,我們不得不寫很多額外代碼!在併發包中,Java通過提供接口Callable來對這種情況進行了語言級的支持!Callable接口類似於Runnable接口,代表一段可以被線程去執行的代碼!任務實現這個接口後,就可以讓線程在執行過程中拋出異常或在執行完畢後返回結果。接口Future代表任務執行後的返回結果。我們先看個使用例子:

  1. package cn.test;  
  2.   
  3. import java.util.concurrent.Callable;  
  4. import java.util.concurrent.ExecutionException;  
  5. import java.util.concurrent.ExecutorService;  
  6. import java.util.concurrent.Executors;  
  7. import java.util.concurrent.Future;  
  8.   
  9. public class CallableTest1 {  
  10.   
  11.     public static void main(String[] args) throws InterruptedException, ExecutionException {  
  12.   
  13.         /** 
  14.          * 創建只有一個線程的線程池,目前只能通過線程池來提交Callable類型的任務 
  15.          */  
  16.         ExecutorService threadPool = Executors.newSingleThreadExecutor();  
  17.         // 向線程池提交任務,並且得到Future類型返回值   
  18.         Future<String> future = threadPool.submit(new MyTask());  
  19.         // 調用future的get方法,主線程進入阻塞狀態,等待線程結果返回,如果線程執行拋異常,這裏也會將異常拋出!   
  20.         System.out.println(future.get());  
  21.     }  
  22.       
  23.     static class MyTask implements Callable<String>{  
  24.   
  25.         @Override  
  26.         public String call() throws Exception {  
  27.               
  28.             // 睡眠3秒,代表執行某段複雜業務邏輯   
  29.             Thread.sleep(3000);  
  30.             String result = "下訂單成功!";  
  31.             return result;  
  32.         }  
  33.           
  34.           
  35.     }  
  36.   
  37. }  
package cn.test;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class CallableTest1 {

	public static void main(String[] args) throws InterruptedException, ExecutionException {

		/**
		 * 創建只有一個線程的線程池,目前只能通過線程池來提交Callable類型的任務
		 */
		ExecutorService threadPool = Executors.newSingleThreadExecutor();
		// 向線程池提交任務,並且得到Future類型返回值
		Future<String> future = threadPool.submit(new MyTask());
		// 調用future的get方法,主線程進入阻塞狀態,等待線程結果返回,如果線程執行拋異常,這裏也會將異常拋出!
		System.out.println(future.get());
	}
	
	static class MyTask implements Callable<String>{

		@Override
		public String call() throws Exception {
			
			// 睡眠3秒,代表執行某段複雜業務邏輯
			Thread.sleep(3000);
			String result = "下訂單成功!";
			return result;
		}
		
		
	}

}

目前只能向線程池中提交Callable類型的任務,提交該任務後,會立即返回一個Futrue對象,調用Future的get方法,即可得到線程的返回值或異常信息!注意get()方法是一個無限等待的阻塞調用的方法,直到線程返回正常結果或執行拋異常返回!Future還提供一個重載的get:

  1. future.get(1, TimeUnit.SECONDS);  
future.get(1, TimeUnit.SECONDS);

調用這個get,表明線程會在這裏阻塞等待1秒鐘,如果線程沒有返回或拋出異常,則get方法會拋出java.util.concurrent.TimeoutException 的超時異常!

上例中,我們只是向線程池中提交了一個任務,並且向線程所要返回值。如果我們要提交多個需要返回值的任務,我們該如何做?一個做法就是for循環提交任務,將所有的future先保存在一個列表中,提交完畢後,主線程循環列表,逐個調用Future的get方法!但這種序列化處理方法無法優先處理排在後面的那些很快就返回結果的任務!併發包中對這種情況也給予了支持!

CompletionService接口用於提交一組Callable任務,其有一個默認實現類爲:ExecutorCompletionService,我們看一下具體用法:

  1. package cn.test;  
  2.   
  3. import java.util.concurrent.Callable;  
  4. import java.util.concurrent.CompletionService;  
  5. import java.util.concurrent.ExecutionException;  
  6. import java.util.concurrent.ExecutorCompletionService;  
  7. import java.util.concurrent.ExecutorService;  
  8. import java.util.concurrent.Executors;  
  9. import java.util.concurrent.Future;  
  10.   
  11. public class CompletionServiceTest1 {  
  12.   
  13.     public static void main(String[] args) {  
  14.           
  15.         // 創建具有固定兩個線程的線程池!   
  16.         ExecutorService threadPool = Executors.newFixedThreadPool(2);  
  17.         // 創建CompletionService,需要傳遞一個線程池,具體任務的執行還是由這個線程池去執行   
  18.         CompletionService<String> completionService = new ExecutorCompletionService<String>(threadPool);  
  19.         completionService.submit(new MyLongTask());  
  20.         completionService.submit(new MyShortTask());  
  21.         try {  
  22.             Future<String> future = null;  
  23.             while((future = completionService.take()) != null){  
  24.                   
  25.                 System.out.println(future.get());  
  26.             }  
  27.         } catch (InterruptedException e) {  
  28.             e.printStackTrace();  
  29.         } catch (ExecutionException e) {  
  30.             e.printStackTrace();  
  31.         }  
  32.     }  
  33.       
  34.     // 需要長時間執行的任務   
  35.     static class MyLongTask implements Callable<String>{  
  36.   
  37.         @Override  
  38.         public String call() throws Exception {  
  39.               
  40.             // 睡眠6秒,代表執行某段複雜業務邏輯   
  41.             Thread.sleep(6000);  
  42.             String result = "大大大大大訂單下單成功!";  
  43.             return result;  
  44.         }  
  45.           
  46.           
  47.     }  
  48.       
  49.     // 短時間就可執行完畢的任務   
  50.     static class MyShortTask implements Callable<String>{  
  51.   
  52.         @Override  
  53.         public String call() throws Exception {  
  54.               
  55.             // 睡眠3秒,代表執行某段複雜業務邏輯   
  56.             Thread.sleep(3000);  
  57.             String result = "小小小訂單下單成功!";  
  58.             return result;  
  59.         }  
  60.           
  61.           
  62.     }  
  63.   
  64. }  
package cn.test;

import java.util.concurrent.Callable;
import java.util.concurrent.CompletionService;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class CompletionServiceTest1 {

	public static void main(String[] args) {
		
		// 創建具有固定兩個線程的線程池!
		ExecutorService threadPool = Executors.newFixedThreadPool(2);
		// 創建CompletionService,需要傳遞一個線程池,具體任務的執行還是由這個線程池去執行
		CompletionService<String> completionService = new ExecutorCompletionService<String>(threadPool);
		completionService.submit(new MyLongTask());
		completionService.submit(new MyShortTask());
		try {
			Future<String> future = null;
			while((future = completionService.take()) != null){
				
				System.out.println(future.get());
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		} catch (ExecutionException e) {
			e.printStackTrace();
		}
	}
	
	// 需要長時間執行的任務
	static class MyLongTask implements Callable<String>{

		@Override
		public String call() throws Exception {
			
			// 睡眠6秒,代表執行某段複雜業務邏輯
			Thread.sleep(6000);
			String result = "大大大大大訂單下單成功!";
			return result;
		}
		
		
	}
	
	// 短時間就可執行完畢的任務
	static class MyShortTask implements Callable<String>{

		@Override
		public String call() throws Exception {
			
			// 睡眠3秒,代表執行某段複雜業務邏輯
			Thread.sleep(3000);
			String result = "小小小訂單下單成功!";
			return result;
		}
		
		
	}

}


執行結果爲:

  1. 小小小訂單下單成功!  
  2. 大大大大大訂單下單成功!  
小小小訂單下單成功!
大大大大大訂單下單成功!

我們看,小任務執行反比後先返回,這個結果就被先被處理了!

在創建ExecutorCompletionService對象completionService時,需要一個線程池的參數,也就是說,completionService在執行任務時,使用的還是線程池!這個對象本身就是去處理線程執行的返回結果。

completionService調用方法take,會阻塞,直到有一個Callable任務執行完畢返回,這個方法就會返回Future對象!completionService還提供一個poll方法,這個方法調用時,如果有Callable任務執行完畢,就返回其Future對象,否則會直接返回null!

使用ExecutorCompletionService提交多個Callable任務,最後會按照返回順序,挨個進行處理!先執行完畢的任務會被先處理掉!

Callable、Future、CompletionService、ExecutorCompletionService幾個類都使用了泛型,泛型即代表了線程的返回結果類型!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章