如何從線程返回信息——輪詢、回調、Callable


  考慮有這樣一個LiftOff類:

/**
 * 類LiftOff.java的實現描述:顯示發射之前的倒計時
 * 
 * @author wql 2016年9月21日 下午1:46:46
 */
public class LiftOff implements Runnable {

    public LiftOff(){
        taskCount++;// 計數自增
    }

    private int        countDown = 3;        // 倒計時數字

    private static int taskCount = 0;

    private int        id        = taskCount;

    @Override
    public void run() {

        while (countDown >= 0) {
            System.out.println("線程編號" + id + "--倒計時" + countDown);
            countDown--;
            Thread.yield();
        }
    }
}

  以及一個發射主線程:

public class Launch {

    public static void main(String[] args) {
        LiftOff liftOff = new LiftOff();
        Thread t = new Thread(liftOff);
        t.start();
        System.out.println("發射!");
    }
}

  我們的本意是先顯示倒計時,然后顯示“發射!”,運行結果卻是

發射!
線程編號0--倒計時3
線程編號0--倒計時2
線程編號0--倒計時1
線程編號0--倒計時0

  因為main()函數也是一個線程,程序能否得到正確的結果依賴於線程的相對執行速度,而我們無法控制這一點。想要使LiftOff線程執行完畢后再繼續執行主線程,比較容易想到的辦法是使用輪詢

/**
 * 類LiftOff.java的實現描述:顯示發射之前的倒計時
 * 
 * @author wql 2016年9月21日 下午1:46:46
 */
public class LiftOff implements Runnable {

    public LiftOff(){
        taskCount++;// 計數自增
    }

    private int        countDown = 3;        // 倒計時數字

    private static int taskCount = 0;

    private int        id        = taskCount;
    
    private boolean isOver = false;

    @Override
    public void run() {

        while (countDown >= 0) {
            System.out.println("線程編號" + id + "--倒計時" + countDown);
            countDown--;
            if(countDown < 0){
                isOver = true;
            }
            Thread.yield();
        }
    }

    public boolean isOver() {
        return isOver;
    }
    
}

  我們添加了isOver變量,在倒計時結束時將isOver置為true,主函數中我們不斷地判斷isOver的狀態,就可以判斷LiftOff線程是否執行完畢:

public class Launch {

    public static void main(String[] args) {
        LiftOff liftOff = new LiftOff();
        Thread t = new Thread(liftOff);
        t.start();
        while (true) {
            if (liftOff.isOver()) {
                System.out.println("發射!");
                break;
            }
        }
    }
}

  執行main(),輸出:

線程編號0--倒計時3
線程編號0--倒計時2
線程編號0--倒計時1
線程編號0--倒計時0
發射!

  這個解決方案是可行的,它會以正確的順序給出正確的結果,但是不停地查詢不僅浪費性能,並且有可能會因主線程太忙於檢查工作的完成情況,以至於沒有給具體的工作線程留出時間,更好的方式是使用回調(callback),在線程完成時反過來調用其創建者,告訴其工作已結束:

public class LiftOff implements Runnable {
    
    private Launch launch;

    public LiftOff(Launch launch){
        taskCount++;// 計數自增
        this.launch = launch;
    }

    private int        countDown = 3;        // 倒計時數字

    private static int taskCount = 0;

    private int        id        = taskCount;
    
    @Override
    public void run() {

        while (countDown >= 0) {
            System.out.println("線程編號" + id + "--倒計時" + countDown);
            countDown--;
            if(countDown < 0){
                launch.callBack();
            }
            Thread.yield();
        }
    }
}

  主線程代碼:

public class Launch {
    
    public void callBack(){
        System.out.println("發射!");
    }
    
    public static void main(String[] args) {
        
        Launch launch = new Launch();
        LiftOff liftOff = new LiftOff(launch);
        
        Thread t = new Thread(liftOff);
        t.start(); 
    }
}

  運行結果:

線程編號0--倒計時3
線程編號0--倒計時2
線程編號0--倒計時1
線程編號0--倒計時0
發射!

  相比於輪詢機制,回調機制的第一個優點是不會浪費那么多的CPU性能,但更重要的優點是回調更靈活,可以處理涉及更多線程,對象和類的更復雜的情況。

  例如,如果有多個對象對線程的計算結果感興趣,那么線程可以保存一個要回調的對象列表,這些對計算結果感興趣的對象可以通過調用方法把自己添加到這個對象列表中完成注冊。當線程處理完畢時,線程將回調這些對計算結果感興趣的對象。我們可以定義一個新的接口,所有這些類都要實現這個新接口,這個新接口將聲明回調方法。這種機制有一個更一般的名字:觀察者(Observer)設計模式。

Callable

  java5引入了多線程編程的一個新方法,可以更容易地處理回調。任務可以實現Callable接口而不是Runnable接口,通過Executor提交任務並且會得到一個Future,之后可以向Future請求得到任務結果:

public class LiftOff implements Callable<String> {
    
    public LiftOff(){
        taskCount++;// 計數自增
    }

    private int        countDown = 3;        // 倒計時數字

    private static int taskCount = 0;

    private int        id        = taskCount;
    
    @Override
    public String call() throws Exception {
        while (countDown >= 0) {
            System.out.println("線程編號" + id + "--倒計時" + countDown);
            countDown--;
        }
        return "線程編號" + id + "--結束";
    }
}

  主函數:

public class Launch {
    
    public static void main(String[] args) {
        
        ExecutorService executor = Executors.newCachedThreadPool();
        Future<String> future = executor.submit(new LiftOff());
        try {
            String s = future.get();
            System.out.println(s);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
        System.out.println("發射!");
    }
}

  運行結果:

線程編號0--倒計時3
線程編號0--倒計時2
線程編號0--倒計時1
線程編號0--倒計時0
線程編號0--結束
發射!

  容易使用Executor提交多個任務:

public class Launch {

    public static void main(String[] args) {

        ExecutorService executor = Executors.newCachedThreadPool();
        List<Future<String>> results = new ArrayList<>();
        
        //多線程執行三個任務
        for (int i = 0; i < 3; i++) {
            Future<String> future = executor.submit(new LiftOff());
            results.add(future);
        }
        
        //獲得線程處理結果
        for (Future<String> result : results) {
            try {
                String s = result.get();
                System.out.println(s);
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }
        }
        
        //繼續主線程流程
        System.out.println("發射!");
    }
}

  結果:

線程編號0--倒計時3
線程編號0--倒計時2
線程編號0--倒計時1
線程編號0--倒計時0
線程編號2--倒計時3
線程編號2--倒計時2
線程編號1--倒計時3
線程編號1--倒計時2
線程編號1--倒計時1
線程編號1--倒計時0
線程編號2--倒計時1
線程編號2--倒計時0
線程編號0--結束
線程編號1--結束
線程編號2--結束
發射!

  可以看到,Future的get()方法,如果線程的結果已經准備就緒,會立即得到這個結果,如果還沒有准備好,輪詢線程會阻塞,直到結果准備就緒。

好處

  使用Callable,我們可以創建很多不同的線程,然后按照需要的順序得到我們想要的答案。另外如果有一個很耗時的計算問題,我們也可以把計算量分到多個線程中去處理,最后匯總每個線程的處理結果,從而節省時間。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM