3

我正在閱讀Java ForkJoin框架。在ForkJoinTask(例如RecursiveTask)的實施中,如果不直接呼叫invoke()還有什麼額外的好處,但是要實例化ForkJoinPool並呼叫pool.invoke(task)?當我們將這兩種方法稱爲invoke時究竟發生了什麼?ForkJoinPool.invoke()和ForkJoinTask.invoke()或compute()

從源頭上看,如果recursiveTask.invoke被調用,它將調用它的exec並最終以管理的線程池方式調用compute。因此,爲什麼我們有成語pool.invoke(task)更令人困惑。

我寫了一些簡單的代碼來測試性能差異,但我沒有看到任何。也許測試代碼是錯誤的?請看下圖:

public class MyForkJoinTask extends RecursiveAction { 

    private static int totalWorkInMillis = 20000; 
    protected static int sThreshold = 1000; 

    private int workInMillis; 


    public MyForkJoinTask(int work) { 
     this.workInMillis = work; 
    } 

    // Average pixels from source, write results into destination. 
    protected void computeDirectly() { 
     try { 

      ForkJoinTask<Object> objectForkJoinTask = new ForkJoinTask<>(); 
      Thread.sleep(workInMillis); 
     } catch (InterruptedException e) { 
      e.printStackTrace(); 
     } 
    } 

    @Override 
    protected void compute() { 
     if (workInMillis < sThreshold) { 
      computeDirectly(); 
      return; 
     } 

     int discountedWork = (int) (workInMillis * 0.9); 
     int split = discountedWork/2; 

     invokeAll(new MyForkJoinTask(split), 
       new MyForkJoinTask(split)); 
    } 

    public static void main(String[] args) throws Exception { 
     System.out.printf("Total work is %d in millis.%n", totalWorkInMillis); 
     System.out.printf("Threshold is %d in millis.%n", sThreshold); 

     int processors = Runtime.getRuntime().availableProcessors(); 
     System.out.println(Integer.toString(processors) + " processor" 
       + (processors != 1 ? "s are " : " is ") 
       + "available"); 

     MyForkJoinTask fb = new MyForkJoinTask(totalWorkInMillis); 

     ForkJoinPool pool = new ForkJoinPool(); 

     long startTime = System.currentTimeMillis(); 


     // These 2 seems no difference! 
     pool.invoke(fb); 
//  fb.compute(); 


     long endTime = System.currentTimeMillis(); 

     System.out.println("Took " + (endTime - startTime) + 
       " milliseconds."); 
    } 
} 

回答

4

RecursiveTask類的compute()方法僅僅是包含任務代碼的抽象方法。它不使用池中的新線程,如果您正常調用它,則不會在池管理線程中運行。

叉式連接池中的invoke方法向池提交一個任務,然後該池開始在單獨的線程上運行,調用該線程上的compute方法,然後等待結果。

你可以在java doc的RecursiveTask和ForkJoinPool中看到這個。 invoke()方法實際上執行任務,而compute()方法只是封裝計算。

protected abstract V compute() 

該任務執行的主要計算。

而且ForkJoinPool

public <T> T invoke(ForkJoinTask<T> task) 

執行給定的任務,完成後返回其結果。 ...所以與計算方法

,你在做什麼是運行在第一次調用compute外叉的加入池。您可以通過在compute方法內添加日誌行來測試這一點。

System.out.println(this.inForkJoinPool()); 

您還可以檢查它在同一個線程通過登錄線程ID

System.out.println(Thread.currentThread().getId()); 

一旦你調用invokeAll運行,包括在調用子任務,然後在池中運行。請注意,它不一定在您撥打compute()之前創建的池中運行。你可以註釋掉你的new ForkJoinPool()代碼,它仍然會運行。有趣的是,java 7 doc說invokeAll()方法會拋出一個異常,如果它在一個池託管線程之外被調用,但java 8 doc不會。我沒有在java 7中介紹過你的測試(只有8個)。但很有可能,在java 7中直接調用compute()時,您的代碼會引發異常。

兩個結果都返回相同時間的原因是毫秒不太不夠準確記錄開始在池進行管理,線程第一個線程,或者只是運行在現有的線程中的第一compute調用的差異。

的方式OCA/OCP學習指南由Sierra和貝茨建議您使用叉join框架是調用invoke()從池中。它清楚地說明了您正在使用的是哪個池,這也意味着您可以將多個任務提交到同一個池中,這樣可以節省每次重新創建新池的開銷。從邏輯上講,將所有任務計算保留在池管理線程內(或者至少我認爲是這樣)也更清晰。

+0

感謝您的回答。但'RecursiveTask'也有一個繼承的'invoke'方法,通常在其主'compute'方法內調用'invoke'。那是什麼呢?還有,怎麼沒有性能差異? – Boyang

+0

所以我深入瞭解源代碼。如果調用'recursiveTask.invoke',它將以託管線程池的方式調用它的'exec'並最終'compute'。在這種情況下,我更加困惑於成語'pool.invoke(task)'。爲什麼? – Boyang

+0

請參閱我的新的編輯 – Boyang