FutureTask的用法及兩種常用的使用場景

搜雲庫技術團隊發表於2017-12-14

FutureTask可用於非同步獲取執行結果或取消執行任務的場景。通過傳入Runnable或者Callable的任務給FutureTask,直接呼叫其run方法或者放入執行緒池執行,之後可以在外部通過FutureTask的get方法非同步獲取執行結果,因此,FutureTask非常適合用於耗時的計算,主執行緒可以在完成自己的任務後,再去獲取結果。另外,FutureTask還可以確保即使呼叫了多次run方法,它都只會執行一次Runnable或者Callable任務,或者通過cancel取消FutureTask的執行等。

1.執行多工計算

FutureTask執行多工計算的使用場景

利用FutureTask和ExecutorService,可以用多執行緒的方式提交計算任務,主執行緒繼續執行其他任務,當主執行緒需要子執行緒的計算結果時,在非同步獲取子執行緒的執行結果。

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;

public class FutureTaskForMultiCompute {

    public static void main(String[] args) {

        FutureTaskForMultiCompute inst = new FutureTaskForMultiCompute();
        // 建立任務集合
        List<FutureTask<Integer>> taskList = new ArrayList<FutureTask<Integer>>();
        // 建立執行緒池
        ExecutorService exec = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 10; i++) {
            // 傳入Callable物件建立FutureTask物件
            FutureTask<Integer> ft = new FutureTask<Integer>(inst.new ComputeTask(i, "" + i));
            taskList.add(ft);
            // 提交給執行緒池執行任務,也可以通過exec.invokeAll(taskList)一次性提交所有任務;
            exec.submit(ft);
        }

        System.out.println("所有計算任務提交完畢, 主執行緒接著幹其他事情!");

        // 開始統計各計算執行緒計算結果
        Integer totalResult = 0;
        for (FutureTask<Integer> ft : taskList) {
            try {
                //FutureTask的get方法會自動阻塞,直到獲取計算結果為止
                totalResult = totalResult + ft.get();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }

        // 關閉執行緒池
        exec.shutdown();
        System.out.println("多工計算後的總結果是:" + totalResult);

    }

    private class ComputeTask implements Callable<Integer> {

        private Integer result = 0;
        private String taskName = "";

        public ComputeTask(Integer iniResult, String taskName) {
            result = iniResult;
            this.taskName = taskName;
            System.out.println("生成子執行緒計算任務: " + taskName);
        }

        public String getTaskName() {
            return this.taskName;
        }

        @Override
        public Integer call() throws Exception {
            // TODO Auto-generated method stub

            for (int i = 0; i < 100; i++) {
                result = +i;
            }
            // 休眠5秒鐘,觀察主執行緒行為,預期的結果是主執行緒會繼續執行,到要取得FutureTask的結果是等待直至完成。
            Thread.sleep(5000);
            System.out.println("子執行緒計算任務: " + taskName + " 執行完成!");
            return result;
        }
    }
}
複製程式碼
生成子執行緒計算任務: 0
生成子執行緒計算任務: 1
生成子執行緒計算任務: 2
生成子執行緒計算任務: 3
生成子執行緒計算任務: 4
生成子執行緒計算任務: 5
生成子執行緒計算任務: 6
生成子執行緒計算任務: 7
生成子執行緒計算任務: 8
生成子執行緒計算任務: 9
所有計算任務提交完畢, 主執行緒接著幹其他事情!
子執行緒計算任務: 0 執行完成!
子執行緒計算任務: 2 執行完成!
子執行緒計算任務: 3 執行完成!
子執行緒計算任務: 4 執行完成!
子執行緒計算任務: 1 執行完成!
子執行緒計算任務: 8 執行完成!
子執行緒計算任務: 7 執行完成!
子執行緒計算任務: 6 執行完成!
子執行緒計算任務: 9 執行完成!
子執行緒計算任務: 5 執行完成!
多工計算後的總結果是:990
複製程式碼

2.高併發環境下

FutureTask在高併發環境下確保任務只執行一次

在很多高併發的環境下,往往我們只需要某些任務只執行一次。這種使用情景FutureTask的特性恰能勝任。舉一個例子,假設有一個帶key的連線池,當key存在時,即直接返回key對應的物件;當key不存在時,則建立連線。對於這樣的應用場景,通常採用的方法為使用一個Map物件來儲存key和連線池對應的對應關係,典型的程式碼如下面所示:

  private Map<String, Connection> connectionPool = new HashMap<String, Connection>();
    private ReentrantLock lock = new ReentrantLock();

    public Connection getConnection(String key) {
        try {
            lock.lock();
            if (connectionPool.containsKey(key)) {
                return connectionPool.get(key);
            } else {
                //建立 Connection  
                Connection conn = createConnection();
                connectionPool.put(key, conn);
                return conn;
            }
        } finally {
            lock.unlock();
        }
    }

    //建立Connection  
    private Connection createConnection() {
        return null;
    }

複製程式碼

在上面的例子中,我們通過加鎖確保高併發環境下的執行緒安全,也確保了connection只建立一次,然而確犧牲了效能。改用ConcurrentHash的情況下,幾乎可以避免加鎖的操作,效能大大提高,但是在高併發的情況下有可能出現Connection被建立多次的現象。這時最需要解決的問題就是當key不存在時,建立Connection的動作能放在connectionPool之後執行,這正是FutureTask發揮作用的時機,基於ConcurrentHashMap和FutureTask的改造程式碼如下:

  private ConcurrentHashMap<String, FutureTask<Connection>> connectionPool = new ConcurrentHashMap<String, FutureTask<Connection>>();

    public Connection getConnection(String key) throws Exception {
        FutureTask<Connection> connectionTask = connectionPool.get(key);
        if (connectionTask != null) {
            return connectionTask.get();
        } else {
            Callable<Connection> callable = new Callable<Connection>() {
                @Override
                public Connection call() throws Exception {
                    // TODO Auto-generated method stub  
                    return createConnection();
                }
            };
            FutureTask<Connection> newTask = new FutureTask<Connection>(callable);
            connectionTask = connectionPool.putIfAbsent(key, newTask);
            if (connectionTask == null) {
                connectionTask = newTask;
                connectionTask.run();
            }
            return connectionTask.get();
        }
    }

    //建立Connection  
    private Connection createConnection() {
        return null;
    }
複製程式碼

經過這樣的改造,可以避免由於併發帶來的多次建立連線及鎖的出現。

Contact

  • 作者:鵬磊
  • 出處:www.ymq.io
  • Email:admin@souyunku.com
  • 版權歸作者所有,轉載請註明出處
  • Wechat:關注公眾號,搜雲庫,專注於開發技術的研究與知識分享

關注公眾號-搜雲庫
搜雲庫

相關文章