程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> Java並發基礎實踐:退出任務II

Java並發基礎實踐:退出任務II

編輯:關於JAVA

在本系列的上一篇中所述的退出並發任務的方式都是基於JDK 5之前的API,本文將介紹使用由JDK 5引入的並發工具包中的API來退出任務。(2013.10.08最後更新)

在本系列的前一篇中講述了三種退出並發任務的方式--停止線程;可取消的任務;中斷,但都是基於JDK 5之前的API。本篇將介紹由JDK 5引入的java.concurrent包中的Future來取消任務的執行。

1. Future模式

Future是並發編程中的一種常見設計模式,它相當於是Proxy模式與Thread-Per-Message模式的結合。即,每次都創建一個單獨的線程去執行一個耗時的任務,並且創建一個Future對象去持有實際的任務對象,在將來需要的時候再去獲取實際任務的執行結果。
依然先創建一個用於掃描文件的任務FileScannerTask,如代碼清單1所示,

清單1
public class FileScannerTask implements Runnable {
    
    private File root = null;
    
    private ArrayList<String> filePaths = new ArrayList<String>();
    
    public FileScannerTask(File root) {
        if (root == null || !root.exists() || !root.isDirectory()) {
            throw new IllegalArgumentException("root must be directory");
        }
    
        this.root = root;
    }
    
    @Override
    public void run() {
        travleFiles(root);
    }
    
    private void travleFiles(File parent) {
        String filePath = parent.getAbsolutePath();
        filePaths.add(filePath);
    
        if (parent.isDirectory()) {
            File[] children = parent.listFiles();
            if (children != null) {
                for (File child : children) {
                    travleFiles(child);
                }
            }
        }
    }
    
    public List<String> getFilePaths() {
        return (List<String>) filePaths.clone();
    }
}

此處的文件掃描任務,提供了一個getFilePaths()方法以允許隨時都可以取出當前已掃描過的文件的路徑(相當於一個任務快照)。然後,創建一個針對該任務的Future類,如代碼清單2所示,

清單2
public class FileScannerFuture {
    
    private FileScannerTask task = null;
    
    public FileScannerFuture(FileScannerTask task) {
        new Thread(task).start();
        this.task = task;
    }
    
    public List<String> getResult() {
        return task.getFilePaths();
    }
}

FileScannerFuture持有FileScannerTask的引用,並創建一個獨立的線程來執行該任務。在任務的執行過程中,應用程序可以在"未來"的某個時刻去獲取一個任務的快照,如代碼清單3所示,

清單3
public static void main(String[] args) throws Exception {
    FileScannerFuture future = new FileScannerFuture(new FileScannerTask(new File("C:")));
    
    TimeUnit.SECONDS.sleep(1);
    List<String> filePaths1 = future.getResult();
    System.out.println(filePaths1.size());
    
    TimeUnit.SECONDS.sleep(1);
    List<String> filePaths2 = future.getResult();
    System.out.println(filePaths2.size());
}

2. 使用並發工具包中的Future實現

前面所展示的Future實現十分的簡陋,沒有實際應用的意義。使用FileScannerFuture,應用程序在獲取filePaths時,無法得知其獲取的是否為最終結果,即無法判斷FileScannerTask是否已經完成。而且,也不能在必要時停止FileScannerTask的執行。毫無疑問,由JDK 5引入的並發工具包肯定會提供此類實用工具,如FutureTask。為了使用並發工具包中的Future,需要修改前述的FileScannerTask實現,讓其實現Callable接口,如代碼清單4所示,

清單4
public class FileScannerTask implements Callable<List<String>> {
    
    private File root = null;
    
    private List<String> filePaths = new ArrayList<String>();
    
    public FileScannerTask(File root) {
        if (root == null || !root.exists() || !root.isDirectory()) {
            throw new IllegalArgumentException("root must be directory");
        }
    
        this.root = root;
    }
    
    @Override
    public List<String> call() {
        travleFiles(root);
        return filePaths;
    }
    
    private void travleFiles(File parent) {
        String filePath = parent.getAbsolutePath();
        filePaths.add(filePath);
    
        if (parent.isDirectory()) {
            File[] children = parent.listFiles();
            if (children != null) {
                for (File child : children) {
                    travleFiles(child);
                }
            }
        }
    }
    
    public List<String> getFilePaths() {
        return (List<String>) filePaths.clone();
    }
}

應用程序也要相應的修改成如代碼清單5所示,使用ExecutorService來提交任務,並創建一個Future/FutureTask實例。

清單5
public static void main(String[] args) {
    ExecutorService executorService = Executors.newCachedThreadPool();
    Future<List<String>> future = executorService.submit(new FileScannerTask(new File("C:")));
    
    try {
        List<String> filePaths = future.get();
        System.out.println(filePaths.size());
    } catch (InterruptedException e) {
        e.printStackTrace();
    } catch (ExecutionException e) {
        e.printStackTrace();
    }
    
    executorService.shutdown();
}

查看本欄目

此處就是調用Future.get()方法來獲取任務的執行結果,如果任務沒有執行完畢,那麼該方法將會被阻塞。該Future實現的好處就是,正常情況下,只有在任務執行完畢之後才能獲取其結果,以保證該結果是最終執行結果。

3. 使用Future取消任務

Future除了定義有可獲取執行結果的get方法(get()以及get(long timeout, TimeUnit unit)),還定義了三個方法:cancel(),isCancelled()以及isDone(),用於取消任務,以及判定任務是否已被取消、已執行完畢。如代碼清單6所示,

清單6
public interface Future<V> {
    
    boolean cancel(boolean mayInterruptIfRunning);
    boolean isCancelled();
    boolean isDone();
        
}

其中,cancel()方法中的boolean參數若為true,表示在取消該任務時,若執行該任務的線程仍在運行中,則對其進行中斷。如代碼清單7所示,若任務執行超時了,那麼就取消它。

清單7
public static void main(String[] args) {
    ExecutorService executorService = Executors.newCachedThreadPool();
    Future<List<String>> future = executorService.submit(new FileScannerTask(new File("C:")));
    
    try {
        List<String> filePaths = future.get(1, TimeUnit.SECONDS);
        System.out.println(filePaths.size());
    } catch (InterruptedException e) {
        e.printStackTrace();
    } catch (ExecutionException e) {
        e.printStackTrace();
    } catch (TimeoutException e) {
        e.printStackTrace();
    } finally {
        future.cancel(true);
    }
    
    executorService.shutdown();
}

在實際應用中,取消任務的原由肯定不僅僅只是超時這麼簡單,還可能是由於接受到了用戶的指令。此時,則可能會從另一個獨立線程去取消該任務。除了取消任務之外,有時還需要取出任務中已經生成的部分結果。但為了能夠響應任務的退出,首先需要修改FileScannerTask,使得當任務被取消(中斷)時,任務能夠真正的快速停止並返回,如代碼清單8所示,

清單8
public class FileScannerTask implements Callable<List<String>> {
    
        
    
    private void travleFiles(File parent) {
        if (Thread.currentThread().isInterrupted()) {
            return;
        }
    
        String filePath = parent.getAbsolutePath();
        filePaths.add(filePath);
    
        if (parent.isDirectory()) {
            File[] children = parent.listFiles();
            if (children != null) {
                for (File child : children) {
                    travleFiles(child);
                }
            }
        }
    }
    
        
}

相應地修改應用程序的代碼,如代碼清單9所示,

清單9
public static void main(String[] args) {
    ExecutorService executorService = Executors.newCachedThreadPool();
    FileScannerTask task = new FileScannerTask(new File("C:"));
    final Future<List<String>> future = executorService.submit(task);
        
    new Thread(new Runnable() {
            
        @Override
        public void run() {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            future.cancel(true);
        }
    }).start();
        
    try {
        List<String> filePaths = future.get();
        System.out.println(filePaths.size());
    } catch (InterruptedException e) {
        e.printStackTrace();
    } catch (ExecutionException e) {
        e.printStackTrace();
    } catch (CancellationException e) {
        List<String> filePaths = task.getFilePaths();
        System.out.println("Partly result: " + filePaths.size());
    }
        
    executorService.shutdown();
}

由上可知,此處使用Future.cancel(true)的本質依然是利用了線程的中斷機制。

4. 小結

使用Future可以在任務啟動之後的特定時機再去獲取任務的執行結果。由JDK 5引入的並發工具包中提供的Future實現不僅可以獲取任務的執行結果,還可以用於取消任務的執行。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved