J.U.C-FutureTask
在Java中一般通過繼承Thread類或者實現Runnable接口這兩種方式來創建線程,但是這兩種方式都有個缺陷,就是不能在執行完成后獲取執行的結果,因此Java 1.5之后提供了Callable和Future接口,通過它們就可以在任務執行完畢之后得到任務的執行結果。
而FutureTask則是J.U.C中的類,但不是AQS的子類,FutureTask是一個可刪除的異步計算類。這個類提供了Future接口的的基本實現,使用相關方法啟動和取消計算,查詢計算是否完成,并檢索計算結果。只有在計算完成時才能使用get方法檢索結果;如果計算尚未完成,get方法將會阻塞。一旦計算完成,計算就不能重新啟動或取消(除非使用runAndReset方法調用計算)。
Runnable與Callable以及Future接口對比:
Runnable是一個接口,在它里面只聲明了一個run()方法。由于run()方法返回值為void類型,所以在執行完任務之后無法返回任何結果:
public interface Runnable {
public abstract void run();
}
Callable接口也只聲明了一個方法,這個方法叫做call()
。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;
}
可以看到Callable是個泛型接口,泛型V就是要call()
方法返回的類型。Callable接口和Runnable接口很像,都可以被另外一個線程執行,但是正如前面所說的,Runnable不會返回數據也不能拋出異常。
Future也是一個接口,Future接口代表異步計算的結果,通過Future接口提供的方法可以查看異步計算是否執行完成,或者等待執行結果并獲取執行結果,同時還可以取消執行。說白了Future就是對于具體的Runnable或者Callable任務的執行結果進行取消、查詢是否完成以及獲取執行結果。其中執行結果通過get方法獲取,該方法會阻塞直到任務返回結果。Future接口的定義如下:
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()方法用來取消異步任務的執行。如果異步任務已經完成或者已經被取消,或者由于某些原因不能取消,則會返回false。如果任務還沒有被執行,則會返回true并且異步任務不會被執行。如果任務已經開始執行了但是還沒有執行完成,若mayInterruptIfRunning為true,則會立即中斷執行任務的線程并返回true,若mayInterruptIfRunning為false,則會返回true且不會中斷任務執行線程。
isCanceled()方法用于判斷任務是否被取消,如果任務在結束(正常執行結束或者執行異常結束)前被取消則返回true,否則返回false。
isDone()方法用于判斷任務是否已經完成,如果完成則返回true,否則返回false。需要注意的是:任務執行過程中發生異常、任務被取消也屬于任務已完成,也會返回true。
get()方法用于獲取任務執行結果,如果任務還沒完成則會阻塞等待直到任務執行完成。如果任務被取消則會拋出CancellationException異常,如果任務執行過程發生異常則會拋出ExecutionException異常,如果阻塞等待過程中被中斷則會拋出InterruptedException異常。
get(long timeout,Timeunit unit)是帶超時時間的get()版本,如果阻塞等待過程中超時則會拋出TimeoutException異常。
綜上,Future主要提供了三種功能:
- 判斷任務是否完成;
- 能夠中斷任務;
- 能夠獲取任務執行結果。
因為Future只是一個接口,所以是無法直接用來創建對象使用的,因此就有了下面的FutureTask。FutureTask的父類是RunnableFuture,而RunnableFuture則繼承了Runnable和Future這兩個接口。所以由此可知,FutureTask最終也屬于是Callable類型的任務。如果往FutureTask的構造函數傳入Runnable的話,也會被轉換成Callable類型。
FutureTask繼承圖如下:
可以看到,FutureTask實現了RunnableFuture接口,則RunnableFuture接口繼承了Runnable接口和Future接口,所以FutureTask既能當做一個Runnable直接被Thread執行,也能作為Future用來得到Callable的計算結果。
使用場景:
假設有一個很費時的邏輯需要計算,并且需要返回計算的結果,但這個結果又不是馬上需要的。那么這時就可以使用FutureTask,用另外一個線程去進行計算,而當前線程在得到這個計算結果之前,就可以去執行其他的操作,等到需要這個結果時再通過Future得到即可。
FutureTask有兩個構造器,支持傳入Callable和Runnable類型,在使用 Runnable 時,需要多指定一個返回結果類型:
public FutureTask(Callable<V> callable) {
if (callable == null)
throw new NullPointerException();
this.callable = callable;
this.state = NEW; // ensure visibility of callable
}
public FutureTask(Runnable runnable, V result) {
this.callable = Executors.callable(runnable, result);
this.state = NEW; // ensure visibility of callable
}
使用示例
1.Future基本使用示例:
@Slf4j
public class FutureExample {
public static void main(String[] args) throws Exception {
ExecutorService executorService = Executors.newCachedThreadPool();
// 使用lambda創建callable任務,使用Future接收任務執行的結果
Future<String> future = executorService.submit(() -> {
log.info("do something in callable");
Thread.sleep(5000);
return "Done";
});
log.info("do something in main");
Thread.sleep(1000);
// 獲取執行結果
String result = future.get();
log.info("result: {}", result);
executorService.shutdown();
}
}
2.FutureTask基本使用示例:
@Slf4j
public class FutureTaskExample {
public static void main(String[] args) throws Exception {
// 構建FutureTask實例,使用lambda創建callable任務
FutureTask<String> futureTask = new FutureTask<>(() -> {
log.info("do something in callable");
Thread.sleep(5000);
return "Done";
});
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(futureTask);
log.info("do something in main");
Thread.sleep(1000);
// 獲取執行結果
String result = futureTask.get();
log.info("result: {}", result);
executorService.shutdown();
}
}
從以上兩個示例可以看到,Future和FutureTask的使用方式是很相似的,畢竟FutureTask就是Future的一個實現。
J.U.C-ForkJoin
Fork/Join框架是Java7提供了的一個用于并行執行任務的框架, 是一個把大任務分割成若干個小任務,最終匯總每個小任務結果后得到大任務結果的框架,其思想和map-reduce非常類似。
我們再通過Fork和Join這兩個單詞來理解下Fork/Join框架,Fork就是把一個大任務切分為若干子任務并行的執行,Join就是合并這些子任務的執行結果,最后得到這個大任務的結果。比如計算1+2+。。+10000,可以分割成10個子任務,每個子任務分別對1000個數進行求和,最終匯總這10個子任務的結果。Fork/Join的運行流程圖如下:
工作竊取算法:
Fork/Join框架主要采用的是工作竊取(work-stealing)算法,該算法是指某個線程從其他隊列里竊取任務來執行。工作竊取的運行流程圖如下:
那么為什么需要使用工作竊取算法呢?假如我們需要做一個比較大的任務,我們可以把這個任務分割為若干互不依賴的子任務,為了減少線程間的競爭,于是把這些子任務分別放到不同的隊列里,并為每個隊列創建一個單獨的線程來執行隊列里的任務,線程和隊列一一對應,比如A線程負責處理A隊列里的任務。但是有的線程會先把自己隊列里的任務干完,而其他線程對應的隊列里還有任務等待處理。干完活的線程與其等著,不如去幫其他線程干活,于是它就去其他線程的隊列里竊取一個任務來執行。而在這時它們會訪問同一個隊列,所以為了減少竊取任務線程和被竊取任務線程之間的競爭,通常會使用雙端隊列,被竊取任務線程永遠從雙端隊列的頭部拿任務執行,而竊取任務的線程永遠從雙端隊列的尾部拿任務執行。
工作竊取算法的優點是充分利用線程進行并行計算,并減少了線程間的競爭,其缺點是在某些情況下還是存在競爭,比如雙端隊列里只有一個任務時。并且消耗了更多的系統資源,比如創建多個線程和多個雙端隊列。
所以對于Fork/Join框架而言,當一個任務正在等待它使用join操作創建的子任務的結束時,執行這個任務的線程(工作線程)查找其他未被執行的任務并開始它的執行。通過這種方式,線程充分利用它們的運行時間,從而提高了應用程序的性能。
為實現這個目標,Fork/Join框架執行的任務有以下局限性:
- 任務只能使用
fork()
和join()
操作,作為同步機制。如果使用其他同步機制,工作線程不能執行其他任務,當它們在同步操作時。比如,在Fork/Join框架中,你使任務進入睡眠,那么在這睡眠期間內,正在執行這個任務的工作線程將不會執行其他任務。 - 任務不應該執行I/O操作,如讀或寫數據文件。
- 任務不能拋出檢查異常,它必須包括必要的代碼來處理它們。
Fork/Join框架的核心主要是以下兩個類:
- ForkJoinPool:它實現ExecutorService接口和work-stealing算法。它管理工作線程和提供關于任務的狀態和它們執行的信息。
- ForkJoinTask: 它是將在ForkJoinPool中執行的任務的基類。它提供在任務中執行
fork()
和join()
操作的機制,并且這兩個方法控制任務的狀態。通常, 為了實現你的Fork/Join任務,你將實現兩個子類的子類的類:RecursiveAction對于沒有返回結果的任務和RecursiveTask 對于返回結果的任務。
Fork/Join使用示例,完成1+2+3+4...+n的計算,代碼如下:
package org.zero.concurrency.demo.example.aqs;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.Future;
import java.util.concurrent.RecursiveTask;
/**
* @program: concurrency-demo
* @description: ForkJoin 使用示例
* @author: 01
* @create: 2018-10-19 20:12
**/
@Slf4j
public class ForkJoinTaskExample extends RecursiveTask<Integer> {
private static final int THRESHOLD = 2;
private int start;
private int end;
private ForkJoinTaskExample(int start, int end) {
this.start = start;
this.end = end;
}
@Override
protected Integer compute() {
int sum = 0;
//如果任務足夠小就直接計算任務
boolean canCompute = (end - start) <= THRESHOLD;
if (canCompute) {
for (int i = start; i <= end; i++) {
sum += i;
}
} else {
// 如果任務大于閾值,就分裂成兩個子任務計算
int middle = (start + end) / 2;
ForkJoinTaskExample leftTask = new ForkJoinTaskExample(start, middle);
ForkJoinTaskExample rightTask = new ForkJoinTaskExample(middle + 1, end);
// 執行子任務
leftTask.fork();
rightTask.fork();
// 等待任務執行結束合并其結果
int leftResult = leftTask.join();
int rightResult = rightTask.join();
// 合并子任務
sum = leftResult + rightResult;
}
return sum;
}
public static void main(String[] args) {
ForkJoinPool forkjoinPool = new ForkJoinPool();
//生成一個計算任務,計算1+2+3+4...+100
ForkJoinTaskExample task = new ForkJoinTaskExample(1, 100);
//執行一個任務
Future<Integer> result = forkjoinPool.submit(task);
try {
log.info("result:{}", result.get());
} catch (Exception e) {
log.error("exception", e);
}
}
}
J.U.C-BlockingQueue
在新增的Concurrent包中,BlockingQueue很好的解決了多線程中,如何高效安全“傳輸”數據的問題,從名字也可以知道它是線程安全的。通過這些高效并且線程安全的隊列類,為我們快速搭建高質量的多線程程序帶來極大的便利。
首先,最基本的來說, BlockingQueue 是一個先進先出的隊列(Queue),為什么說是阻塞(Blocking)的呢?是因為 BlockingQueue 支持當獲取隊列元素但是隊列為空時,會阻塞等待隊列中有元素再返回;也支持添加元素時,如果隊列已滿,那么等到隊列可以放入新元素時再放入。所以 BlockingQueue 主要應用于生產者消費者場景。
BlockingQueue 是一個接口,繼承自 Queue,所以其實現類也可以作為 Queue 的實現來使用,而 Queue 又繼承自 Collection 接口。
BlockingQueue 對插入操作、移除操作、獲取元素操作提供了四種不同的方法用于不同的場景中使用,總結如下表:
- | Throws exception | Special value | Blocks | Times out |
---|---|---|---|---|
Insert | add(e) | offer(e) | put(e) | offer(e, time, unit) |
Insert | remove() | poll() | take() | poll(time, unit) |
Examine | element() | peek() | not applicable | not applicable |
說明:
1、Throws Exceptions :如果不能立即執行就拋出異常
2、Special Value:如果不能立即執行就返回一個特殊的值(null 或 true/false,取決于具體的操作)
3、Blocks:如果不能立即執行就阻塞等待此操作,直到這個操作成功
4、Times Out:如果不能立即執行就阻塞一段時間,直到成功或者超時指定時間
BlockingQueue 的實現類:
ArrayBlockingQueue:它是一個有界的阻塞隊列,內部實現是數組,需在初始化時指定容量大小,一旦指定大小就不能再變。采用FIFO方式存儲元素:
public class ArrayBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
/** The queued items */
final Object[] items;
...
}
DelayQueue:阻塞內部元素,DelayQueue內部元素必須實現Delayed接口,Delayed接口又繼承了Comparable接口,原因在于DelayQueue內部元素需要排序,一般情況下按元素過期時間優先級排序:
public interface Delayed extends Comparable<Delayed> {
long getDelay(TimeUnit unit);
}
DalayQueue內部采用PriorityQueue與ReentrantLock實現:
public class DelayQueue<E extends Delayed> extends AbstractQueue<E>
implements BlockingQueue<E> {
private final transient ReentrantLock lock = new ReentrantLock();
private final PriorityQueue<E> q = new PriorityQueue<E>();
...
}
LinkedBlockingQueue:使用獨占鎖實現的阻塞隊列,大小配置可選,如果初始化時指定了大小,那么它就是有邊界的。不指定就無邊界(最大整型值)。內部實現是鏈表,采用FIFO形式保存數據。
public LinkedBlockingQueue() {
// 不指定大小,無邊界采用默認值,最大整型值
this(Integer.MAX_VALUE);
}
PriorityBlockingQueue:帶優先級的無界阻塞隊列,無邊界隊列,允許插入null。插入的對象必須實現Comparator接口,隊列優先級的排序規則就是按照我們對Comparable接口的實現來指定的。我們可以從PriorityBlockingQueue中獲取一個迭代器,但這個迭代器并不保證能按照優先級的順序進行迭代:
public class PriorityBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
...
public boolean add(E e) {
return offer(e);
}
public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
final ReentrantLock lock = this.lock;
lock.lock();
int n, cap;
Object[] es;
while ((n = size) >= (cap = (es = queue).length))
tryGrow(es, cap);
try {
//必須實現Comparator接口
final Comparator<? super E> cmp;
if ((cmp = comparator) == null)
siftUpComparable(n, e, es);
else
siftUpUsingComparator(n, e, es, cmp);
size = n + 1;
notEmpty.signal();
} finally {
lock.unlock();
}
return true;
}
...
}
SynchronousQueue:同步阻塞隊列,只能插入一個元素,無界非緩存隊列,不存儲元素。其內部并沒有數據緩存空間,你不能調用peek()方法來看隊列中是否有數據元素,當然遍歷這個隊列的操作也是不允許的:
public class SynchronousQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
...
}
參考: