Java中Future和FutureTask的示例詳解及使用

一、Future 接口

當 call()方法完成時,結果必須存儲在主線程已知的對象中,以便主線程可以知道該線程返回的結果。為此,可以使用 Future 對象。

將 Future 視為保存結果的對象–它可能暫時不保存結果,但將來會保存(一旦Callable 返回)。Future 基本上是主線程可以跟蹤進度以及其他線程的結果的一種方式。要實現此接口,必須重寫 5 種方法,這裡列出瞭重要的方法,如下:

public boolean isDone()

public boolean cancel(boolean mayInterruptIfRunning)

用於停止任務。如果尚未啟動,它將停止任務。如果已啟動,則僅在 mayInterrupt 為 true時才會中斷任務。

boolean isCancelled()

如果任務在正常結束之前被取消返回true

public V get() throws InterruptedException, ExecutionException

用於獲取任務的結果。如果任務完成,它將立即返回結果,否則將等待任務完成,然後返回結果。

public V get(long timeout, TimeUnit unit)
    throws InterruptedException, ExecutionException, TimeoutException

如果任務完成,則返回 true,否則返回 false。

Callable 與 Runnable 類似,因為它封裝瞭要在另一個線程上運行的任務,而 Future 用於存儲從另一個線程獲得的結果

實際上,Future 也可以與 Runnable 一起使用。要創建線程,需要 Runnable。為瞭獲得結果,需要 future。

二、FutureTask

介紹:當一個線程需要等待另一個線程把某個任務執行完後它才能繼續執行,此時可以使用FutureTask。假設有多個線程執行若幹任務,每個任務最多隻能被執行一次。當多個線程試圖同時執行同一個任務時,隻允許一個線程執行任務,其他線程需要等待這個任務執行完後才能繼續執行。

Java 庫具有具體的 FutureTask 類型,該類型實現 Runnable 和 Future,並方便地將兩種功能組合在一起。 可以通過為其構造函數提供 Callable 來創建FutureTask。然後,將 FutureTask 對象提供給 Thread 的構造函數以創建Thread 對象因此,間接地使用 Callable 創建線程

FutureTask狀態轉換

FutureTask有以下7種狀態:

FutureTask任務的運行狀態,最初為NEW。運行狀態僅在set、setException和cancel方法中轉換為終端狀態。在完成過程中,狀態可能呈現出瞬時值INTERRUPTING(僅在中斷運行程序以滿足**cancel(true)**的情況下)或者COMPLETING(在設置結果時)狀態時。從這些中間狀態到最終狀態的轉換使用成本更低的有序/延遲寫,因為值是統一的,需要進一步修改。

state:表示當前任務的運行狀態,FutureTask的所有方法都是圍繞state開展的,state聲明為volatile,保證瞭state的可見性,當對state進行修改時所有的線程都會看到

NEW:表示一個新的任務,初始狀態

COMPLETING:當任務被設置結果時,處於COMPLETING狀態,這是一個中間狀態。

NORMAL:表示任務正常結束。

EXCEPTIONAL:表示任務因異常而結束

CANCELLED:任務還未執行之前就調用瞭cancel(true)方法,任務處於CANCELLED

INTERRUPTING:當任務調用cancel(true)中斷程序時,任務處於INTERRUPTING狀態,這是一個中間狀態。

INTERRUPTED:任務調用cancel(true)中斷程序時會調用interrupt()方法中斷線程運行,任務狀態由INTERRUPTING轉變為INTERRUPTED

可能的狀態過渡:
1、NEW -> COMPLETING -> NORMAL:正常結束
2、NEW -> COMPLETING -> EXCEPTIONAL:異常結束
3、NEW -> CANCELLED:任務被取消
4、NEW -> INTERRUPTING -> INTERRUPTED:任務出現中斷

三、使用 Callable 和 Future

Runnable缺少的一項功能是,當線程終止時(即 run()完成時),我們無法使線程返回結果。

為瞭支持此功能,Java 中提供瞭 Callable 接口。不能直接替換 runnable,因為 Thread 類的構造方法根本沒有 Callable。

所以我們可以找一個中間人,也就是FutureTask。

案例

class MyThreadA implements Callable {
    @Override
    public Object call() throws Exception {
        System.out.println(Thread.currentThread().getName() + "在call方法裡");
        System.out.println(Thread.currentThread().getName() + "線程進入瞭 call方法,開始睡覺(進行瞭一些計算)");
        Thread.sleep(10000);
        System.out.println(Thread.currentThread().getName() + "睡醒瞭");
        return Thread.currentThread().getName() + "返回的:" + System.currentTimeMillis();
    }
}


public class demo1 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<Integer> futureTaskA = new FutureTask<>(new MyThreadA());

        FutureTask<String> futureTaskB = new FutureTask<>(()->{
            System.out.println(Thread.currentThread().getName() + "在call方法裡");
            return Thread.currentThread().getName() + "返回的:" + System.currentTimeMillis();
        });

        new Thread(futureTaskA,"線程A").start();
        new Thread(futureTaskB,"線程B").start();

        while (!futureTaskB.isDone()){  //isDone表示FutureTask的計算是否完成
            System.out.println("wait.......");
        }
        System.out.println(futureTaskA.get());
        System.out.println(futureTaskB.get());

        System.out.println(Thread.currentThread().getName() + "結束瞭");
    }
}

輸出結果:

由上圖兩個線程返回的時間差約等於10秒可以看出,當一個線程(線程B)需要等待(一直wait…)另一個線程(線程A)把某個任務(進行瞭一些計算)執行完後它才能繼續執行,此時可以使用FutureTask。不管futureTaskA.get()和futureTaskB.get()誰在前面,輸出結果一定是“線程B返回的:xxx”在“wait…”的後面。假設有多個線程執行若幹任務,每個任務最多隻能被執行一次。當多個線程試圖同時執行同一個任務時,隻允許一個線程執行任務,其他線程需要等待這個任務執行完後才能繼續執行。

四、小結(FutureTask核心原理)

FutureTask核心原理

在主線程中需要執行比較耗時的操作時,但又不想阻塞主線程時,可以把這些作業交給 Future 對象在後臺完成,當主線程將來需要時,就可以通過 Future對象獲得後臺作業的計算結果或者執行狀態。

• 一般 FutureTask 多用於耗時的計算,主線程可以在完成自己的任務後,再去獲取結果

• 僅在計算完成時才能檢索結果;如果計算尚未完成,則阻塞 get 方法。一旦計算完成,就不能再重新開始或取消計算。get 方法而獲取結果隻有在計算完成時獲取,否則會一直阻塞直到任務轉入完成狀態,然後會返回結果或者拋出異常。

• get隻計算一次,因此 get 方法放到最後

附:FutureTask在高並發環境下確保任務隻執行一次

網上有篇例子,但是中間講的不是很清楚。我重新梳理瞭一下。

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

package com.concurrency.chapter15;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @program: 錯誤示例
 *
 * @description: 在很多高並發的環境下,往往我們隻需要某些任務隻執行一次。
 * 這種使用情景FutureTask的特性恰能勝任。舉一個例子,假設有一個帶key的連接池,
 * 當key存在時,即直接返回key對應的對象;當key不存在時,則創建連接。對於這樣的應用場景,
 * 通常采用的方法為使用一個Map對象來存儲key和連接池對應的對應關系,典型的代碼如下
 * 在例子中,我們通過加鎖確保高並發環境下的線程安全,也確保瞭connection隻創建一次,然而卻犧牲瞭性能。
 *
 * @author: zhouzhixiang
 *
 * @create: 2019-05-14 20:22
 */
public class FutureTaskConnection1 {

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

    public static Connection getConnection(String key) {
        try {
            lock.lock();
            Connection connection = connectionPool.get(key);
            if (connection == null) {
                Connection newConnection = createConnection();
                connectionPool.put(key, newConnection);
                return newConnection;
            }
            return connection;
        } finally {
            lock.unlock();
        }
    }

    private static Connection createConnection() {
        try {
            return DriverManager.getConnection("");
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return null;
    }
}

總結

到此這篇關於Java中Future和FutureTask的文章就介紹到這瞭,更多相關Java Future和FutureTask使用內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: