2.線程的併發工具類
2.1 Fork-Join
JDK 7中引入了fork-join框架,專門來解決計算密集型的任務。可以將一個大任務,拆分成若干個小任務,如下圖所示:
Fork-Join框架利用了分而治之的思想:什麼是分而治之?規模爲N的問題,N<閾值,直接解決,N>閾值,將N分解爲K個小規模子問題,子問題互相對立,與原問題形式相同,將子問題的解合併得到原問題的解.
具體使用中,需要向ForkJoinPool線程池提交一個ForkJoinTask任務。ForkJoinTask任務有兩個重要的子類,RecursiveAction類和RecursiveTask類,他們分別表示沒有返回值的任務和可以有返回值的任務。
RecursiveAction類
下面的例子中,我們使用RecursiveAction遍歷指定目錄來尋找特定類型文件,需要實現compute方法。
public class FindDirsFiles extends RecursiveAction{
private File path;//當前任務需要搜尋的目錄
public FindDirsFiles(File path) {
this.path = path;
}
public static void main(String [] args){
try {
// 用一個 ForkJoinPool 實例調度總任務
ForkJoinPool pool = new ForkJoinPool();
FindDirsFiles task = new FindDirsFiles(new File("F:/"));
pool.execute(task);//異步調用
System.out.println("Task is Running......");
Thread.sleep(1);
int otherWork = 0;
for(int i=0;i<100;i++){
otherWork = otherWork+i;
}
System.out.println("Main Thread done sth......,otherWork="+otherWork);
task.join();//阻塞的方法
System.out.println("Task end");
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
protected void compute() {
List<FindDirsFiles> subTasks = new ArrayList<>();
File[] files = path.listFiles();
if(files!=null) {
for(File file:files) {
if(file.isDirectory()) {
subTasks.add(new FindDirsFiles(file));
}else {
//遇到文件,檢查
if(file.getAbsolutePath().endsWith("txt")) {
System.out.println("文件:"+file.getAbsolutePath());
}
}
}
if(!subTasks.isEmpty()) {
for(FindDirsFiles subTask:invokeAll(subTasks)) {
subTask.join();//等待子任務執行完成
}
}
}
}
}
RecursiveTask類
下面的例子中,利用RecursiveTask來實現數值累加。
public class SumArray {
private static class SumTask extends RecursiveTask<Integer>{
private final static int THRESHOLD = MakeArray.ARRAY_LENGTH/10;
private int[] src; //表示我們要實際統計的數組
private int fromIndex;//開始統計的下標
private int toIndex;//統計到哪裏結束的下標
public SumTask(int[] src, int fromIndex, int toIndex) {
this.src = src;
this.fromIndex = fromIndex;
this.toIndex = toIndex;
}
@Override
protected Integer compute() {
if(toIndex-fromIndex < THRESHOLD) {
int count = 0;
for(int i=fromIndex;i<=toIndex;i++) {
//SleepTools.ms(1);
count = count + src[i];
}
return count;
}else {
//fromIndex....mid....toIndex
//1...................70....100
int mid = (fromIndex+toIndex)/2;
SumTask left = new SumTask(src,fromIndex,mid);
SumTask right = new SumTask(src,mid+1,toIndex);
invokeAll(left,right);
// left.fork();
// right.fork();
return left.join()+right.join();
}
}
}
public static void main(String[] args) {
ForkJoinPool pool = new ForkJoinPool();
int[] src = MakeArray.makeArray();
SumTask innerFind = new SumTask(src,0,src.length-1);
long start = System.currentTimeMillis();
pool.invoke(innerFind);//同步調用
System.out.println("Task is Running.....");
System.out.println("The count is "+innerFind.join()
+" spend time:"+(System.currentTimeMillis()-start)+"ms");
}
}
注意到fork和invokeAll都能達到相同的效果,只是fork將task交給工作線程後立刻返回;但是invokeAll會fork其中一個任務後,同時同步的調用另外一個任務,然後等待兩個任務完成,可以參考invokeAll的實現:
ublic static void invokeAll(ForkJoinTask<?> t1, ForkJoinTask<?> t2) {
int s1, s2;
t2.fork();
if (((s1 = t1.doInvoke()) & ABNORMAL) != 0)
t1.reportException(s1);
if (((s2 = t2.doJoin()) & ABNORMAL) != 0)
t2.reportException(s2);
}
工作密取(Work Stealing)
在後臺,fork-join框架使用了一種有效的方法來平衡可用線程的負載,稱爲工作密取(Work stealing)。每個工作線程都有一個雙端隊列(deque)來完成任務,一個工作線程將子任務壓入其雙端隊列的隊頭。當一個工作線程空閒時,它會從另一個雙端隊列的隊尾密取一個任務。
ForkJoinPool內部利用循環數組實現了一個雙端隊列,稱爲WorkQueue。對於這個Queue,有3種操作方法,分別是push、pop和poll。對於push和pop操作,只能被擁有該Queue的線程所調用。poll操作被用於其他工作線程從該Queue中獲得task。
考慮到多線程steal work的情況,當進行poll操作時,會通過CAS操作來保證多線程下的安全性。如果CAS操作成功,則說明竊取成功。
更多細節可以查看ForkJoinPool的實現以及論文https://www.dre.vanderbilt.edu/~schmidt/PDF/work-stealing-dequeue.pdf。
invoke && execute && submit 區別
invoke是同步調用,它會馬上執行執行,並且將task join到當前線程,也就是阻塞當前線程。
/**
* Performs the given task, returning its result upon completion.
* If the computation encounters an unchecked Exception or Error,
* it is rethrown as the outcome of this invocation. Rethrown
* exceptions behave in the same way as regular exceptions, but,
* when possible, contain stack traces (as displayed for example
* using {@code ex.printStackTrace()}) of both the current thread
* as well as the thread actually encountering the exception;
* minimally only the latter.
*
* @param task the task
* @param <T> the type of the task's result
* @return the task's result
* @throws NullPointerException if the task is null
* @throws RejectedExecutionException if the task cannot be
* scheduled for execution
*/
public <T> T invoke(ForkJoinTask<T> task) {
if (task == null)
throw new NullPointerException();
externalSubmit(task);
return task.join();
}
execute和submit是異步調用,它會將Task送到Work Queue中等待運行。如果需要看到運行結果,可以在execute和submit後調用join方法。兩者的區別只是submit會返回task,execute返回空值。
/**
* Submits a ForkJoinTask for execution.
*
* @param task the task to submit
* @param <T> the type of the task's result
* @return the task
* @throws NullPointerException if the task is null
* @throws RejectedExecutionException if the task cannot be
* scheduled for execution
*/
public <T> ForkJoinTask<T> submit(ForkJoinTask<T> task) {
return externalSubmit(task);
}
下面是execute的實現:
/**
* Arranges for (asynchronous) execution of the given task.
*
* @param task the task
* @throws NullPointerException if the task is null
* @throws RejectedExecutionException if the task cannot be
* scheduled for execution
*/
public void execute(ForkJoinTask<?> task) {
externalSubmit(task);
}
2.2 CountDownLatch
Latch是門栓的意思,顧名思義,CountDownLatch是一個多線程的控制工具類。通常用於讓一組線程等待直到倒計時結束,再開始執行。
CountDownLatch的用法如下,
- 初始化count down的次數;
- 在初始化線程中調用countDown對計數器進行減1;
- 工作線程中調用await進行等待,當計時器爲0時,工作線程開始工作。
public class UseCountDownLatch {
static CountDownLatch latch = new CountDownLatch(6);
// 初始化線程(只有一步,有4個)
private static class InitThread implements Runnable{
@Override
public void run() {
System.out.println("Thread_"+Thread.currentThread().getId() +" finish init work......");
latch.countDown();//初始化線程完成工作了,countDown方法只扣減一次;
// We can add some tasks after the countDown is invoked
for(int i =0;i<2;i++) {
System.out.println("Thread_"+Thread.currentThread().getId() +" ........continue do its work");
}
}
}
// 業務線程
private static class BusinessThread implements Runnable{
@Override
public void run() {
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
for(int i =0;i<3;i++) {
System.out.println("BusinessThread_"+Thread.currentThread().getId() +" start to do business-----");
}
}
}
public static void main(String[] args) throws InterruptedException {
// 單獨的初始化線程,初始化分爲2步,需要扣減兩次
new Thread(new Runnable() {
@Override
public void run() {
SleepTools.ms(1);
System.out.println("Thread_"+Thread.currentThread().getId() +" finish init work step 1st......");
latch.countDown();//每完成一步初始化工作,扣減一次
System.out.println("begin step 2nd.......");
SleepTools.ms(1);
System.out.println("Thread_"+Thread.currentThread().getId() +" finish init work step 2nd......");
latch.countDown();//每完成一步初始化工作,扣減一次
}
}).start();
new Thread(new BusinessThread()).start();
// Start 3 new init thread
for(int i=0;i<=3;i++){
Thread thread = new Thread(new InitThread());
thread.start();
}
latch.await();
System.out.println("Main do ites work........");
}
}
2.3 CyclicBarrier
CyclicBarrier類實現了一個集結點,稱爲屏障(barrier)。當一個線程完成了那部分任務之後,它運行到屏障處,一旦所有線程都到達了這個屏障,屏障就撤銷,線程就可以繼續運行了。
CyclicBarrier的用法如下:
- 構造一個Barrier,需要給出參與的線程數。JDK裏提供了兩個構造函數,barrierAction爲屏障打開之後需要執行的action。
CyclicBarrier(int parties)
CyclicBarrier(int parties, Runnable barrierAction)
- 每個線程做一些事情,完成後在屏障上調用await等待,
public void run() {
doSomeWork();
barrier.await();
...
}
- 當所有線程都到達了await後,此時屏障打開。如果有定義屏障打開後執行的action,則會先執行action。然後其他線程繼續往下執行await後面的部分。
下面是具體的例子:
在打開屏障後,輸出了各個線程的id。
public class UseCyclicBarrier {
private static CyclicBarrier barrier = new CyclicBarrier(5,new TaskAfterBarrierIsOpenThread());
private static ConcurrentHashMap<String,Long> resultMap = new ConcurrentHashMap<>();//存放子線程工作結果的容器
public static void main(String[] args) {
for(int i=0;i< 5;i++){
Thread thread = new Thread(new SubThread());
thread.start();
}
}
//負責屏障開放以後的工作
private static class TaskAfterBarrierIsOpenThread implements Runnable{
@Override
public void run() {
StringBuilder result = new StringBuilder();
for(Map.Entry<String,Long> workResult:resultMap.entrySet()){
result.append("["+workResult.getValue()+"]");
}
System.out.println(" the result = "+ result);
System.out.println("do other business........");
}
}
//工作線程
private static class SubThread implements Runnable{
@Override
public void run() {
long id = Thread.currentThread().getId();//線程本身的處理結果
resultMap.put(Thread.currentThread().getId()+"",id);
Random r = new Random();//隨機決定工作線程的是否睡眠
try {
if(r.nextBoolean()) {
Thread.sleep(2000+id);
System.out.println("Thread_"+id+" ....do something ");
}
System.out.println(id+"....is await");
barrier.await();
Thread.sleep(1000+id);
System.out.println("Thread_"+id+" ....do its business ");
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
注意屏障是可以重複使用的,當所有等待線程被釋放後可以被重用。
CountDownLatch和CyclicBarrier對比
countdownlatch的放行由第三者控制,CyclicBarrier放行由一組線程本身控制
countdownlatch放行條件 >= 線程數,CyclicBarrier放行條件 = 線程數
2.4 Semaphore
Semaphore也叫信號量,在JDK1.5被引入,可以用來控制同時訪問特定資源的線程數量,通過協調各個線程,以保證合理的使用資源。
Semaphore內部維護了一組虛擬的許可,許可的數量可以通過構造函數的參數指定。
- 訪問特定資源前,必須使用acquire方法獲得許可,如果許可數量爲0,該線程則一直阻塞,直到有可用許可。
- 訪問資源後,使用release釋放許可。
示例程序如下:
public class MySemaphoreTest {
static Semaphore semaphore = new Semaphore(4);
private static class BusinessThread extends Thread {
String name = "";
BusinessThread(String name) {
this.name = name;
}
@Override
public void run() {
try {
System.out.println(name + " try to acquire lock...");
System.out.println(name + " : available Semaphore permits now: " + semaphore.availablePermits());
semaphore.acquire();
System.out.println(name + " : got the permit!");
// Do some business work
Thread.sleep(1000);
System.out.println(name + " : release lock...");
semaphore.release();
} catch (Exception e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
for (int i = 0; i < 6; i++) {
new BusinessThread((i+1) + "").start();
}
}
}
2.5 Exchanger
當兩個線程在同一個數據緩衝區的兩個實例上工作時,就可以使用Exchanger。典型的情況是,一個線程向緩衝區填入數據,另一個線程消耗這些數據。當他們都完成之後,相互交換緩衝區。
下面的例子中,在兩個線程中分別填入數據,然後交換數據,最後打印從對方線程交換得來的數據。
public class UseExchange {
private static final Exchanger<Set<String>> exchange
= new Exchanger<Set<String>>();
public static void main(String[] args) {
//第一個線程
new Thread(new Runnable() {
@Override
public void run() {
Set<String> setA = new HashSet<String>();//存放數據的容器
try {
setA.add("1");
System.out.println(Thread.currentThread().getName() + " Add 1 to the set");
setA.add("2");
System.out.println(Thread.currentThread().getName() + " Add 2 to the set");
setA.add("3");
System.out.println(Thread.currentThread().getName() + " Add 3 to the set");
setA = exchange.exchange(setA);//交換setA出去,返回交換來的數據setB
/*處理交換後的數據*/
System.out.println(Thread.currentThread().getName() + " print the data after exchange ");
setA.forEach(string -> System.out.println(Thread.currentThread().getName() + " print" + string));
} catch (InterruptedException e) {
}
}
}).start();
//第二個線程
new Thread(new Runnable() {
@Override
public void run() {
Set<String> setB = new HashSet<String>();//存放數據的容器
try {
setB.add("A");
System.out.println(Thread.currentThread().getName() + " Add A to the set");
setB.add("B");
System.out.println(Thread.currentThread().getName() + " Add B to the set");
setB.add("C");
System.out.println(Thread.currentThread().getName() + " Add C to the set");
setB = exchange.exchange(setB);//交換setB出去,返回交換來的數據setA
/*處理交換後的數據*/
System.out.println(Thread.currentThread().getName() + " print the data after exchange ");
setB.forEach(string -> System.out.println(Thread.currentThread().getName() + " print" + string));
} catch (InterruptedException e) {
}
}
}).start();
}
}
2.6 Future、Callable和FutureTask
Future是多線程開發中的一種常見設計模式,核心思想是異步調用。當需要調用一個函數方法時,如果這個函數很慢,需要進行等待,這時可以先處理一些其他任務,在真正需要數據的時候再去嘗試獲得需要的數據。
以上是Future的基本結構,RunnableFuture繼承了Future和Runnable接口,FutureTask可以接收一個Callable實例作爲運行的任務。
Future的使用比較簡單,例子如下:
public class UseFuture {
/*實現Callable接口,允許有返回值*/
private static class UseCallable implements Callable<Integer>{
private int sum;
@Override
public Integer call() throws Exception {
System.out.println("Callable子線程開始計算");
Thread.sleep(2000);
for(int i=0;i<5000;i++) {
sum = sum+i;
}
System.out.println("Callable子線程計算完成,結果="+sum);
return sum;
}
}
public static void main(String[] args)
throws InterruptedException, ExecutionException {
UseCallable useCallable = new UseCallable();
FutureTask<Integer> futureTask = new FutureTask<Integer>(useCallable);
new Thread(futureTask).start();
Random r = new Random();
SleepTools.second(1);
if(r.nextBoolean()) {//隨機決定是獲得結果還是終止任務
System.out.println("Get UseCallable result = "+futureTask.get());
}else {
System.out.println("中斷計算");
futureTask.cancel(true);
}
}
}
注意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。
本文由『後端精進之路』原創,首發於博客 http://teckee.github.io/ , 轉載請註明出處
搜索『後端精進之路』關注公衆號,立刻獲取最新文章和價值2000元的BATJ精品面試課程。