出處: Java回調實現異步
在正常的業務中使用同步線程,如果服務器每處理一個請求,就創建一個線程的話,會對服務器的資源造成浪費。因為這些線程可能會浪費時間在等待網絡傳輸,等待數據庫連接等其他事情上,真正處理業務邏輯的時間很短很短,但是其他線程在線程池滿了之后又會阻塞,等待前面的線程處理完成。而且,會出現一個奇怪的現象,客戶端的請求被阻塞,但是cpu的資源使用卻很低,大部分線程都浪費在處理其他事情上了。所以,這就導致服務器並發量不高。
而異步,則可以解決這個問題。
我們可以把需要用到cpu的業務處理使用異步來實現,這樣其他請求就不會被阻塞,而且cpu會保持比較高的使用率。
今天就學習了使用回調來實現異步的方法。我們設想一個情景,A是處理業務的一個步驟,A需要解決一個問題,這時候A可以問B,讓B來告訴A答案,這期間,A可以繼續做自己的事情,而不用因為B做的事而阻塞。於是,我們想到給B設置一個線程,讓B去處理耗時的操作,然后處理完之后把結果告訴A。所以這個問題的要點就在於B處理完之后如何把結果告訴A。我們可以直接在A中寫一個方法對B處理完的結果進行處理,然后B處理完之后調用A這個方法。這樣A調用B去處理過程,B調用A的C方法去處理結果就叫做回調。
package CallBack; public interface CallBack { /* *A處理結果的方法,為什么要寫這個接口呢? *因為可能不止A需要用到B的處理過程,如果很多地方需要用到B * 那么傳入B的方法就不可能只傳A類,所以要定義一個接口, * 傳入B的處理方法的參數就是這個接口對象 * */ public void solve(String result); }
package CallBack; public class A implements CallBack { private B b; public A(B b){ this.b=b; } //A需要解決一個問題,所以他把問題交給B處理,B單獨創建一個線程,不影響A的運行 public void ask(final String question){ System.out.println("A問了B一個問題"); new Thread(()->{ //B想要幫A處理東西,就必須知道誰讓自己處理的,所以要傳入a,也要知道a想處理什么,所以要傳入question b.executeMessage(A.this,question); }).start(); //A把要處理的事情交給b之后,就可以自己去玩耍了,或者去處理其他事情 play(); } public void play(){ System.out.println("我要逛街去了"); } //A拿到了B處理完成的結果,可以進行一些操作,比如把結果輸出 @Override public void solve(String result) { System.out.println("B告訴A的答案是--》"+result); } }
package CallBack; public class B { public void executeMessage(CallBack callBack,String question){ System.out.println(callBack.getClass()+"問的問題--》"+question); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } String result="答案是2"; callBack.solve(result); } }
package CallBack; public class test { public static void main(String[] args) { B b=new B(); A a=new A(b); a.ask("1+1=?"); } }
console結果:
運行結果: A問了B一個問題 我要逛街去了 class CallBack.A問的問題--》1+1=? B告訴A的答案是--》答案是2 Process finished with exit code 0
異步回調的實現依賴於多線程或者多進程
軟件模塊之間總是存在着一定的接口,從調用方式上,可以把他們分為三類:同步調用、回調和異步調用。同步調用是一種阻塞式調用,調用方要等待對方執行完畢才返回,它是一種單向調用;回調是一種雙向調用模式,也就是說,被調用方在接口被調用時也會調用對方的接口;異步調用是一種類似消息或事件的機制,不過它的調用方向剛好相反,接口的服務在收到某種訊息或發生某種事件時,會主動通知客戶方(即調用客戶方的接口)。回調和異步調用的關系非常緊密,通常我們使用回調來實現異步消息的注冊,通過異步調用來實現消息的通知。
多線程中的“回調” (JDK8之前)
Java多線程中可以通過callable和future或futuretask結合來獲取線程執行后的返回值。實現方法是通過get方法來調用callable的call方法獲取返回值。
其實這種方法本質上不是回調,回調要求的是任務完成以后被調用者主動回調調用者的接口。而這里是調用者主動使用get方法阻塞獲取返回值。
一般情況下,我們會結合Callable和Future一起使用,通過ExecutorService的submit方法執行Callable,並返回Future。
public class 多線程中的回調 { //這里簡單地使用future和callable實現了線程執行完后 public static void main(String[] args) throws ExecutionException, InterruptedException { ExecutorService executor = Executors.newCachedThreadPool(); Future<String> future = executor.submit(new Callable<String>() { @Override public String call() throws Exception { System.out.println("call"); TimeUnit.SECONDS.sleep(1); return "str"; } }); //手動阻塞調用get通過call方法獲得返回值。 System.out.println(future.get()); //需要手動關閉,不然線程池的線程會繼續執行。 executor.shutdown(); //使用futuretask同時作為線程執行單元和數據請求單元。 FutureTask<Integer> futureTask = new FutureTask(new Callable<Integer>() { @Override public Integer call() throws Exception { System.out.println("dasds"); return new Random().nextInt(); } }); new Thread(futureTask).start(); //阻塞獲取返回值 System.out.println(futureTask.get()); } @Test public void test () { Callable callable = new Callable() { @Override public Object call() throws Exception { return null; } }; FutureTask futureTask = new FutureTask(callable); } }
比起future.get(),其實更推薦使用get (long timeout, TimeUnit unit) 方法,設置了超時時間可以防止程序無限制的等待future的結果。
CompletableFuture介紹(JDK8)
Future模式的缺點
-
Future雖然可以實現獲取異步執行結果的需求,但是它沒有提供通知的機制,我們無法得知Future什么時候完成。
-
要么使用阻塞,在future.get()的地方等待future返回的結果,這時又變成同步操作。要么使用isDone()輪詢地判斷Future是否完成,這樣會耗費CPU的資源。
CompletableFuture
Netty、Guava分別擴展了Java 的 Future 接口,方便異步編程。
Java 8新增的CompletableFuture類正是吸收了所有Google Guava中ListenableFuture和SettableFuture的特征,還提供了其它強大的功能,讓Java擁有了完整的非阻塞編程模型:Future、Promise 和 Callback(在Java8之前,只有無Callback 的Future)。
CompletableFuture能夠將回調放到與任務不同的線程中執行,也能將回調作為繼續執行的同步函數,在與任務相同的線程中執行。它避免了傳統回調最大的問題,那就是能夠將控制流分離到不同的事件處理器中。
CompletableFuture彌補了Future模式的缺點。在異步的任務完成后,需要用其結果繼續操作時,無需等待。可以直接通過thenAccept、thenApply、thenCompose等方式將前面異步處理的結果交給另外一個異步事件處理線程來處理。
相關詳細API內容介紹: 理解Java8里面CompletableFuture異步編程
