1.場景
假如有個需求,隨機出10個數,打印比x大的數,那我們可以用for循環這10個數,找到比x大的數即可;
那麼如果是隨機100萬個數中打印比x大的數,那這樣我們就可以充分利用多線程的優勢了。
public static void main(String[] args) {
final int x=4000;
// 10個線程一起跑
for(int y = 0;y<10;y++){
new Thread(()->{
Random random = new Random();
// 循環10萬次
for(int i =0;i<100000;i++){
int num = random.nextInt(1000000);
if(num>x){
System.out.println(num);
}
}
}).start();
}
}
我們發現10個線程一起跑,每個線程選中1萬個數,這樣的速度一定必單線程快。這種方式可以非常有效的發揮系統的性能;
但是產品大大的需求是千變萬化的,突然產品大大說, 把選中的10萬個數全部加起來 。
或者我的一個實戰場景:人臉庫中有10萬左右個人臉,然後用戶掃臉驗證的時候,多線程比對10萬個人臉的相似度,相似度超過0.8的要收集起來,然後全部線程對比完成之後,把所有超過0.8分數的人臉進行處理,分數最大人臉則認爲是用戶的人臉。
我們知道多線程的start()方法之後是執行了run()方法,但是run()方法是沒有返回值的,如果我們的知識只有這點的話,就很棘手了。
像這種場景就需要我們異步編程來做了。異步編程處理之後,可以在線程執行之後的返回值供我們進行處理。
2.線程返回值
2.1 Callable接口
Callable接口和Runable接口及其相似,Callable中call()可以看作爲Runable.run()方法的加強,提供了返回值,並且可以在執行過程中拋出異常。V代表返回值參數類型。
@FunctionalInterface
public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
V call() throws Exception;
}
2.2 ThreadPoolExecutor 類
由於Callable 有返回值,所以在執行過程衝必須使用一個執行器來使用ThreadPoolExecutor
的submit方法來提交任務,get方法獲取任務返回值,如下:
public <T> Future<T> submit(Callable<T> task) ;
// 調用get方法 會一直阻塞,直到任務完成,所以建議使用有等待時間的方法
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
如果get一直阻塞,可以使用boolean cancel(boolean mayInterruptIfRunning);進行任務取消,mayInterruptIfRunning
表示是否允許發送中斷來取消線程,返回值代表是否取消成功。boolean isCancelled();代表是否成功取消,在任務取消之後,如果再次執行get方法會拋異常;
boolean isDone();方法返回知否執行完畢,任務執行完畢、任務執行過程中拋出異常、任務取消都會返回true。
ThreadPoolExecutor構造器參數如下:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
- corePoolSize 線程池核心大小
- maximumPoolSize 線程池最大線程數
- keepAliveTime 和unit 空閒線程存活時間
- workQueue 隊列類型
2.3 代碼示例
瞭解有返回值的多線程之後,就可以處理上面的場景的,不過爲了測試數量調小一點。
public static void main(String[] args) {
final int x=4000;
// 創建執行器對象,參數下面解釋
ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 7, 10, TimeUnit.SECONDS, new LinkedBlockingDeque<>(10));
List<Future<Integer>> list = new ArrayList<>();
// 創建2個線程
for(int y = 0;y<2;y++){
Future<Integer> submit = executor.submit(() -> {
Random random = new Random();
Integer sum = 0;
// 循環2次
for(int i =0;i<2;i++){
int num = random.nextInt(10000);
if(num>x){
sum=sum+num;
}
}
System.out.println(sum);
return sum;
});
list.add(submit);
}
int sum = 0;
for(int y=0;y<list.size();y++){
Future<Integer> integerFuture = list.get(y);
try {
sum=sum+integerFuture.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
System.out.println("總和:"+sum);
}
如上代碼即可完成線程執行完成之後,返回值進行累加。
問題來了,如上代碼比較簡單,線程間執行速度基本一致,但是如果出現線程間速度不一樣的情況就會很大的效率問題。調用get() 方法之後,會一直阻塞,直到當前線程執行完畢爲止,那麼如果線程1線程執行內容比較多,但是線程2執行完畢,主線程也只能等待線程1返回之後再處理線程2。缺陷顯而易見。
3.批量執行異步任務
3.1CompletionService接口
Future接口可以實現獲取異步線程的執行結果,但是如果很多異步任務一起處理的話,實現起來會很繁瑣。java.util.concurrent.CompletionService更好的解決了批量任務的處理。
// 提交任務
Future<V> submit(Callable<V> task);
//獲取任務執行結果
Future<V> take() throws InterruptedException;
//獲取任務執行結果 異步
Future<V> poll();
//獲取任務執行結果 異步
Future<V> poll(long timeout, TimeUnit unit) throws InterruptedException;
submit方法與ThreadPoolExecutor.submit方法效果類似;不同的是take()方法,CompletionService執行了幾次submit()方法,就可以執行幾次take()方法,如果take()執行的時候沒有執行結束的異步任務,那麼該線程就會被暫停,直到有異步任務執行結束。
ExecutorCompletionService構造器
public ExecutorCompletionService(Executor executor)
public ExecutorCompletionService(Executor executor,
BlockingQueue<Future<V>> completionQueue)
通過構造方法很明顯的看出,Executor負責執行異步任務,BlockingQueue負責接收任務執行的結果。
CompletionService接口和ThreadPoolExecutor並不存在繼承關係,只是方法名稱相似而已。ThreadPoolExecutor接口繼承的Executor接口,就一個execute方法,這種抽象使我們不需要關注任務的具體邏輯,達到了解耦的效果。
public interface Executor {
void execute(Runnable command);
}
3.2代碼實例
優化上面實例中,性能問題。通過take方法,即可做到哪個線程執行速度快先處理哪個線程的返回內容。
public static void main(String[] args) {
final int x=4000;
ExecutorService executorService = Executors.newCachedThreadPool();
CompletionService completionService = new ExecutorCompletionService(executorService);
// 創建執行器對象,參數下面解釋
// 創建2個線程
for(int y = 0;y<2;y++ ){
completionService.submit(() -> {
Random random = new Random();
Integer sum = 0;
// 循環2次
for(int i =0;i<2;i++){
int num = random.nextInt(10000);
if(num>x){
sum=sum+num;
}
}
System.out.println(sum);
return sum;
});
}
int sum = 0;
for(int y=0;y<2;y++){
Future<Integer> integerFuture = null;
try {
//通過take方法,即可做到哪個線程執行速度快先處理哪個線程的返回內容
integerFuture = completionService.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
sum=sum+integerFuture.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
executorService.shutdown();
System.out.println("總和:"+sum);
}