Java并发编程工具类篇1-CompletableFuture

412 阅读12分钟

欢迎大家关注 github.com/hsfxuebao ,希望对大家有所帮助,要是觉得可以的话麻烦给点一下Star哈

1. Future和Callable接口

Future接口定义了操作异步任务执行一些方法,如获取异步任务的执行结果、取消任务的执行、判断任务是否被取消、判断任务执行是否完毕等。

@FunctionalInterface
public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}

Callable接口中定义了需要有返回的任务需要实现的方法。比如主线程让一个子线程去执行任务,子线程可能比较耗时,启动子线程开始执行任务后,主线程就去做其他事情了,过了一会才去获取子任务的执行结果。

2. FutureTask

本源的Future接口相关架构如下:

image.png

代码演示如下:

public class FutureTaskDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
        FutureTask<Integer> futureTask = new FutureTask<>(() -> {
            System.out.println(Thread.currentThread().getName() + "\t" + "---come in");
            try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); }
            return 1024;
        });

        new Thread(futureTask,"t1").start();
        System.out.println(futureTask.get());//不见不散,只要出现get方法,不管是否计算完成都阻塞等待结果出来再运行
        
	    ////3秒钟后才出来结果,我只想等待1秒钟,过时不候
	    System.out.println(Thread.currentThread().getName()+"\t"+futureTask.get(1L,TimeUnit.SECONDS));

    }
}

不见不散get()方法会导致阻塞,即如果在主线程中调用FutureTask中的get()方法,不管是否计算完成都阻塞等待结果出来再运行。即不见不散,就是没有计算出结果之前我都会一直等待你计算出结果。

过时不候: get(long timeout, TimeUnit unit)即我只会等待指定的时间,如果你指定的时间没有计算出结果,那我也不等待了,直接抛出超时异常。【工作当中一定要用这个!!!

工作中不要使用阻塞,高并发别忘记使用CAS,使用轮询替代阻塞,isDone()轮询。如果想要异步获取结果,通常都会以轮询的方式去获取结果,尽量不要阻塞;轮询的方式会耗费无谓的CPU资源,而且也不见得能及时地得到计算结果,如果想要异步获取结果,通常都会以轮询的方式去获取结果 尽量不要阻塞

代码演示如下:

//不要阻塞,尽量用轮询替代
while(true) {
    if(futureTask.isDone()) {
        System.out.println("----result: "+futureTask.get());
        break;
    } else {
        System.out.println("还在计算中,别催,越催越慢,再催熄火");
    }
}

想完成一些复杂的任务

  • 应对Future的完成时间,完成了可以告诉我,也就是我们的回调通知。即当子线程执行完计算任务时,应该主动通知主线程,

  • 将两个异步(即两个FutrueTask)计算合成一个异步计算,这两个异步计算互相独立,同时第二个又依赖第一个的结果。【张三想要吃水煮鱼,他就吩咐班长去买锅,副班长卖鱼,小明同学做鱼,最后做出水煮鱼端到张三面前】

  • 当Future集合中某个任务最快结束时,返回结果。

  • 等待Future结合中的所有任务都完成。

3. CompletableFuture

3.1 CompletableFuture和CompletionStage介绍

类架构说明如下图所示

在这里插入图片描述

public class CompletableFuture<T> implements Future<T>, CompletionStage<T> {

接口CompletionStage如下:

  • CompletionStage代表异步计算过程中的某一个阶段,一个阶段完成以后可能会触发另外一个阶段
  • 一个阶段的计算执行可以是一个Function,Consumer或者Runnable。比如:stage.thenApply(x->square(x).thenAccept(x->System.out.print(x)).thenRun((->System.out.println())
  • 一个阶段的执行可能是被单个阶段的完成触发,也可能是由多个阶段一起触发

代表异步计算过程中的某一个阶段,一个阶段完成以后可能会触发另外一个阶段,有些类似Linux系统的管道分隔符传参数。

CompletableFuture如下:

  • 在Java8中,CompletableFuture提供了非常强大的Future的扩展功能,可以帮助我们简化异步编程的复杂性,并且提供了函数式编程的能力,可以通过回调的方式处理计算结果,也提供了转换和组合CompletableFuture的方法。

  • 它可能代表一个明确完成的Future,也有可能代表一个完成阶段(CompletionStage),它支持在计算完成以后触发一些函数或执行某些动作。

  • 它实现了Future和CompletionStage接口

3.2 CompletableFuture核心四个静态方法

  • runAsync 无 返回值如下:

    public static CompletableFuture<Void> runAsync(Runnable runnable) {
        return asyncRunStage(asyncPool, runnable);
    }
    
    public static CompletableFuture<Void> runAsync(Runnable runnable,
                                                   Executor executor) {
        return asyncRunStage(screenExecutor(executor), runnable);
    }
    
  • supplyAsync 有 返回值如下:

    public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier) {
        return asyncSupplyStage(asyncPool, supplier);
    }
    
    public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier,
                                                       Executor executor) {
        return asyncSupplyStage(screenExecutor(executor), supplier);
    }
    

上述Executor executor参数说明:

  • 没有指定Executor的方法,直接使用默认的ForkJoinPool.commonPool() 作为它的线程池执行异步代码。

  • 如果指定线程池,则使用我们自定义的或者特别指定的线程池执行异步代码。

3.3 代码演示

入门代码如下:

public class CompletableFutureDemo1 {
    public static void main(String[] args) throws Exception{
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 20, 1L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(50), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());

        CompletableFuture<Void> future1 = CompletableFuture.runAsync(() -> {
            System.out.println(Thread.currentThread().getName());
        });
        System.out.println(future1.get());

        CompletableFuture<Void> future2 = CompletableFuture.runAsync(() -> {
            System.out.println(Thread.currentThread().getName());
        },threadPoolExecutor);
        System.out.println(future2.get());

        CompletableFuture<Integer> future3 = CompletableFuture.supplyAsync(() -> {
            System.out.println(Thread.currentThread().getName());
            return 11;
        });
        System.out.println(future3.get());

        CompletableFuture<Integer> future4 = CompletableFuture.supplyAsync(() -> {
            System.out.println(Thread.currentThread().getName());
            return 12;
        }, threadPoolExecutor);
        System.out.println(future4.get());
    }
}

运行结果如下:

ForkJoinPool.commonPool-worker-9【使用默认线程池】
null【无返回值】
pool-1-thread-1 【使用自定义线程池】
null【无返回值】 
ForkJoinPool.commonPool-worker-9【使用默认线程池】
11【有返回值】
pool-1-thread-1【使用自定义线程池】
12【有返回值】

上述Executor executor参数说明

  • 没有指定Executor的方法,直接使用默认的ForkJoinPool.commonPool() 作为它的线程池执行异步代码。
  • 如果指定线程池,则使用我们自定义的或者特别指定的线程池执行异步代码

从Java8开始引入了CompletableFuture,它是Future的功能增强版,可以传入回调对象,当异步任务完成或者发生异常时,自动调用回调对象的回调方法。Code之通用演示,减少阻塞和轮询

public class cfuture4 {
    public static void main(String[] args) throws Exception {
        CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
            System.out.println(Thread.currentThread().getName() + "\t" + "-----come in");
            int result = ThreadLocalRandom.current().nextInt(10);
            //暂停几秒钟线程
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println("-----计算结束耗时1秒钟,result: "+result);
            if(result > 6)
            {
                int age = 10/0;
            }
            return result;
        }).whenComplete((v,e) ->{
            if(e == null)
            {
                System.out.println("-----result: "+v);
            }
        }).exceptionally(e -> {
            System.out.println("-----exception: "+e.getCause()+"\t"+e.getMessage());
            return -44;
        });

        //主线程不要立刻结束,否则CompletableFuture默认使用的线程池会立刻关闭:暂停3秒钟线程
        try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
    }
}

CompletableFuture的优点

  • 异步任务结束时,会自动回调某个对象的方法;
  • 异步任务出错时,会自动回调某个对象的方法;
  • 主线程设置好回调后,不再关心异步任务的执行,异步任务之间可以顺序执行

4. 案例精讲-从电商网站的比价需求

面试题:说说你过去工作中的项目亮点? 可以说说这个比价需求

经常出现在等待某条 SQL 执行完成后,再继续执行下一条 SQL ,而这两条 SQL 本身是并无关系的,可以同时进行执行的。我们希望能够两条 SQL 同时进行处理,而不是等待其中的某一条 SQL 完成后,再继续下一条。同理,对于分布式微服务的调用,按照实际业务,如果是无关联step by step的业务,可以尝试是否可以多箭齐发,同时调用。

我们去比同一个商品在各个平台上的价格,要求获得一个清单列表,

    1. step by step,查完京东查淘宝,查完淘宝查天猫......
    1. all 一口气同时查询。。。。。

代码演示如下:

/**
 *
 * 案例说明:电商比价需求
 * 1 同一款产品,同时搜索出同款产品在各大电商的售价;
 * 2 同一款产品,同时搜索出本产品在某一个电商平台下,各个入驻门店的售价是多少
 *
 * 出来结果希望是同款产品的在不同地方的价格清单列表,返回一个List<String>
 * 《mysql》 in jd price is 88.05
 * 《mysql》 in pdd price is 86.11
 * 《mysql》 in taobao price is 90.43
 *
 * 3 要求深刻理解
 *   3.1 函数式编程
 *   3.2 链式编程
 *   3.3 Stream流式计算
 */
public class CompletableFutureNetMallDemo {
    //各大平台
    static List<NetMall> list = Arrays.asList(
            new NetMall("jd"),
            new NetMall("pdd"),
            new NetMall("taobao"),
            new NetMall("dangdangwang"),
            new NetMall("tmall")
    );

    //同步 ,step by step

    /**
     * List<NetMall>  ---->   List<String>
     * @param list
     * @param productName
     * @return
     */
    public static List<String> getPriceByStep(List<NetMall> list,String productName)
    {
        return list
                .stream().
                map(netMall -> String.format(productName + " in %s price is %.2f", netMall.getMallName(), netMall.calcPrice(productName)))
                .collect(Collectors.toList());
    }
    //异步 ,多箭齐发

    /**
     * List<NetMall>  ---->List<CompletableFuture<String>> --->   List<String>
     * @param list
     * @param productName
     * @return
     */
    public static List<String> getPriceByASync(List<NetMall> list,String productName)
    {
        return list
                .stream()
                .map(netMall -> CompletableFuture.supplyAsync(() -> String.format(productName + " is %s price is %.2f", netMall.getMallName(), netMall.calcPrice(productName))))
                .collect(Collectors.toList())
                .stream()
                .map(CompletableFuture::join)
                .collect(Collectors.toList());
    }

    public static void main(String[] args)
    {
        long startTime = System.currentTimeMillis();
        List<String> list1 = getPriceByStep(list, "mysql");
        for (String element : list1) {
            System.out.println(element);
        }
        long endTime = System.currentTimeMillis();
        System.out.println("----costTime: "+(endTime - startTime) +" 毫秒");

        System.out.println();

        long startTime2 = System.currentTimeMillis();
        List<String> list2 = getPriceByASync(list, "mysql");
        for (String element : list2) {
            System.out.println(element);
        }
        long endTime2 = System.currentTimeMillis();
        System.out.println("----costTime: "+(endTime2 - startTime2) +" 毫秒");

    }
}

class NetMall
{
    @Getter
    private String mallName;//商场名字 jd pdd,taobao

    public NetMall(String mallName)
    {
        this.mallName = mallName;
    }

    public double calcPrice(String productName)
    {
        //检索需要1秒钟
        try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
        return ThreadLocalRandom.current().nextDouble() * 2 + productName.charAt(0);
    }
}

4. CompletableFuture常用方法

4.1 获得结果和触发计算

4.1.1 获取结果

  • 不见不散:public T get()

  • 过时不候:public T get(long timeout,TimeUnit unit)

  • public T getNow(T valueIfAbsent):没有计算完成的情况下,给我一个替代结果,即立即获取结果不阻塞。计算完,返回计算完成后的结果,没算完,返回设定的valueIfAbsent值。代码演示如下:

    public class CompletableFutureDemo2 {
        public static void main(String[] args) throws ExecutionException, InterruptedException {
            CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
                try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
                return 533;
            });
    
            //去掉注释上面计算没有完成,返回444
            //开启注释上满计算完成,返回计算结果
            try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
    
            //如果上面睡眠两秒则可以立即获得结果,如果没有睡眠两秒则直接返回444
            System.out.println(completableFuture.getNow(444));
        }
    }
    
  • public T join(),代码演示如下:

    public class CompletableFutureDemo2 {
        public static void main(String[] args) throws ExecutionException, InterruptedException {
            System.out.println(CompletableFuture.supplyAsync(() -> "abc").thenApply(r -> r + "123").join());
        }
    }
    

4.1.2 主动触发计算

  • public boolean complete(T value) 是否打断get方法立即返回括号值,即如果此时能够获得计算结果,则打断失败,complete方法返回false,CompletableFuture中的get()方法返回计算结果,反之如果此时不能够获得计算结果,则打断成功,complete方法返回true,CompletableFuture中的get()方法返回complete(T value)中参数value的值,代码演示如下:

    public class CompletableFutureDemo2 {
        public static void main(String[] args) throws ExecutionException, InterruptedException {
            CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
                try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
                return 533;
            });
    
            //注释掉暂停线程,get还没有算完只能返回complete方法设置的444;暂停2秒钟线程,异步线程能够计算完成返回get
            try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
    
            //当调用CompletableFuture.get()被阻塞的时候,complete方法就是结束阻塞并get()获取设置的complete里面的值.
        System.out.println(completableFuture.complete(444)+"\t"+completableFuture.get());
        }
    }
    

4.2 对计算结果进行处理

  • thenApply:计算结果存在依赖关系,这两个线程串行化,上一步的计算结果作为下一步的参数,如果当前步骤出现了异常则就停留在哪步,不会继续往下执行了,代码演示如下:
public class CompletableFutureDemo2 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
    //当一个线程依赖另一个线程时用 thenApply 方法来把这两个线程串行化,
    CompletableFuture.supplyAsync(() -> {
        //暂停几秒钟线程
        try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println("111");
        return 1024;
    }).thenApply(f -> {
        System.out.println("222");
        return f + 1;
    }).thenApply(f -> {
        //int age = 10/0; // 异常情况:那步出错就停在那步。
        System.out.println("333");
        return f + 1;
    }).whenCompleteAsync((v,e) -> {
        System.out.println("*****v: "+v);
    }).exceptionally(e -> {
        e.printStackTrace();
        return null;
    });

    System.out.println("-----主线程结束,END");

    // 主线程不要立刻结束,否则CompletableFuture默认使用的线程池会立刻关闭:
    try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
}
}
  • handle:跟thenApply不同,有异常也可以往下一步走,根据带的异常参数可以进一步处理。代码演示如下:
public class CompletableFutureDemo2 {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //当一个线程依赖另一个线程时用 handle 方法来把这两个线程串行化,
        
        CompletableFuture.supplyAsync(() -> {
            //暂停几秒钟线程
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println("111");
            return 1024;
        }).handle((f,e) -> {
            // 异常情况:有异常也可以往下一步走,根据带的异常参数可以进一步处理
            int age = 10/0;
            System.out.println("222");
            return f + 1;
        }).handle((f,e) -> {
            System.out.println("333");
            return f + 1;
        }).whenCompleteAsync((v,e) -> {
            System.out.println("*****v: "+v);
        }).exceptionally(e -> {
            e.printStackTrace();
            return null;
        });

        System.out.println("-----主线程结束,END");

        // 主线程不要立刻结束,否则CompletableFuture默认使用的线程池会立刻关闭:
        try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
    }
}

总结:
在这里插入图片描述

whenCompletewhenCompleteAsync的区别:

  • whenComplete:是执行当前任务的线程执行继续执行whenComplete的任务。
  • whenCompleteAsync:是执行把whenCompleteAsync这个任务继续提交给线程池来进行执行。

4.3 对计算结果进行消费

  • thenAccept(Consumer<? super T> action):接收任务的处理结果,并消费处理,无返回结果。代码演示如下:
public static void main(String[] args) throws ExecutionException, InterruptedException
{
    CompletableFuture.supplyAsync(() -> {
        return 1;
    }).thenApply(f -> {
        return f + 2;
    }).thenApply(f -> {
        return f + 3;
    }).thenApply(f -> {
        return f + 4;
    }).thenAccept(r -> System.out.println(r));
}

补充:Code之任务之间的顺序执行: ①thenRun:thenRun(Runnable runnable),任务 A 执行完执行 B,并且 B 不需要 A 的结果

  • thenAccept:thenAccept(Consumer action),任务 A 执行完执行 B,B 需要 A 的结果,但是任务 B 无返回值

  • thenApply:thenApply(Function fn),任务 A 执行完执行 B,B 需要 A 的结果,同时任务 B 有返回值

    代码演示如下:

    System.out.println(CompletableFuture.supplyAsync(() -> "resultA").thenRun(() -> {}).join());
    
    
    System.out.println(CompletableFuture.supplyAsync(() -> "resultA").thenAccept(resultA -> {}).join());
    
    
    System.out.println(CompletableFuture.supplyAsync(() -> "resultA").thenApply(resultA -> resultA + " resultB").join());
    

4.4 对计算速度进行选用

  • applyToEither:谁快用谁,代码演示如下:
public static void m4() {
        System.out.println(CompletableFuture.supplyAsync(() -> {
            //暂停几秒钟线程
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return 1;
        }).applyToEither(CompletableFuture.supplyAsync(() -> {
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return 2;
        }), r -> {
            //r为执行最快的结果,按照上面当然是第一个执行快,因为只是暂停了一秒钟,
            //所以最终r的值为1,而不是2
            return r;
        }).join());

        //暂停几秒钟线程
        try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
    }

4.5 对计算结果进行合并

  • thenCombine:两个CompletionStage任务都完成后,最终能把两个任务的结果一起交给thenCombine 来处理,先完成的先等着,等待其它分支任务

代码演示如下:

public static void m5() {
        System.out.println(CompletableFuture.supplyAsync(() -> {
            return 10;
        }).thenCombine(CompletableFuture.supplyAsync(() -> {
            return 20;
        }), (r1, r2) -> {
            //返回10+20
            return r1 + r2;
        }).join());
    }
public class CompletableFutureDemo2 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<Integer> thenCombineResult = CompletableFuture.supplyAsync(() -> {
            System.out.println(Thread.currentThread().getName() + "\t" + "---come in 1");
            return 10;
        }).thenCombine(CompletableFuture.supplyAsync(() -> {
            System.out.println(Thread.currentThread().getName() + "\t" + "---come in 2");
            return 20;
        }), (x,y) -> {
            System.out.println(Thread.currentThread().getName() + "\t" + "---come in 3");
            return x + y;
        }).thenCombine(CompletableFuture.supplyAsync(() -> {
            System.out.println(Thread.currentThread().getName() + "\t" + "---come in 4");
            return 30;
        }),(a,b) -> {
            System.out.println(Thread.currentThread().getName() + "\t" + "---come in 5");
            return a + b;
        });
        System.out.println("-----主线程结束,END");
        System.out.println(thenCombineResult.get());


        // 主线程不要立刻结束,否则CompletableFuture默认使用的线程池会立刻关闭:
        try { TimeUnit.SECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); }
    }
}

参考资料

Java并发编程知识体系
Java并发编程的艺术
Java多线程编程核心技术
Java并发实现原理 JDK源码剖析