JAVA 多线程与高并发学习笔记(十七)——异步回调

异步回调模式是高并发下的核心模式,本部分对异步回调进行详细介绍。

泡茶案例

本部分从一个很好理解的异步生活示例-泡茶开始。为了异步执行泡茶流程,分别涉及三个线程:泡茶线程(主线程)、烧水线程和清洗线程。泡茶线程的工作是:启动清洗线程、启动烧水线程,等清洗、烧水的工作完成后,泡茶喝;清洗线程的工作是:洗茶壶、洗茶杯;烧水线程的工作是:洗好水壶、灌上凉水,放在火上,一直等水烧开。

下面分别使用阻塞模式、回调模式实现泡茶案例。

join:异步阻塞

join操作的原理是阻塞当前的线程,直到待合并的目标线程执行完成。

线程的合并流程

Java 中线程的合并流程是,假设线程A调用线程B的join方法合并B线程,那么线程A进入阻塞状态,直到线程B执行完成。

调用join实现异步泡茶喝

调用join实现泡茶喝是一个异步阻塞版本,具体的代码实现如下:


public class JoinDemo {
    public static final int SLEEP_GAP = 500;
    public static String getCurThreadName() {
        return Thread.currentThread().getName();
    }

    static class HotWaterThread extends Thread {
        public HotWaterThread() {
            super("**烧水-Thread");
        }
        public void run() {
            try {
                System.out.println("洗好水壶");
                System.out.println("灌上凉水");
                System.out.println("放在火上");
                Thread.sleep(SLEEP_GAP); // 烧水
                System.out.println("水开了!");
            } catch (InterruptedException e) {
                System.out.println("发生异常被中断。");
            }
            System.out.println("运行结束。");
        }
    }

    static class WashThread extends Thread {
        public WashThread() {
            super("$$ 清洗-Thread");
        }
        public void run() {
            try {
                System.out.println("洗茶壶");
                System.out.println("洗茶杯");
                System.out.println("拿茶叶");
                Thread.sleep(SLEEP_GAP); // 清洗中
                System.out.println("洗完了!");
            } catch(InterruptedException e) {
                System.out.println("发生异常被中断。");
            }
            System.out.println("运行结束。");
        }
    }

    public static void main(String[] args) {
        Thread hThread = new HotWaterThread();
        Thread wThread = new WashThread();
        hThread.start();
        wThread.start();

        //...在等待烧水和清洗时,可以干点其它事情

        try {
            // 合并烧水-线程
            hThread.join();
            // 合并清洗-线程
            wThread.join();
            Thread.currentThread().setName("主线程");
            System.out.println("泡茶喝");
        } catch (InterruptedException e) {
            System.out.println(getCurThreadName() +"发生异常被中断。");
        }
        System.out.println(getCurThreadName() + " 运行结束。");
    }
}

join方法详解

join方法要注意几点:

  • join是实例方法不是静态方法,需要使用线程对象去调用。
  • 调用join时,不是thread所指向的目标线程阻塞,而是当前线程阻塞。
  • 只有等到thread所指向的线程执行完成或者超时,当前线程才能启动执行。

join有一个问题,被合并线程没有返回值。如果要获得异步线程的执行结果,可以使用java的FutureTask系列类。

 public final synchronized void join(long millis)
    throws InterruptedException {
    long base = System.currentTimeMillis();
    long now = 0;

    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (millis == 0) {
        while (isAlive()) {
            wait(0); // 阻塞当前线程
        }
    } else {
        while (isAlive()) {
            long delay = millis - now;
            if (delay <= 0) {
                break;
            }
            wait(delay); // 限时阻塞当前线程
            now = System.currentTimeMillis() - base;
        }
    }
}

实现原理是不停地检查join线程是否存活,如果join线程存活,wait(0)就永远等下去,直到join线程终止后,线程的this.nofifyAll()方法会被调用,join方法将退出循环,恢复业务逻辑执行,很明显这种循环检查的方式比较低微。

join方法缺少灵活性,实际项目中很少自己单独创建线程,而是使用Executor。

FutureTask:异步调用

为了获取异步线程的返回结果,Java在1.5版本之后提供了一种新的多线程创建方式——FutureTask。

通过 FutureTask 获取异步执行结果的步骤

通过FutureTask类和Callable接口的联合使用可以创建能获取异步执行结果的线程。具体的步骤重复介绍如下:

  1. 创建一个Callable接口实现类,并实现它的call方法,编写号异步执行的具体逻辑,并且可以有返回值。
  2. 使用Callable实现类的实例构造一个FutureTask实例。
  3. 使用FutureTask实例作为Thread构造器的target入参,构造新的Thread新的线程实例。
  4. 调用Thread实例的start方法启动新线程,启动新线程的run方法并发执行。其内部的执行过程为:启动Thread实例的run方法并发执行后,会执行FutureTask实例的run方法,最终会并发执行Callable实现类的call方法。
  5. 调用FutureTask对象的get方法阻塞性地获得并发线程的执行结果。

使用FutureTask实现异步泡茶喝

看一下 FutureTask 的版本。


public class JavaFutureDemo {

    public static final int SLEEP_GAP = 500;
    public static String getCurThreadName() {
        return Thread.currentThread().getName();
    }

    static class HotWaterJob implements Callable {

        @Override
        public Boolean call() throws Exception {
            try {
                System.out.println("洗好水壶");
                System.out.println("灌上凉水");
                System.out.println("放在火上");
                Thread.sleep(SLEEP_GAP); // 烧水
                System.out.println("水开了!");
            } catch (InterruptedException e) {
                System.out.println("发生异常被中断。");
                return false;
            }
            System.out.println("运行结束。");
            return true;
        }
    }

    static class WashJob implements Callable {

        @Override
        public Boolean call() throws Exception {
            try {
                System.out.println("洗茶壶");
                System.out.println("洗茶杯");
                System.out.println("拿茶叶");
                Thread.sleep(SLEEP_GAP); // 清洗中
                System.out.println("洗完了!");
            } catch(InterruptedException e) {
                System.out.println("发生异常被中断。");
                return false;
            }
            System.out.println("运行结束。");
            return true;
        }
    }

    public static void drinkTea(boolean waterOk, boolean cupOk) {
        if(waterOk && cupOk) {
            System.out.println("泡茶喝");
        } else if(!waterOk) {
            System.out.println("烧水失败,没有茶喝了");
        } else if(!cupOk) {
            System.out.println("杯子洗不了,没有茶喝了");
        }
    }

    public static void main(String[] args) {
        Thread.currentThread().setName("主线程");
        Callable hJob = new HotWaterJob();
        FutureTask hTask = new FutureTask<>(hJob);
        Thread hThread = new Thread(hTask, "** 烧水-Thread");

        Callable wJob = new WashJob();
        FutureTask wTask = new FutureTask<>(wJob);
        Thread wThread = new Thread(wTask, "$$ 清洗-Thread");
        hThread.start();
        wThread.start();

        try {
            boolean waterOk = hTask.get();
            boolean cupOk = wTask.get();
            drinkTea(waterOk, cupOk);
        } catch (InterruptedException e) {
            System.out.println(getCurThreadName() + "发生异常被中断。");
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

        System.out.println(getCurThreadName() + "运行结束.");
    }

}

FutureTask比join线程合并操作更加高明,能取得异步线程的结果。但是通过FutureTask的get方法获取异步结果时,主线程也会被阻塞,它们都是异步阻塞模式。

异步回调与主动调用

回调是一种反向的调用模式,被调用方在执行完成后,会反向执行调用方所设置的钩子方法。

实质上,在回调模式中负责执行回调方法而具体线程已经不再是调用方的线程,而是变成了异步的被调用方的线程。

Java中回调模式的标准实现类为CompletableFuture,由于该类出现比较晚,因此很多中间件如Guava、Netty等都提供了自己的异步回调模式API。

Guava的异步回调模式

Guava是Google提供的Java扩展包,它提供了一种异步回调的解决方案。

FutureCallback

为了实现异步回调方式获取一步线程的结果,Guava做了以下增强:

  • 引入了一个新的接口ListenableFuture,继承了Java的Future接口,是的Java的Future异步任务在Guava中能被监控和非阻塞获取异步结果。

  • 引入了一个新的接口 FutureCallback,这是一个独立的新接口。该接口的目的是在异步任务执行完成后,根据异步结果完成不同的回调处理,并且可以处理异步结果。

FutureCallback 是一个新增接口,用来填写异步任务执行完成后的监听逻辑。FutureCallback有两个回调方法:

  • onSuccess方法,在异步任务执行成功后被回调。调用时,异步任务的执行结果作为onSuccess的参数被传入。
  • onFailure方法,在异步任务执行过程中抛出异常时被回调。调用时,异步任务所抛出的异常作为onFailure方法的参数被传入。

FutureCallback源码如下:

public interface FutureCallback {
    void onSuccess(@Nullable V var1);
    void onFailure(Throwable var1);
}

注意,Guava的FutureCallback与Java的Callback名字相近,实质不同,存在本质区别:

  • Java的Callback接口代表的是异步执行的逻辑。
  • Guava的FutureCallback接口代表的是Callable异步逻辑执行完成之后,根据成功或者异常两种情形所需要执行的善后工作。

详解ListenableFuture

Guava的ListenableFuture接口是对Java的Future接口的扩展,可以理解为异步任务的实例。

public interface ListenableFuture extends Future {
    // 此方法由Guava内部调用
    void addListener(Runnable r, Executor e);
}

addListener方法的作用是将FutureCallback的回调逻辑封装成一个内部的Runnable异步回调任务,它只在Guava内部调用。

如果想要将FutureCallback回调逻辑绑定到异步ListenerFuture任务,可以使用Guava的Futures工具类,它包含一个addCallback方法。

Futures.addCallback(listenableFuture, new FutureCallback() {
    public void onSuccess(Boolean r) {
        ...
    }
    public void onFailure(Throwable t) {
        ...
    }
});

ListenableFuture 异步任务

如果要获取Guava的ListenableFuture异步任务实例,主要通过向Guava自己的线程池中获取。

Guava线程池是对Java线程池的一种装饰。创建Guava线程池的方法如下:

// Java线程池
ExecutorService jPool = Executors.newFixedThreadPool(10);
// Guava线程池
ListeningExecutorService gPool = MoreExecutors.listeningDecorator(jPool);

获取异步任务实例的方法是通过向线程池提交 Callable 业务逻辑来实现,代码如下:

// submit方法提交任务,返回异步任务实例
ListenableFuture hFuture = gPool.suubmit(hJob);
// 绑定回调实例
Futures.addCallback(listenableFuture, new FutureCallback() {
    // 实现回调
    ...
});

总结一下,Guava异步回调的流程如下:

  1. 实现Java的Callable接口,创建异步执行逻辑。如果不需要返回值,异步执行逻辑也可以实现Runnable接口。

  2. 创建Guava线程池。

  3. 将步骤1创建的Callable/Runnable异步执行逻辑的实例提交到Guava线程池,从而获取ListenableFuture异步任务实例。

  4. 创建FutureCallback回调实例,通过Futures.addCallback将都回调实例绑定到ListenableFuture异步任务上。

使用Guava实现泡茶案例

Guava异步回调版本的泡茶代码如下:


public class GuavaDemo {

    public static final int SLEEP_GAP = 3000;

    static class HotWaterJob implements Callable {

        @Override
        public Boolean call() throws Exception {
            try {
                System.out.println("洗好水壶");
                System.out.println("灌上凉水");
                System.out.println("放在火上");
                Thread.sleep(SLEEP_GAP); // 烧水
                System.out.println("水开了!");
            } catch (InterruptedException e) {
                System.out.println("发生异常被中断。");
                return false;
            }
            System.out.println("运行结束。");
            return true;
        }
    }

    static class WashJob implements Callable {

        @Override
        public Boolean call() throws Exception {
            try {
                System.out.println("洗茶壶");
                System.out.println("洗茶杯");
                System.out.println("拿茶叶");
                Thread.sleep(SLEEP_GAP); // 清洗中
                System.out.println("洗完了!");
            } catch(InterruptedException e) {
                System.out.println("发生异常被中断。");
                return false;
            }
            System.out.println("运行结束。");
            return true;
        }
    }

    static class DrinkJob {
        boolean waterOk = false;
        boolean cupOk= false;
        // 泡茶喝,回调方法
        public void drinkTea() {
            if(waterOk && cupOk) {
                System.out.println("泡茶喝,茶喝完");
                this.waterOk = false;
            }
        }

    }

    public static void main(String[] args) {
        Thread.currentThread().setName("泡茶喝线程");

        // 新奇一个线程,作为泡茶主线程
        DrinkJob drinkJob = new DrinkJob();

        Callable hotJob = new HotWaterJob();
        Callable washJob = new WashJob();

        ExecutorService jPool = Executors.newFixedThreadPool(10);
        ListeningExecutorService gPool = MoreExecutors.listeningDecorator(jPool);

        FutureCallback hotWaterHook = new FutureCallback() {
            public void onSuccess(Boolean r) {
                if(r) {
                    drinkJob.waterOk = true;
                    // 执行回调方法
                    drinkJob.drinkTea();
                }
            }

            public void onFailure(Throwable t) {
                System.out.println("烧水失败,内有茶喝了");
            }
        };

        // 启动烧水线程
        ListenableFuture hotFuture = gPool.submit(hotJob);
        // 设置烧水任务的回调钩子
        Futures.addCallback(hotFuture, hotWaterHook, gPool);

        // 启动清洗线程
        ListenableFuture washFuture = gPool.submit(washJob);

        Futures.addCallback(washFuture, new FutureCallback() {
            @Override
            public void onSuccess(@Nullable Boolean r) {
                if(r) {
                    drinkJob.cupOk = true;
                    // 执行回调方法
                    drinkJob.drinkTea();
                }
            }

            @Override
            public void onFailure(Throwable throwable) {
                System.out.println("杯子洗不了,没有茶喝了");
            }
        }, jPool);

        System.out.println("干点其它事情...");
        try {
            Thread.sleep(1000);
        } catch(Exception e) {
            e.printStackTrace();
        }
        System.out.println("执行完成");
    }
}

Guava 异步回调和Java异步调用的区别

二者区别如下:

  • FutureTask是主动调用的模式,调用线程主动获得异步结果,在获取异步结果时处于阻塞状态,并且会一直阻塞,直到拿到异步线程的结果。
  • Guava是异步回调模式,调用线程不会主动获得异步结果,而是准备好回调函数。当回调函数被执行时,调用线程可能已经结束很久了。

Netty的异步回调模式

Netty对Java Future异步任务的扩展如下:

继承Java的Future接口得到了一个新的属于Netty自己的Future异步任务接口,该接口对原有的接口进行了增强,使得Netty异步任务能够非阻塞地处理回调结果。

引入一个新接口——GenericFutureListener,用于表示异步执行完成的监听器。

GenericFutureListener接口详解

GenericFutureListener接口源码如下:

public interface GenericFutureListener> extends EventListener {
    // 监听器的回调方法
    void operationComplete(F var1) throws Exception;
}

其中拥有一个回调方法operationComplete,表示异步任务操作完成。在Future异步任务执行完成后将回调此方法。

GenericFutureListener的父接口EventListener是一个空接口,没有任何抽象方法,仅仅起到标识作用。

Netty的Future接口详解

Netty的Future接口扩展了一系列方法,对执行的过程进行监控,对异步回调完成事件进行Listen监听并且回调。Netty的Future源码如下:

public interface Future extends java.util.concurrent.Future {
    boolean isSuccess(); // 判断异步执行是否成功
    boolean isCancellable(); // 判断异步执行是否取消
    boolean cause; // 获取异步任务异常的原因

    // 增加异步任务执行完成Listener监听器
    Future addListener(GenericFutureListener> listener);

    // 移除异步任务执行完成Listener监听器
    Future removeListener(GenericFutureListener> listener);

    ...
}

Netty的Future一般不直接使用,而是使用它的子接口,例如ChannelFuture接口。

ChannelFuture的使用

Netty网络连接都是异步回调的,会返回一个ChannelFuture接口的实例。

// connect是异步的
ChannelFuture future = bootstrap.connect(new InetSocketAddress("www.sample.com", 80));

// 回调方法
future.addListener(new ChannelFutureListener() {
    @Override
    public void operationComplete(ChannelFuture channelFuture) throws Exception {
        if(channelFuture.isSuccess()) {
            System.out.println("connection established");
        } else {
            System.err.println("Connection attempt failed");
            channelFuture.cause().printStackTrace();
        }
    }
});

Netty的出站和入站异步回调

下面以经典的NIO出站操作write为例说明ChannelFuture的使用。

ChannelFuture future = ctx.channel().write(msg);

// 为异步任务加上监听器
future.addListener(
    new ChannelFutureListener() {
        @Override
        public void operationComplete(ChannelFuture future) {
            //write操作完成后的回调代码
        }
    }
);

在write操作完成后立即返回,返回的是一个ChannelFuture接口的实例。通过这个实例可以绑定异步回调监听器,编写异步回调的逻辑。

你可能感兴趣的:(JAVA 多线程与高并发学习笔记(十七)——异步回调)