Android併發程式設計之白話文詳解Future,FutureTask和Callable
從最簡單的說起Thread和Runnable
說到併發程式設計,就一定是多個執行緒併發執行任務。那麼併發程式設計的基礎是什麼呢?沒錯那就是Thread了。一個Thread可以執行一個Runnable型別的物件。那麼Runnable是什麼呢?其實Runnable是一個介面,他只定義了一個方法run(),這個run()方法裡就是我們要執行的任務,並且是要被Thread呼叫的。因此,一個Runnable就可以理解為一個要被執行的任務,而Thread就是一個執行任務的工人!
接下來我們用一個例子來實踐一下,我們有一個任務(Runnable),這個任務就是來計算1+2+3+…+10,然後我們叫來一個工人(Thread)來執行這個任務。
public class ThreadDemo {
public static void main(String[] args) {
Thread worker = new Thread(new CountRunnable());
worker.start();
}
public static class CountRunnable implements Runnable{
private int sum;
@Override
public void run() {
for (int i=1 ; i<11 ; i++){
sum = sum+i;
}
System.out.println("sum="+sum);
}
}
}
這裡我們呼叫了Thread的start()方法,相當於通知我們的工人去幹活,然後工人Thread去呼叫任務Runnable的run()方法去幹活。
如果我們覺得一個工人不夠用,那麼我們可以多叫來幾個工人,讓他們一起來工作,這就是併發程式設計了!
public class ThreadDemo {
public static void main(String[] args) throws Exception{
List<Thread> threads = new ArrayList<Thread>();
for(int i=1 ; i<101 ; i++){
Thread thread = new Thread(new CountRunnable(),"Thread"+i);
threads.add(thread);
thread.start();
}
for(Thread t : threads){
t.join();
}
System.out.println("所有執行緒執行完畢!");
}
public static class CountRunnable implements Runnable{
private int sum;
@Override
public void run() {
for(int i=1 ; i<11 ; i++){
sum = sum+i;
}
System.out.println(Thread.currentThread().getName()+"執行完畢 sum="+sum);
}
}
}
我們看到,每個sum都等於55,不是聽說多執行緒併發執行會帶來執行緒不安全的問題嗎?其實這裡我們是給每個工人分配一個只屬於自己的任務,每個工人幹自己的活,所以並不會影響到其他的人
Thread thread = new Thread(new CountRunnable(),"Thread"+i);
那麼什麼情況下會出現執行緒併發的問題的?我們要做的就是把一個任務同時分配給100名工人,那麼就會出現執行緒不安全的問題
public class ThreadDemo {
public static void main(String[] args) throws Exception{
List<Thread> threads = new ArrayList<Thread>();
//注意這裡,我們在外面new出一個任務來,讓100個執行緒都來執行這個任務
CountRunnable work = new CountRunnable();
for(int i=1 ; i<101; i++){
Thread thread = new Thread(work,"Thread"+i);
threads.add(thread);
}
for(Thread t : threads){
t.start();
}
for(Thread t : threads){
t.join();
}
System.out.println("所有執行緒執行完畢");
}
public static class CountRunnable implements Runnable{
private int sum;
@Override
public void run() {
for(int i=1 ; i<11 ; i++){
try {
//在這裡我們讓每個工人每進行一次加法運算後就休息1ms,這樣會使得結果明顯
TimeUnit.MILLISECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
sum = sum+i;
}
System.out.println(Thread.currentThread().getName()+"執行完畢 sum="+sum);
}
}
}
我們看到的結果簡直不堪入目,正確結果應該是5500對吧,這時因為我們有100名工人去幹一件任務,他們都操作的是同一個變數,因此多執行緒修改共享變數會出現問題,至於原理是什麼,大家可以參考我的另一篇文章Java併發程式設計之圖文解析volatile關鍵字來了解一下Java的記憶體模型(JMM)。
這篇文章的題目不是叫Future,FutureTask和Callable嗎?我怎麼連他們的影子都還沒有見到?大家先彆著急,還是和我一起慢慢深入,這樣才能真正理解他們存在的道理。
最簡單的方法出現了問題,執行緒池來解決
現在我們可以叫來這100名工人來為我們幹活了,可是這樣有個問題,這100名工人不是說找就找的,首先你得去發招聘啟事,接著再去面試,再去培訓等等,非常的費時費力,所以我們應該找到一個外包公司,比如我們需要100名工人,我們直接就到外包公司去借100名工人,直接來幹活,這樣就省了不少的力氣了,這個外包公司就是執行緒池了。關於執行緒池的介紹,有一篇寫的非常詳細的部落格Android效能優化之使用執行緒池處理非同步任務,既然已經有人把它的理論總結的很清晰透徹了,我就不再重複去介紹一遍了,如果大家有對執行緒池的基礎還不瞭解的話,推薦看看這篇文章。下面我就來說說執行緒池的使用,慢慢引出Future和Callable。
我們在使用執行緒池的時候,可以把一個任務(Runnable)交給執行緒池,呼叫執行緒池的execute(runnable)來執行
public class ExecutorDemo {
public static void main(String[] args) {
ExecutorService es = Executors.newSingleThreadExecutor();
CountRunnable work = new CountRunnable();
es.execute(work);
es.shutdown();
System.out.println("任務結束"+es.isShutdown());
}
public static class CountRunnable implements Runnable{
private int sum;
@Override
public void run() {
for(int i=1 ; i<11 ; i++){
sum+=i;
}
System.out.println("sum="+sum);
}
}
}
不知道大家有沒有發現一個問題,我們執行Runnable任務,他的run()方法是沒有返回值的,那如果我們想要執行完一個任務,並且能夠拿到一個返回值結果,那麼應該怎麼做呢?
Future登場
噹噹噹!沒錯!主角就要登場了!首先介紹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是一個介面,他提供給了我們方法來檢測當前的任務是否已經結束,還可以等待任務結束並且拿到一個結果,通過呼叫Future的get()方法可以當任務結束後返回一個結果值,如果工作沒有結束,則會阻塞當前執行緒,直到任務執行完畢,我們可以通過呼叫cancel()方法來停止一個任務,如果任務已經停止,則cancel()方法會返回true;如果任務已經完成或者已經停止了或者這個任務無法停止,則cancel()會返回一個false。當一個任務被成功停止後,他無法再次執行。isDone()和isCancel()方法可以判斷當前工作是否完成和是否取消。
簡單介紹一番,我們發現原來那些工人,只會去執行工作,做完工作之後也不給我們反饋資訊,並且我們也不知道他們何時能完工,更不能打斷他們的工作,這種工人的弊端就顯現出來了。
現在我們有了更高階的工人,這些工人只能是從外包公司來借(利用執行緒池),當這些工人幹完活之後,他們會給我們返回執行的結果,而且我們還可以暫停他們的工作。
我們看到執行緒池還有一個方法可以執行一個任務,那就是submit()方法
public Future<?> submit(Runnable task) {
return e.submit(task);
}
我們看到他會返回一個Future物件,這個Future物件的泛型裡還用的是一個問號“?”,問號就是說我們不知道要返回的物件是什麼型別,那麼就返回一個null好了,因為我們執行的是一個Runnable物件,Runnable是沒有返回值的,所以這裡用一個問號,說明沒有返回值,那麼就返回一個null好了。
public class ExecutorDemo {
public static void main(String[] args) {
ExecutorService es = Executors.newSingleThreadExecutor();
CountRunnable work = new CountRunnable();
Future<?> future = es.submit(work);
System.out.println("任務開始於"+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
for(int i=0 ; i<10 ; i++){
try {
TimeUnit.SECONDS.sleep(1);
System.out.println("主執行緒"+Thread.currentThread().getName()+"仍然可以執行");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
Object object = future.get();
System.out.println("任務結束於"+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())+" result="+object);
} catch (Exception e) {
e.printStackTrace();
}
es.shutdown();
System.out.println("關閉執行緒池"+es.isShutdown());
}
public static class CountRunnable implements Runnable{
private int sum;
@Override
public void run() {
for(int i=1 ; i<11 ; i++){
try {
TimeUnit.MILLISECONDS.sleep(1000);
sum+=i;
System.out.println("工作執行緒"+Thread.currentThread().getName()+"正在執行 sum="+sum);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
我們看到Future有一個get()方法,這個方法是一個阻塞方法,我們呼叫submit()執行一個任務的時候,會執行Runnable中的run()方法,當run()方法沒有執行完的時候,這個工人就會歇著了,直到run()方法執行結束後,工人就會立即將結果取回並且交給我們。我們看到返回的result=null。那既然返回null的話還有什麼意義呢??彆著急,那就要用到Callable介面了
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介面和Runnable介面很像,也是隻有一個方法,不過這個call()方法是有返回值的,這個返回值是一個泛型,也就是說我們可以根據我們的需求來指定我們要返回的result的型別
public class CallableDemo {
public static void main(String[] args) throws Exception{
ExecutorService es = Executors.newSingleThreadExecutor();
Future<Number> future = es.submit(new CountCallable());
System.out.println("任務開始於"+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
Number number = future.get();
System.out.println("任務結束於"+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
if(future.isDone()){
System.out.println("任務執行完畢 result="+number.num);
es.shutdown();
}
}
public static class CountCallable implements Callable<Number>{
@Override
public Number call() throws Exception {
Number number = new Number();
TimeUnit.SECONDS.sleep(2);
number.setNum(10);
return number;
}
}
static class Number{
private int num;
private int getNum(){
return num;
}
private void setNum(int num){
this.num = num;
}
}
}
我們建立我們的任務(Callable)的時候,傳入了一個Number類的泛型,那麼在call()方法中就會返回這個Number型別的物件,最後在Future的get()方法中就會返回我們的Number型別的結果。
然而Future不僅僅可以獲得一個結果,他還可以被取消,我們通過呼叫future的cancel()方法,可以取消一個Future的執行
public class CallableDemo {
public static void main(String[] args) throws Exception{
ExecutorService es = Executors.newSingleThreadExecutor();
Future<Number> future = es.submit(new CountCallable());
System.out.println("任務開始於"+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
future.cancel(true);
if (future.isCancelled()) {
System.out.println("任務被取消於"+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
es.shutdownNow();
}else{
Number number = future.get();
System.out.println("任務結束於"+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
if(future.isDone()){
System.out.println("任務執行完畢 result="+number.num);
es.shutdown();
}
}
}
public static class CountCallable implements Callable<Number>{
@Override
public Number call() throws Exception {
Number number = new Number();
TimeUnit.SECONDS.sleep(5);
number.setNum(10);
return number;
}
}
static class Number{
private int num;
private int getNum(){
return num;
}
private void setNum(int num){
this.num = num;
}
}
}
FutureTask登場
說完了Future和Callable,我們再來說最後一個FutureTask,Future是一個介面,他的唯一實現類就是FutureTask,其實FutureTask的一個很好地特點是他有一個回撥函式done()方法,當一個任務執行結束後,會回撥這個done()方法,我們可以在done()方法中呼叫FutureTask的get()方法來獲得計算的結果。為什麼我們要在done()方法中去呼叫get()方法呢? 這是有原因的,我在Android開發中,如果我在主執行緒去呼叫futureTask.get()方法時,會阻塞我的UI執行緒,如果在done()方法裡呼叫get(),則不會阻塞我們的UI執行緒。
public class FutureTask<V> implements RunnableFuture<V> {
}
我們來看看FutureTask實現了RunnableFuture介面
public interface RunnableFuture<V> extends Runnable, Future<V> {
/**
* Sets this Future to the result of its computation
* unless it has been cancelled.
*/
void run();
}
Runnable介面又實現了Runnable和Future介面,所以說FutureTask可以交給Executor執行,也可以由呼叫執行緒直接執行FutureTask.run()方法。FutureTask的run()方法中又會呼叫Callable的call()方法
public void run() {
if (state != NEW ||
!UNSAFE.compareAndSwapObject(this, runnerOffset,
null, Thread.currentThread()))
return;
try {
Callable<V> c = callable;
if (c != null && state == NEW) {
V result;
boolean ran;
try {
result = c.call();
ran = true;
} catch (Throwable ex) {
result = null;
ran = false;
setException(ex);
}
if (ran)
set(result);
}
} finally {
// runner must be non-null until state is settled to
// prevent concurrent calls to run()
runner = null;
// state must be re-read after nulling runner to prevent
// leaked interrupts
int s = state;
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
}
所以一個FutureTask實際上執行的是一個Callable型別例項的call()方法,call()方法才是我們的最終任務。其實Android中的AsyncTask內部也是使用的FutureTask,我們寫一個小的例子來模仿AsyncTask的可以停止的功能
public class MainActivity extends AppCompatActivity {
FutureTask<Number> futureTask;
CountCallable countCallable;
ExecutorService es;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
countCallable = new CountCallable();
futureTask = new FutureTask<Number>(countCallable){
@Override
protected void done() {
try {
Number number = futureTask.get();
Log.i("zhangqi", "任務結束於" + new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").format(new Date()) + " result=" + number.getNum());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
} catch (CancellationException e) {
Log.i("zhangqi", "任務被取消於" + new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").format(new Date()));
}
}
};
es = Executors.newFixedThreadPool(2);
es.execute(futureTask);
Log.i("zhangqi", "任務被開始於" + new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").format(new Date()));
}
public void cancel(View view) {
futureTask.cancel(true);
}
public static class CountCallable implements Callable<Number> {
@Override
public Number call() throws Exception {
Number number = new Number();
Log.i("zhangqi","執行在"+Thread.currentThread().getName());
Thread.sleep(5000);
number.setNum(10);
return number;
}
}
static class Number {
private int num;
private int getNum() {
return num;
}
private void setNum(int num) {
this.num = num;
}
}
}
我們寫了一個CountCallable類,在call()方法裡是我們要執行的任務,最終我們的任務要返回一個Number型別的物件,我們在call()方法中首先會讓執行緒睡眠5秒鐘,然後new出一個Number物件並且給他賦值為10.
接著我們new一個FutureTask並且把我們的CountCallable物件傳入進去,FutureTask的泛型就是我們要返回的結果的型別,並且我們要重寫FutureTask的done()方法,這個方法會在任務結束後自動執行,在done()方法中我們呼叫get()方法獲得執行的結果
現在我們執行cancel方法,來結束這個FutureTask任務
在任務執行2秒的時候,我點選了cancel按鈕,執行了FutureTask的cancel方法,當我們執行了cancel()方法後,FutureTask的get()方法會丟擲CancellationException異常,我們捕捉這個異常,然後在這裡來處理一些後事 =-=!
相信大家都已經理解了為什麼Java要提供給我們Future,FutureTask和Callable了,他們其實是併發程式設計中更高階的應用,我們應該理解他們並且正確的使用它們。