java Callable & Future & FutureTask

前言

實現Runnable接口的線程類與一個缺陷,就是在任務執行完之後無法取得任務的返回值。 如果需要獲取執行結果,就必須通過共享變量或者使用線程通信的方式來達到效果,這樣使用起來就比較麻煩 。所以,從JDK 1.5開始,java提供了Callable接口,該接口和Runnable接口相類似,提供了一個call()方法可以作爲線程的執行體,但是call()方法要比run()方法更爲強大:# call()方法可以有返回值;# call()方法可以聲明拋出異常。那麼使用callable接口是如何獲取返回值的呢?

一、Callable與Runnable

既然Callable接口可以看做是Runnable的“增強版”,那我們先看看Runnable接口的實現,追根溯源也是搬磚的普遍素質嘛~

public interface Runnable {
    public abstract void run();
}

Runnable接口中只包含一個抽象方法run()返回值爲void, 所以在執行完任務之後無法返回任何結果。接下來我們可以看看Callable接口

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;
}

這是一個泛型接口,call()函數返回的類型就是傳遞進來的V類型。 那麼該如何使用Callable接口呢?Callable接口不是Runnable接口的子接口,所以Callable對象不能直接作爲Thread的target去運行;而且call方法還有返回值–call()方法並不是直接調用。

JDK提供了Future接口來代表call()方法裏的返回值,並且爲Future接口提供了一個實現類FutureTask,該類實現了Future接口,並實現了Runnable接口,其實該類也是Future接口的唯一實現類,所以FutureTask對象可以作爲Thread的target。Callable一般配合ExecutorService來使用的,在ExecutorService接口中聲明瞭若干個submit方法的重載版本:

/**
* Submits a value-returning task for execution and returns a
* Future representing the pending results of the task. The
* Future's <tt>get</tt> method will return the task's result upon
* successful completion.
*/
<T> Future<T> submit(Callable<T> task);

/**
* Submits a Runnable task for execution and returns a Future
* representing that task. The Future's <tt>get</tt> method will
* return the given result upon successful completion.
*/
<T> Future<T> submit(Runnable task, T result);

/**
* Submits a Runnable task for execution and returns a Future
* representing that task. The Future's <tt>get</tt> method will
* return <tt>null</tt> upon <em>successful</em> completion.
*/
Future<?> submit(Runnable task);

一般情況下我們使用第一個submit方法和第三個submit方法,第二個submit方法很少使用 。

這裏提一下ExecutorService的submit與execute方法的區別:

ExecutorService的submit與execute方法都能執行任務,但在使用過程,發現其對待run方法拋出的異常處理方式不一樣。兩者執行任務最後都會通過Executor的execute方法來執行,但對於submit,會將runnable物件包裝成FutureTask,其run方法會捕捉被包裝的Runnable Object的run方法拋出的Throwable異常,待submit方法所返回的的Future Object調用get方法時,將執行任務時捕獲的Throwable Object包裝成java.util.concurrent.ExecutionException來拋出。
而對於execute方法,則會直接拋出異常,該異常不能被捕獲,想要在出現異常時做些處理,可以實現Thread.UncaughtExceptionHandler接口。

當將一個Callable的對象傳遞給ExecutorService的submit方法,則該call方法自動在一個線程上執行,並且會返回執行結果Future對象。同樣,將Runnable的對象傳遞給ExecutorService的submit方法,則該run方法自動在一個線程上執行,並且會返回執行結果Future對象,但是在該Future對象上調用get方法,將返回null。

二、Future接口

Future就是對於具體的Runnable或者Callable任務的執行結果進行取消、查詢是否完成、獲取結果。必要時可以通過get方法獲取執行結果,該方法會阻塞直到任務返回結果。

Future類位於java.util.concurrent包下 :

public interface Future<V> {
    boolean cancel(boolean mayInterruptIfRunning);
    boolean isCancelled();
    boolean isDone();
    V get() throws InterruptedException, ExecutionException;
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

在Future接口中聲明瞭5個方法,下面依次解釋每個方法的作用:

  • cancel方法用來取消任務,如果取消任務成功則返回true,如果取消任務失敗則返回false。參數 mayInterruptIfRunning表示是否允許取消正在執行卻沒有執行完畢的任務,如果設置true,則表示可以取消正在執行過程中的任務。如果任務已經完成,則無論mayInterruptIfRunning爲true還是false,此方法肯定返回false,即如果取消已經完成的任務會返 回false;如果任務正在執行,若mayInterruptIfRunning設置爲true,則返回true,若 mayInterruptIfRunning設置爲false,則返回false;如果任務還沒有執行,則無論 mayInterruptIfRunning爲true還是false,肯定返回true。

  • isCancelled方法表示任務是否被取消成功,如果在任務正常完成前被取消成功,則返回 true。

  • isDone方法表示任務是否已經完成,若任務完成,則返回true;

  • get()方法用來獲取執行結果,這個方法會產生阻塞,會一直等到任務執行完畢才返回;

  • get(long timeout, TimeUnit unit)用來獲取執行結果,如果在指定時間內,還沒獲取到結果,就直接返回null。

也就是說Future提供了三種功能:

1)判斷任務是否完成;

2)能夠中斷任務;

3)能夠獲取任務執行結果。

因爲Future只是一個接口,所以是無法直接用來創建對象使用的,因此就有了FutureTask。

三、FutureTask實現類

我們先來看一下FutureTask的實現:

public class FutureTask<V> implements RunnableFuture<V>

FutureTask類實現了RunnableFuture接口,我們再來看一下RunnableFuture接口的實現:

public interface RunnableFuture<V> extends Runnable, Future<V> {
    void run();
}

可以看出RunnableFuture繼承了Runnable接口和Future接口,而FutureTask實現了RunnableFuture接口。所以它既可以作爲Runnable被線程執行,又可以作爲Future得到Callable的返回值

FutureTask提供了2個構造器:

public FutureTask(Callable<V> callable) {
}
public FutureTask(Runnable runnable, V result) {
}

如何有童鞋對FutureTask具體內容感興趣,可以看一下這篇文章:FutureTask深入解析

四、使用示例

package com.blog.www.thread;

import com.blog.www.util.ListUtils;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.ThreadFactoryBuilder;

import java.util.List;
import java.util.concurrent.*;

/**
 * 第一個線程
 * <br/>
 *
 * @author :leigq
 * @date :2019/8/13 16:12
 */
public class FirstThread implements Callable<String> {

	private List<String> list;

	public FirstThread(List<String> list) {
		this.list = list;
	}

	@Override
	public String call() {
		System.out.println(list);
		return String.format("My name is %s", Thread.currentThread().getName());
	}
}

class Test{
	public static void main(String[] args) {
		/*
		 * 多線程使用思路:
		 * 1、查詢數據,每次查詢 dataCount 條
		 * 2、開 threadCount 個線程處理數據,每個線程處理 dataCount / threadCount 條
		 * */
		int dataCount = 100, threadCount = 20, maxThreadCount = 30;
		// 使用 ThreadPoolExecutor 創建線程池,阿里 java 開發手冊推薦,詳見:https://blog.csdn.net/fly910905/article/details/81584675
		ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("test-pool-%d").build();
		ExecutorService pool = new ThreadPoolExecutor(threadCount, maxThreadCount, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(1024), threadFactory, new ThreadPoolExecutor.AbortPolicy());

		// 模擬 dataCount 條數據
		List<String> strings = Lists.newArrayList();
		for (int i = 0; i < dataCount; i++) {
			strings.add(String.format("data %d", i));
		}
		// 平分數據
		List<List<String>> lists = ListUtils.avgAssign(strings, threadCount);
		// 保存執行結果
		List<FutureTask<String>> futureTasks = Lists.newArrayList();
		lists.forEach(list -> {
			FirstThread firstThread = new FirstThread(list);
			FutureTask<String> futureTask = new FutureTask<>(firstThread);
			pool.submit(futureTask);
			futureTasks.add(futureTask);
		});

		futureTasks.forEach(futureTask -> {
			try {
				System.out.println(futureTask.get());
			} catch (InterruptedException | ExecutionException e) {
				e.printStackTrace();
			}
		});
		pool.shutdown();
	}
}

執行結果:

[data 0, data 1, data 2, data 3, data 4]
[data 10, data 11, data 12, data 13, data 14]
[data 5, data 6, data 7, data 8, data 9]
[data 15, data 16, data 17, data 18, data 19]
[data 20, data 21, data 22, data 23, data 24]
[data 25, data 26, data 27, data 28, data 29]
[data 30, data 31, data 32, data 33, data 34]
[data 35, data 36, data 37, data 38, data 39]
[data 40, data 41, data 42, data 43, data 44]
[data 45, data 46, data 47, data 48, data 49]
[data 50, data 51, data 52, data 53, data 54]
[data 55, data 56, data 57, data 58, data 59]
[data 60, data 61, data 62, data 63, data 64]
[data 65, data 66, data 67, data 68, data 69]
[data 70, data 71, data 72, data 73, data 74]
[data 75, data 76, data 77, data 78, data 79]
[data 80, data 81, data 82, data 83, data 84]
[data 85, data 86, data 87, data 88, data 89]
[data 90, data 91, data 92, data 93, data 94]
[data 95, data 96, data 97, data 98, data 99]
My name is test-pool-0
My name is test-pool-1
My name is test-pool-2
My name is test-pool-3
My name is test-pool-4
My name is test-pool-5
My name is test-pool-6
My name is test-pool-7
My name is test-pool-8
My name is test-pool-9
My name is test-pool-10
My name is test-pool-11
My name is test-pool-12
My name is test-pool-13
My name is test-pool-14
My name is test-pool-15
My name is test-pool-16
My name is test-pool-17
My name is test-pool-18
My name is test-pool-19

Process finished with exit code 0

上面用到了一個ListUtils.avgAssign(List<T> list, Integer n)方法,如下:

package com.blog.www.util;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * 集合工具類
 * <br/>
 *
 * @author :leigq
 * @date :2019/8/13 14:16
 */
public class ListUtils {

	/**
	 * 按指定大小,分隔集合,將集合按規定個數分爲 n 個部分
	 * <br/>
	 * create by: leigq
	 * <br/>
	 * create time: 2019/8/13 14:16
	 *
	 * @param list         : 待拆分的集合
	 * @param elementCount : 元素個數
	 * @return 拆分後的集合
	 */
	public static <T> List<List<T>> split(List<T> list, Integer elementCount) {
		if (list == null || list.isEmpty() || elementCount < 1) {
			return Collections.emptyList();
		}
		List<List<T>> result = new ArrayList<>();
		int size = list.size();
		int count = (size + elementCount - 1) / elementCount;
		for (int i = 0; i < count; i++) {
			List<T> subList = list.subList(i * elementCount, (Math.min((i + 1) * elementCount, size)));
			result.add(subList);
		}
		return result;
	}

	/**
	 * 將一個list均分成 n 個list
	 * <br/>
	 * create by: leigq
	 * <br/>
	 * create time: 2019/8/13 14:11
	 *
	 * @param list : 待拆分的集合
	 * @param n    : 拆分數量
	 * @return 拆分後的集合, if list.size > n, 平分或者前幾個集合有多個元素; if list.size < n, 後幾集合爲空
	 */
	public static <T> List<List<T>> avgAssign(List<T> list, Integer n) {
		List<List<T>> result = new ArrayList<>();
		// 先計算出餘數
		int remainder = list.size() % n;
		// 然後是商
		int number = list.size() / n;
		//偏移量
		int offset = 0;
		for (int i = 0; i < n; i++) {
			List<T> value;
			if (remainder > 0) {
				value = list.subList(i * number + offset, (i + 1) * number + offset + 1);
				remainder--;
				offset++;
			} else {
				value = list.subList(i * number + offset, (i + 1) * number + offset);
			}
			result.add(value);
		}
		return result;
	}
}

還有一點需要注意,就是在線程類中注入Springbean:詳見:https://www.cnblogs.com/xuyuanjia/p/6097875.html

感謝

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