簡述Java執行緒池以及使用
建立執行緒的幾種方式
- 繼承Thread,重寫run()方法
- 實現Runnable介面,實現run()方法
- 實現Callable介面,實現Call()方法
- 使用執行緒池,並向其提交任務task,其內部自動建立執行緒排程完成。
上述對比:
一般來說,使用第一種和第二種比較普遍,考慮到Java不支援多繼承,通常使用第二種,實現Runnable介面建立執行緒。
然而,假設建立執行緒進行一些複雜的處理,比如需要執行後做出反饋,這時候可以使用實現Callable方式建立執行緒。
About Callable的返回值,可以使用Future或者FutureTask來獲取。
Future
它是一個介面,包含方法:
boolean cancel();
嘗試取消任務,返回ture包括(任務已完成,任務成功取消)
boolean isCancelled();
區別於上一種方法這個更像是一個未完成狀態(完成任務之前的取消),不包括已完成
boolean isDone();
判斷任務是否完成
get();
獲取執行結果,如果任務在執行該方法會阻塞,直到有返回值
get(long timeout,TimeUnitunit);
在有限時間內阻塞得到返回值,否則返回null
FutureTask:
底部實現RunnableFuture介面,而RunnableFuture
所以FutureTask同樣具備上述5個方法。
參考部分原始碼:
/**
* Creates a {@code FutureTask} that will, upon running, execute the
* given {@code Callable}.
*
* @param callable the callable task
* @throws NullPointerException if the callable is null
*/
public FutureTask (Callable<V> callable) {
if (callable == null)
throw new NullPointerException();
this.callable = callable;
this.state = NEW; // ensure visibility of callable
}
/**
* Creates a {@code FutureTask} that will, upon running, execute the
* given {@code Runnable}, and arrange that {@code get} will return the
* given result on successful completion.
*
* @param runnable the runnable task
* @param result the result to return on successful completion. If
* you don't need a particular result, consider using
* constructions of the form:
* {@code Future<?> f = new FutureTask<Void>(runnable, null)}
* @throws NullPointerException if the runnable is null
*/
public FutureTask(Runnable runnable, V result) {
this.callable = Executors.callable(runnable, result);
this.state = NEW; // ensure visibility of callable
}
可以看到FutureTask可以傳入引數Callable或者Runnable,Result作為構造方法,區別在於:
1. 前者呼叫get()方法獲取的是Callable的call()返回值
2. 後者傳入引數Runnable和Result,如果執行成功,返回Result
Futuretask和Callable實現建立執行緒的Demo
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class CallableThread {
public static void main(String[] args) {
ThreadCall tc = new ThreadCall();
FutureTask<String> result = new FutureTask<>(tc);
Thread t = new Thread(result);
t.start();
while (true) {
if (result.isDone()) {
try {
System.out.println("Call執行結束:"+result.get());
break;
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}
}
}
class ThreadCall implements Callable<String>{
@Override
public String call() throws Exception {
return "為Java打Call!";
}
}
執行結果:
Call執行結束:為Java打Call!
執行緒池:
執行緒池可以避免頻繁建立銷燬執行緒,提高效能。常見如資料庫的連線池。
在JUC(java.util.concurrent)中,Java提供了一個工具類Executors用於建立執行緒池[多種型別],返回一個執行器,再將Runnable或者Callable提交到執行器中[詳見demo]。
執行緒池的幾個型別:
- newCachedThreadPool(int nThreads)
接受任務才建立執行緒,如果某個執行緒空閒超過60秒,則會被撤銷。 - newFixedThreadPool()
固定執行緒數量的執行緒池,如果執行緒池數量超過nThreads,新任務會放在任務佇列中,如果某個執行緒終止了,另一個執行緒會來取代它。 - newSingleThreadExecutor()
只有一個執行緒的執行緒池,依次執行任務 - newScheduledThreadPool(int corePoolSize)
指定按照設定的時間週期執行任務,池中執行緒數量根據引數corePoolSize決定,即時有執行緒空閒狀態也不會撤銷。
Demo:
package com.dd.code.ThreadByCallable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TestThreadPool {
public static void main(String[] args) {
//建立執行緒池(5個執行緒)
ExecutorService pool = Executors.newFixedThreadPool(5);
ThreadPoolDemo tpd = new ThreadPoolDemo();
//為執行緒池分配10個任務
for (int i = 0; i < 10; i++) {
pool.submit(tpd);
}
//關閉執行緒池(等待任務執行完成才關閉),區別於shutdownnow立即關閉
pool.shutdown();
}
}
class ThreadPoolDemo implements Runnable{
private int i = 0;
@Override
public void run() {
System.out.println("當前執行緒號:"+Thread.currentThread().getName() + ",i = " + i++);
}
}
執行結果:
當前執行緒號:pool-1-thread-3,i = 2
當前執行緒號:pool-1-thread-5,i = 3
當前執行緒號:pool-1-thread-1,i = 0
當前執行緒號:pool-1-thread-2,i = 1
當前執行緒號:pool-1-thread-3,i = 6
當前執行緒號:pool-1-thread-5,i = 5
當前執行緒號:pool-1-thread-4,i = 4
當前執行緒號:pool-1-thread-3,i = 8
當前執行緒號:pool-1-thread-2,i = 7
當前執行緒號:pool-1-thread-1,i = 9
可以看到,儘管分配10個任務給執行緒池,然而我們初始化5個固定執行緒的執行緒池,所以執行緒池執行的所在程序號不會超過5,而且可以看出在newFixedThreadPool中執行緒的排程是隨機的。
執行緒池多工計算
假如我們要使用執行緒池來執行相應計算,並且取得返回值要如何實現呢?
通常可以使用Future或者FutureTask以結合Callable來實現。
思路:
- 建立所需執行緒池,返回執行器[ExecutorService]executor
- 使用FutureTask構造方法傳入需要計算的Callable例項化建立FutureTask
- 呼叫執行器executor的方法executor.execute(futuretask);
- 建立
List<FutureTask>
將執行的futuretask新增到List中,方便後續取值。
示例:
下面模擬一個計算延遲操作,我們來看看多執行緒和單執行緒執行的效率對比
package com.dd.code.ThreadByCallable;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;
import java.util.concurrent.RejectedExecutionException;
public class MutilThreadTask {
// 執行緒(內部類)實現Callable介面
static class MyCallable implements Callable<Long> {
private long uid;
public MyCallable(long uid) {
super();
this.uid = uid;
}
@Override
public Long call() throws Exception {
return Work(uid);
}
public long Work(long uid) {
try {
//模擬查詢操作延遲
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
return uid * 2;
}
}
public static void main(String[] args) {
// 多執行緒累加list
List<Long> uidList = new ArrayList<Long>();
for(int i = 0; i < 100; i++){
uidList.add(10L);
}
long t1 = System.currentTimeMillis();
//使用單執行緒進行計算
System.out.println("累加結果:" + SumWithOneThread(uidList));
System.out.println("常規計算時間:"+(System.currentTimeMillis()-t1));
long t2 = System.currentTimeMillis();
//建立10個執行緒的執行緒池參與運算
System.out.println("累加結果:" + SumWithMutilThread(uidList,10));
System.out.println("執行緒池計算時間:"+(System.currentTimeMillis()-t2));
}
static Long SumWithOneThread(List<Long> uidList){
Long result = 0L;
for (Long l : uidList) {
result += DoubleIt(l);
}
return result;
}
static Long DoubleIt(Long l){
try {
//模擬延遲計算
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
return l*2;
}
static Long SumWithMutilThread(List<Long> uidList,int THREAD_NUM) {
// 建立執行緒池
ExecutorService executor = Executors.newFixedThreadPool(THREAD_NUM);
// 關於Future和Futuretask可以參考http://blog.csdn.net/zmx729618/article/details/51596414
// 建立任務列表
List<FutureTask<Long>> ftlist = new ArrayList<FutureTask<Long>>();
// MutilThreadTask mt = new MutilThreadTask();
// 逐個執行任務並且新增到任務列表
for (Long uid : uidList) {
/*
* 例項化內部類MyCallable方法
* 1.把內部類設為靜態.
* 2.通過外部類.例項化內部類,MyCallable callable = mt.new MyCallable(uid);
*/
MyCallable callable = new MyCallable(uid);
FutureTask<Long> futuretask = null;
try {
futuretask = new FutureTask<Long>(callable);
executor.execute(futuretask);
} catch (RejectedExecutionException e) {
//Re:處理進入執行緒池失敗的情況,也就是說,不僅獲取執行緒資源失敗,並且由於等待佇列已滿,甚至無法進入佇列直接 失敗
e.printStackTrace();
}
ftlist.add(futuretask);
}
// 遍歷任務列表,把完成任務取出來獲取資料
long totalResult = 0L; // 初始化計算資料量
for (FutureTask<Long> ft : ftlist) {
Long result = null;
try {
result = ft.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
if (result != null) {
totalResult += result;
}
}
// 關閉執行器
executor.shutdown();
return totalResult;
}
}
執行結果:
累加結果:2000
常規計算時間:10035
累加結果:2000
執行緒池計算時間:1020
效率對比
果然,使用執行緒池建立多個執行緒計算效率快了許多,但是使用多執行緒就一定快嗎?
我們稍作修改一下方法的引數,只給FutureTask分配5個任務[區別於之前的100個任務],並且將模擬計算時間修改為1毫秒[區別於之前的100毫秒],再看看執行結果。
累加結果:100
常規計算時間:6
累加結果:100
執行緒池計算時間:24
發現使用執行緒池的多執行緒計算反而更慢了,可以初步斷定,如果任務量不是很大,計算量不是很複雜的情況下,多執行緒並不是首先,相反執行緒池還需要耗費資源去維護池中的執行緒,所以考慮多執行緒的使用場景,還要權衡一下任務量。