future and promise

200 阅读7分钟

相关文章

怎样理解阻塞非阻塞与同步异步的区别?
IO - 同步,异步,阻塞,非阻塞(亡羊补牢篇)
服务化基石之远程通信系列三:I/O模型

java异步的几个例子

同步调用

public class SyncDemo {

    public static void main(String[] args) throws InterruptedException {
        long l = System.currentTimeMillis();
        int i = syncCalculate();//<1>
        System.out.println("计算结果:" + i);
        System.out.println("主线程运算耗时:" + (System.currentTimeMillis() - l) + " ms");
    }
    //最常用的同步调用
    static int syncCalculate() {
        System.out.println("执行耗时操作...");
        timeConsumingOperation();
        return 100;
    }
    static void timeConsumingOperation() {
        try {
            Thread.sleep(3000);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

输出:

执行耗时操作...
计算结果:100
主线程运算耗时:3001 ms

同步阻塞程序,大部门的程序逻辑,在<1>处执行耗时操作,程序必须等到耗时方法执行完毕之后,才能执行下面的语句。

Future 模式--回调模式1

上面的例子在耗时操作的时候,程序必须有等待耗时操作完成之后才能继续下面的程序,我们可以使用future模式,这个程序使用一个future占位符,主程序并不需要等待future函数完成耗时任务,而是初始化future对象即可进行下一步程序的操作,callback耗时任务会同步进行计算,需要耗时任务的返回值的时候,只需要调用初始化好的future占位符来获取callback的结果。Future代表一个占位符,代表未来的的结果;Future 模式可以细分为将来式和回调式两种模式。

public class TestFuture1 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        long l = System.currentTimeMillis();
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        Future<Integer> future = executorService.submit(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                System.out.println("执行耗时操作...");
                timeConsumingOperation();
                return 100;
            }
        });//<1>
        //其他耗时操作..
        System.out.println("计算结果:" + future.get());//<2>
        System.out.println("主线程运算耗时:" + (System.currentTimeMillis() - l) + " ms");
    }


    static void timeConsumingOperation() {
        try {
            Thread.sleep(3000);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

输出:

执行耗时操作...
计算结果:100
主线程运算耗时:3057 ms
  • 结果分析:<1> 将回调接口放给线程池执行,但是并不是立马执行任务,而是立即先返回一个future占位符,让主程序可以继续往下执行不需要阻塞,这一步是非阻塞的 <2>future.get()是获取future占位符回调接口的结果,如果回调接口执行完毕,会立即返回结果,如果没有执行完毕,这一步主程序就会阻塞等待回调接口返回结果。

从执行结果来看,异步的future模式主线程的运算耗时比同步的模式还要长,并没有比同步的模式快,因为把耗时操作提交任务给线程池(非阻塞)和获取结果(阻塞)需要一些额外的开销,但是他们之间可以进行多个耗时操作的并行计算,消耗比提交任务给线程池的消耗大的多。所以Future模式适合多个相对独立的耗时任务并行执行。

用2个耗时任务来看一下结果:

public class TestFuture2 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        long l = System.currentTimeMillis();
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        Future<Integer> future = executorService.submit(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                System.out.println("执行耗时操作...");
                timeConsumingOperation();
                return 100;
            }
        });//<1>

        //其他耗时操作..<3>
        timeConsumingOperation2();
        System.out.println("计算结果:" + future.get());//<2>
        System.out.println("主线程运算耗时:" + (System.currentTimeMillis() - l) + " ms");
    }


    static void timeConsumingOperation() {
        try {
            Thread.sleep(3000);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    static void timeConsumingOperation2() {
        try {
            Thread.sleep(4000);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

输出:

执行耗时操作...
计算结果:100
主线程运算耗时:4008 ms
  • 结果分析:由上面的例子可以看出,2个耗时操作,如果串行执行的话,总时间应该大于7s,如果其中一个使用future模式,另一个串行,最后的时间4008ms,其实2个任务是并行执行的,要比串行执行更节省时间。

Future 模式--回调式2

写过前端 ajax 代码的朋友对 callback 的写法并不会陌生,而 Future 模式的第二种用法便是回调。很不幸的事,jdk 实现的 Future 并没有实现 callback,addListener 这样的方法,想要在 JAVA 中体验到 callback 的特性,得引入一些额外的框架。

回调模式实现 --netty

Netty 除了是一个高性能的网络通信框架之外,还对 jdk 的Future 做了扩展,翻看其文档 netty.io/wiki/using-… 可以发现其扩展了一个 listener 接口。 maven 依赖:

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.22.Final</version>
</dependency>

public class NettyFuture {
    public static void main(String[] args) throws InterruptedException {
        long l = System.currentTimeMillis();
        DefaultEventExecutorGroup executors = new DefaultEventExecutorGroup(4);
        Future<Integer> f = executors.submit(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                System.out.println("执行耗时操作...");
                timeConsumingOperation();
                return 100;
            }
        });

        f.addListener(new FutureListener<Object>(){
            @Override
            public void operationComplete(Future<Object> objectFuture) throws Exception {
                System.out.println("计算结果::"+objectFuture.get());
            }
        });

        System.out.println("主线程运算耗时:" + (System.currentTimeMillis() - l)+" ms");
        new CountDownLatch(1).await();//不让守护线程退出
    }

    static void timeConsumingOperation() {
        try {
            Thread.sleep(3000);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

输出:

主线程运算耗时:183 ms
执行耗时操作...
计算结果::100
  • 结果分析:把耗时操作提交给另一个线程去完成,并且通过addListener给这个线程添加了一个监听器回调函数,监听这个线程执行结束之后触发其他的操作,从打印结果来看,主线程耗时183ms,说明主线程完全没有被阻塞,耗时操作的执行和结果都没有阻塞主线程执行。这才是真正的异步编程,回调。

回调式实现二-Guava

maven依赖:

<dependency>
   <groupId>com.google.guava</groupId>
   <artifactId>guava</artifactId>
   <version>21.0</version>
</dependency>
public class GuavaFuture {
    public static void main(String[] args) throws InterruptedException {
        long l = System.currentTimeMillis();
        ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor());
        ListenableFuture<Integer> future = service.submit(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                System.out.println("执行耗时操作...");
                timeConsumingOperation();
                return 100;
            }
        });

        Futures.addCallback(future, new FutureCallback<Integer>() {
            @Override
            public void onSuccess(Integer result) {
                System.out.println("计算结果:" + result);
            }

            @Override
            public void onFailure(Throwable t) {
                System.out.println("异步处理失败,e=" + t);
            }
        });//<2>

        System.out.println("主线程运算耗时:" + (System.currentTimeMillis() - l)+" ms");
        new CountDownLatch(1).await();//不让守护线程退出
    }

    static void timeConsumingOperation() {
        try {
            Thread.sleep(3000);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

输出:

执行耗时操作...
主线程运算耗时:61 ms
计算结果:100
  • 结果分析:这种写法和最后的结果和netty差不多。

callback hell 和 promise模式

同样的如果你对 ES6 有所接触,就不会对 Promise 这个模式感到陌生,如果你对前端不熟悉,也不要紧,我们先来看看回调地狱(Callback Hell)是个什么概念。

回调是一种我们推崇的异步调用方式,但也会遇到问题,也就是回调的嵌套。当需要多个异步回调一起书写时,就会出现下面的代码(以 js 为例):

asyncFunc1(opt, (...args1) => {
   asyncFunc2(opt, (...args2) => {
       asyncFunc3(opt, (...args3) => {
            asyncFunc4(opt, (...args4) => {
                // some operation
            });
        });
    });
});

java的业务代码很少出现这样的多层嵌套,这种场景出现在前端编程中比较多。多层嵌套的代码晦涩难懂,为了让代码更易读可维护,提出了promise模式,让代码变得更扁薄一些。

前面提到了 Netty 和 Guava 的扩展都提供了 addListener 这样的接口,用于处理 Callback 调用,但其实 jdk1.8 已经提供了一种更为高级的回调方式:CompletableFuture。首先尝试用 CompletableFuture 来解决回调的问题。

public class CompleteFuture {
    public static void main(String[] args) throws InterruptedException {
        long l = System.currentTimeMillis();
        CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
            System.out.println("执行耗时操作...");
            timeConsumingOperation();
            return 100;
        });

        completableFuture.whenComplete((result, e) -> {
            System.out.println("结果:" + result);
        });
        System.out.println("主线程运算耗时:" + (System.currentTimeMillis() - l)+" ms");
        new CountDownLatch(1).await();
    }

    static void timeConsumingOperation() {
        try {
            Thread.sleep(3000);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

输出:

主线程运算耗时:132 ms
执行耗时操作...
结果:100
  • 结果分析: 从打印结果来看,耗时操作并没有阻塞主线程的运行,主线程运行耗时132ms,然后并行执行耗时操作,最后耗时操作执行完成的时候,返回结果。不需要引入第三方类库,仅依靠jdk自己提供的java.util.concurrent.CompletableFuture就可以实现。

解决回调地狱的问题:

public class CompleteFuture2 {
    public static void main(String[] args) throws InterruptedException {
        long l = System.currentTimeMillis();
        CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
            System.out.println("在回调中执行耗时操作...");
            timeConsumingOperation();
            return 100;
        });
        completableFuture = completableFuture.thenCompose(i -> {
            return CompletableFuture.supplyAsync(() -> {
                System.out.println("在回调的回调中执行耗时操作...");
                timeConsumingOperation();
                return i + 100;
            });
        });
        completableFuture.whenComplete((result, e) -> {
            System.out.println("计算结果:" + result);
        });
        System.out.println("主线程运算耗时:" + (System.currentTimeMillis() - l) + " ms");
        new CountDownLatch(1).await();

    }
    static void timeConsumingOperation() {
        try {
            Thread.sleep(3000);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

输出:

在回调中执行耗时操作...
主线程运算耗时:56 ms
在回调的回调中执行耗时操作...
计算结果:200
  • 结果分析: 使用 thenCompose 或者 thenComposeAsync 等方法可以实现回调的回调,且写出来的方法易于维护。