理解回调函数,java异步执行,多线程
很多语言都有回调函数,所以必须理解它。
java的形式:传递this指针给其他线程(等对方执行完后,用this引用来拿到回调函数)、传递匿名类以及显式的Callback接口。
js中常见的ajax(我觉得这个是最容易理解的回调函数)。
Nodejs中的事件编程。
RocketMQ里面的客户端异步发消息给Broker。
。。。。。。。。
好了,现在以最平凡的java为例,假设有两个函数:
函数g(): public int g(){
return 1+1;
}
函数f(): public void f1(){
int l =g();
System.out.println(l);//本例中输出2就算成功
}
将以上代码随便写个main跑一下f1(),执行过程分析:
启动main函数时,jvm在栈区划出一块-Xss大小的内存给main线程。接下来main()调用f1(),f1()需要的栈帧会在main的调用栈里面分配。同理,f1()调用g(),g()也是在这个栈上分配空间。执行结束后,g弹栈àf1弹栈。整个过程都在main的栈里面发生。
假如g是个耗时的任务,而且f()接下来的代码中暂时还不需要g的返回值,g早一点返回更好,晚一点也能接受。那就可以把g()放到另一个线程,main线程继续执行f()接下来的逻辑去。
至于g()什么时候返回、f()怎么拿到值,撅滴尅提供两种方式:
1、future.get(),
2、以及第二种通常更优的方式:回调函数。
如果还有第三种,请各位大神不吝赐教啊!
无论哪一种,第一步都要先new子线程、让子线程执行g()。而f()还是在父线程调用栈里面。
开始动手:总共分两步:
第1步、定义一个Callable,实现call,里面只调g()。
//不采用匿名类方式,定义Callable线程:
private class A implements Callable{
public Integer call() {
return g();
}
}
第2步、实例化这个Callable, start()线程,并用一个futureTask.get()去等返回值。
//再到f2中实例化线程A。并等待结果。
public void f2() throws ExecutionException, InterruptedException {
new Thread(new FutureTask<Integer>(new A())).start();
f.get();//这是个阻塞-轮询方法,不会让父线程结束或者干其他的去,一直等到子线程返回
System.out.println(l);//输出l就算成功
}
分析:
f2创造了一个子线程,jvm又新建一块大小为-Xss的调用栈给新线程使用,可以通过打印线程名字来查看。在这个新调用栈里执行g()。可是父线程因为执行了阻塞方法f.get(),会每隔一段时间去询问一下子线程“搞完了没”。直到子线程回答“搞完了”。
虽然满足了g()在另一个线程中执行,也获取了cpu时间片和内存资源,但是很明显,父线程在等子线程,这样岂不是很傻?如果子线程知道父线程会怎么对待它的返回值,那可以代劳啊!
所以应该用回调函数来实现我们的目的。当父线程发出命令后就可以去干别的事情了(虽然本例中没有别的事情~V~)。等到子线程自己弄完,它自己会根据回调函数的逻辑去处理返回值了。
//第一步:定义回调函数。相当于父线程告诉子线程:“将来你执行完你自己的逻辑后,按照我给的MyCallback的onSuccess方法去处理一下结果”。
interface MyCallback{
void onSuccess(int l);
}
//第二步:子线程的定义有点小复杂:子线程是个Callable,它的call方法中只调用g()。它要提供一个可以注入回调函数的构造器。当然也可以通过其他方式设置进来啊,不一定非要构造函数。
private class A implements Runnable {
private MyCallback myCallback;
A(MyCallback myCallback){
this.myCallback = myCallback;
}
public void run() {
int l = g();
myCallback.onSuccess(l);//按照父线程给的逻辑来处理g()的结果
}
}
第二步的最后需要子线程配合,要在应用层改代码,要是程序员忘了调onSuccess,那就白费功夫设计这半天啦!所以spring就很聪明,用扫描注解(@Async)的方法,再通过cglib生成一个新的class的方式尽量减少应用代码的改动。
//第三步(本例中最后一步 ):主驱动f方法。用匿名类实例化了new MyCallback,逻辑都放在onSuccess里面:当g()执行成功后,需要主动调一下onSuccess来额外处理一下结果。
public void f3() throws ExecutionException, InterruptedException{
Runnable a = new A(new MyCallback() {
public void onSuccess(int l) {
System.out.println(l);
}
});
new Thread(a).start();
}
从客户端f()的视角来看,需要多准备好一个回调函数给被调用者。注意理解一下,回调函数上如果带形参,将来需要被调用方(本例中的g方法)传入实参进来的哦!
回调函数才是真正的异步。jdk的future.get()机制只做到了一大半。一些第三方框架,例如akka已经提供了回调机制的future了,而且api也很简洁。所以真正编码时并不需要我们自己写这么复杂的逻辑。
另外,名字很重要,“回调”这个叫法是因为本来是f调用g,子线程是被调用方,但却需要在最后额外执行一段调用方(本例中执行f方法的线程)提供的onSuccess()函数。它与调用的方向相反,回去了,所以onSuccess被称为回调。
引入回调函数是为了解决被调用方---这里是g()---执行任务耗时不确定的难点。就好像小明妈妈让小明去买酱油,妈妈不知道小明啥时候能回来,就嘱咐了一句(相当于给一个回调函数):“酱油买回来、放到厨房后,你才能去玩电脑”。然后小明妈妈自己忙自己的去了。小明也去买酱油了。要是用future.get(),在小明出去的这段时间内,妈妈最终(最终是因为妈妈不会在小明刚出门就开始给小明打电话,比喻父线程在子线程start()之后和future.get()之前的那些代码)会陷入每隔三分钟给小明打电话的循环中,问小明你买到了吗?一直打到小明回来后,妈妈才给小明说:“好了,酱油买回来了,你去玩电脑吧”。
最后切记、切记,回调函数一定要和多线程配合才有意义。否则,同一个线程调用栈里面,上下栈帧在那里自娱自乐,这太傻。这就好比妈妈自己去买酱油,还对自己说:“酱油买回来,要放好,才能玩电脑”。不仅没有并行,还浪费妈妈口水。