浅析Java回调机制

2018-12-27 by Victor Lv Filed under 技术博客

浅析Java回调机制

  生活中,我们经常碰到这样的例子:当我们想完成某个工作时 ,期间会有一个非常耗时的子任务,但我们又不想干等待它的完成才继续工作, 于是我们希望把这个子任务交付出去给别人完成, 然后我们就可以继续往下做其他事情了, 等别人完成工作的时候,又会自动回来我这告诉我完成情况。

  好比如你住酒店,你在打扫房间的时候, 有一堆衣服要洗,但洗衣服这事太耗时了, 所以你把清洁阿姨叫过来让她帮你拿衣服去洗, 并告诉她衣服洗完后帮我放回A201房间的柜子里, 托付完毕,你可以继续干手头的打扫工作了。

  基于这样的思想,应用于编程中,那就衍生了回调。 在应用系统中,我们经常碰到很耗时的 I/O 子任务(包括磁盘I/O、网络I/O), 如果我们完全照顺序执行的,那不得不等待这个 I/O 操作完成了我们才能继续往下执行程序, 那在这段 I/O 时间内 CPU 是被极度闲置的。 于是,我们很自然地想到了异步,另起一个线程做那个 I/O 操作不就行了么? 但是,我们又增加了一个需求, 那就是当这个 I/O 操作完成后,我希望它能告诉我,好让我继续去做某些事情, 因为这些事情必须在这个 I/O 操作完成后才能执行。于是,就产生了异步回调

  下面结合程序示例来讲一讲(异步)回调。在讲异步回调之前, 先来看几个更简单些的概念:“同步调用”、“异步调用”、“同步回调”

同步调用

程序示例:

A.java

package callback;

/**
 * @ClassName: A
 * @Description: TODO
 * @Author: Victor Lv (http://langlv.me)
 * @Date: 2018/12/27 9:27
 * @Version: 1.0
 */

public class A implements InterfaceA{

    private InterfaceB b;

    public A(InterfaceB b) {
        this.b = b;
    }

    public void work() {
        System.out.println("Begin work");
        //call b to help me do something
        b.handle();
        continueWorking();
    }

    public void continueWorking() {
        System.out.println("Continue to work");
    }

}

B.java

package callback;

/**
 * @ClassName: B
 * @Description: TODO
 * @Author: Victor Lv (http://langlv.me)
 * @Date: 2018/12/27 9:27
 * @Version: 1.0
 */

public class B implements InterfaceB {

    public void handle() {
        //do something
        System.out.println("B takes long long time to do something");
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

  这是一个很简单的示例,作用类似于:老板A在办公室干活(work), 他有一封信要寄出去,但他不想自己跑腿,然后他交代他的秘书B去处理(handle), 等B处理完之后他就继续工作了(continueWorking)。

  这就是一个同步调用。

  顺便,我们丰富了下功能,就是把老板A和秘书B接口化(面向接口编程), 这样的话,秘书B即便换了人,老板还是同样的差使动作; 反之,即便老板换了又换,秘书B也能干相同的差事。

InterfaceA.java

package callback;

public interface InterfaceA {
}

InterfaceB.java**

package callback;

public interface InterfaceB {
    public void handle();
}

  测试一下输出:

package callback;

/**
 * @ClassName: CallbackTest
 * @Description: TODO
 * @Author: Victor Lv (http://langlv.me)
 * @Date: 2018/12/27 9:27
 * @Version: 1.0
 */

public class CallbackTest {

    public static void main(String[] args) {
        B b = new B();
        A a = new A(b);
        a.work();
    }

    /**
     * Output:
        Begin work
        B takes long long time to do something
        Continue to work
     */

}

异步调用

  注意上面的同步调用中,老板A必须等待秘书B处理完之后才能继续往下工作, 因为大家是在同一个线程,顺序执行的, 这就导致了老板A在秘书B寄信期间不能干活。 但是寄信这事老板并不是老板继续往下工作的必要前提,所以我们可以采用异步调用的方式处理。

  异步调用相比同步调用,增加一个多线程处理即可:

  只需要改下 A.java 即可:

package callback;

/**
 * @ClassName: A
 * @Description: TODO
 * @Author: Victor Lv (http://langlv.me)
 * @Date: 2018/12/27 9:27
 * @Version: 1.0
 */

public class A implements InterfaceA, Runnable{

    private InterfaceB b;

    public A(InterfaceB b) {
        this.b = b;
    }

    public void work() {
        System.out.println("Begin work");

        //start another thread to handle
        new Thread(this).start();

        continueWorking();
    }

    public void continueWorking() {
        System.out.println("Continue to work");
    }

    public void run() {
        //call b to help me do something
        b.handle();
    }
}

测试输出:

Begin work
Continue to work
B takes long long time to do something

  通过输出,我们知道,在把寄信的任务托付给了秘书B之后,老板A马上就继续工作了, 而过了一段时间B才完成了寄信的工作。

同步回调

  顾名思义,同步回调相比同步调用增加了一个回调机制,来看看程序怎么实现的:

A.java

package callback;

/**
 * @ClassName: A
 * @Description: TODO
 * @Author: Victor Lv (http://langlv.me)
 * @Date: 2018/12/27 9:27
 * @Version: 1.0
 */

public class A implements InterfaceA{

    private InterfaceB b;

    public A(InterfaceB b) {
        this.b = b;
    }

    public void work() {
        System.out.println("Begin work");

        //call b to help me do something
        b.handle(this);

        System.out.println("End work");
    }

    public void continueWorking() {
        System.out.println("Continue to work");
    }

    public void callback() {
        System.out.println("B callback");
        continueWorking();
    }
}

B.java

package callback;

/**
 * @ClassName: B
 * @Description: TODO
 * @Author: Victor Lv (http://langlv.me)
 * @Date: 2018/12/27 9:27
 * @Version: 1.0
 */

public class B implements InterfaceB {

    public void handle(InterfaceA a) {
        //do something
        System.out.println("B takes long long time to do something");
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        a.callback();
    }
}

InterfaceA.java

package callback;

public interface InterfaceA {
    public void callback();
}

InterfaceB

package callback;

public interface InterfaceB {
    public void handle(InterfaceA a);
}

测试输出

Begin work
B takes long long time to do something
B callback
Continue to work
End work

  这个示例,作用类似于:老板A在办公室干活(work), 这时他感觉饿了,想吃楼下的山东煎饼,但他不想自己跑腿, 然后他交代他的秘书B去处理(handle),并告诉B处理完之后敲下我的门通知我(callback), 在B做完事回来之前,他都可以躺在椅子上休息(CPU空闲), 等到B把煎饼买回来并给到他之后,他吃完又有力气可以继续干活了(continueWorking)。 同步回调和同步调用一样,所有动作都是按顺序执行的, 也就是类似如下的程序执行:

System.out.println("Begin work");

System.out.println("B takes long long time to do something");
try {
    Thread.sleep(3000);
} catch (InterruptedException e) {
    e.printStackTrace();
}

System.out.println("Continue to work");

  在程序中,在调用 B 进行 handle 的时候,A 需要把 this 也就是自身对象的引用传给B, 这就相当于我们住酒店时把房门钥匙给了清洁阿姨, 然后阿姨洗完衣服后通过房门钥匙开了门并把洗完的衣服放回桌上。 问题来了,房门钥匙都给了别人,那岂不是有被盗风险? B 通过this不就可以随意调用 A 对象的 public 方法了吗? 别担心,A 和 B 都尽在程序设计者的掌控当中。因为,请注意, B 是通过接口 InterfaceA 的方式来操作this调用回调函数的, 也就是说只有我们在 InterfaceA 定义的方法,B 才有权访问, 看看我们在 InterfaceA 定义了哪些方法?对的,只有 callback 这一种方法, 也就是说我们赋予清洁阿姨的权限只有把衣服放回桌上这一操作。

  这就是一个同步回调。

异步回调

  介绍完上面三个概念之后,很自然地就可以引出我们今天的主角:异步回调

  上面同步回调的例子中,老板A在秘书B把煎饼买回来之前是没法继续干活的, 因为他实在太饿了。但是哪一天老板A没那么饿呢,他想在秘书B买煎饼期间又继续干活去, 那怎么办,这就是异步回调的用武之地了。

  异步回调无非就是在上面的同步回调的基础上增加异步调用嘛。来看程序示例:

A.java

package callback;

/**
 * @ClassName: A
 * @Description: TODO
 * @Author: Victor Lv (http://langlv.me)
 * @Date: 2018/12/27 9:27
 * @Version: 1.0
 */

public class A implements InterfaceA, Runnable{

    private InterfaceB b;

    public A(InterfaceB b) {
        this.b = b;
    }

    public void work() {
        System.out.println("Begin work");

        //start another thread to handle
        new Thread(this).start();

        System.out.println("Let's take a dance");

    }

    public void continueWorking() {
        System.out.println("Continue to work");
    }

    public void callback() {
        System.out.println("B callback");
        continueWorking();
    }

    public void run() {
        //call b to help me do something
        b.handle(this);
    }
}

InterfaceA.java

package callback;

public interface InterfaceA {
    public void callback();
}

测试输出

Begin work
Let's take a dance
B takes long long time to do something
B callback
Continue to work

  可以看到,因为我们启用了一个新的线程让 B 去 handle, 所以不影响 A 继续干别的活,比如老板 A 想跳支舞自嗨下(take a dance)。

参考文章:

Java回调机制解读-博客园

Java 中回调机制是什么原理?-知乎


本文作者为 Victor Lv ,原出处为Victor Lv's Blog(http://langlv.me),转载请保留此句。


标签 Callback 设计模式
Fork me on GitHub