Java多线程实战:FutureTask与CountDownLatch的完美结合

前提概要

在知乎上看到一道Java多线程的笔试题,心想我多线程只会ThreadRunable,在写WebFluxTest用过一次CountDownLatch,除此之外还没怎么去看cocurrent包下的类,所以就想试试。

题目

知乎传送门:某大型电商Java面试题:一主多从多线程协作

客户请求下单服务(OrderService),服务端会验证用户的身份(RemotePassportService),用户的银行信用(RemoteBankService),用户的贷款记录(RemoteLoanService)。为提高并发效率,要求三项服务验证工作同时进行,如其中任意一项验证失败,则立即返回失败,否则等待所有验证结束,成功返回。要求Java实现。

原解法反思

知乎中使用CountDownLatch解决了这个问题,不过CountDownLatch本来就是用来解决异步转同步问题的,不过作者给的代码还是感觉不太优雅,评论中提到了Future这个类,所以我就以Future为起点开始了探索。

探索Future

那么一开始,我肯定是去找找Future这个类啦,然后就查到类的类定义。

public interface Future<V>{
    boolean cancel(boolean mayInterruptIfRunning);
    boolean isCancelled();
    boolean isDone();
    V get();
    V get(long timeout, TimeUnit unit)
}

这样一看,原来只是一个接口,不过确实使用这个接口会是一个不错的选择,类似Callable的升级版,可以定义返回值,更重要是有cancel方法,那么终止进程的方法就有了。

Future有一个实现类:FutureTask,他同时实现了FutureRunable两个接口,然后有两个构造方法:

public FutureTask(Runnable runnable, V result)
public FutureTask(Callable callable)

因为用来管理多线程的ThreadPoolExecutor只能execute实现Runable的类,所以到这里已经有个大概的思路了,那么就开始撸代码。

思路

ThreadPoolExecutor

说到多线程,那么肯定得要ThreadPoolExecutor来管理多线程,然后使用LinkedBlockingQueue保存线程,那么我们就在main里new一个备用。

ThreadPoolExecutor executor = new ThreadPoolExecutor(poolSize,maxPoolSize,
                1,TimeUnit.MINUTES, new LinkedBlockingQueue<>(),new MyThreadFactory());

定义三个服务类

public class RemoteLoanService {

    public boolean checkAuth(int uid){
        boolean flag;

        System.out.println("不良贷款 - 验证开始");
        try {
            Thread.sleep(1000);
            // 这里让时间最短的直接失败,方便查看测试结果
            // flag = new Random().nextBoolean();
            flag = false;
        } catch (InterruptedException e) {
            System.out.println("不良贷款 - 验证终止");
            return false;
        }

        if(flag){
            System.out.println("不良贷款 - 验证成功");
            return true;
        }
        else {
            System.out.println("不良贷款 - 验证失败");
            return false;
        }
    }
}

其他两个类除了flag是随机数生成的,然后sleep时间不一样,其他都大同小异。

  • RemoteLoanService : Thread.sleep(1000);

  • RemotePassportServiceThread.sleep(3000);

  • RemoteBankServiceThread.sleep(5000);

CheckThread和三个实现类

public abstract class BaseCheckThread implements Callable<Boolean>{

    protected final int uid;

    public BaseCheckThread(int uid){
        this.uid = uid;
    }

    public int getUid() {
        return uid;
    }
}

由于我决定使用FutureTask(Callable callable)作为构造器,那么就要定义一个Callable的抽象类,由于三个线程返回的都是boolean,那么直接确定类型即可。

public class RemoteLoanThread extends BaseCheckThread {

    public RemoteLoanThread(int uid) {
        super(uid);
    }

    @Override
    public Boolean call() throws Exception {
        return new RemoteLoanService().checkAuth(uid);
    }
}

还有2个大同小异的BaseCheckThread子类,这里也省略。

设计与CountDownLatch

由于要实现异步转同步,那么肯定要用CountDownLatch,并且要设计两种状态:

  1. 三个线程都成功,返回成功

  2. 其中一个失败,终止其他两个线程,返回失败

那么设置countdown次数为3次即可:

  1. 成功就正常的,每个线程countdown一次

  2. 失败就countdown三次,强行结束

CountDownLatch latch = new CountDownLatch(taskNumber);

CheckFutureTask

FutureTask中有一个空的done方法:

    /**
     * Protected method invoked when this task transitions to state
     * {@code isDone} (whether normally or via cancellation). The
     * default implementation does nothing.  Subclasses may override
     * this method to invoke completion callbacks or perform
     * bookkeeping. Note that you can query status inside the
     * implementation of this method to determine whether this task
     * has been cancelled.
     */
    protected void done() { }

看下注释就懂,当状态变为isDone时自动调用,那么肯定是我们最佳的countdown时间了。

由于三个线程大同小异,那么最好是再写一个CheckFutureTask

public class CheckFutureTask extends FutureTask<Boolean>{

    private volatile CountDownLatch latch;

    private final int number;

    public CheckFutureTask(BaseCheckThread checkThread, CountDownLatch latch, int number) {
        super(checkThread);
        this.latch = latch;
        this.number = number;
    }

    @Override
    protected void done() {
        try {
            if(!get()){
                afterFail();
            }
        } catch (Exception e) {
            afterFail();
        } finally {
            latch.countDown();
        }
    }

    /**
     * 在失败后调用
     */
    private void afterFail(){
        for(int i = 0 ; i < number - 1 ; i++){
            latch.countDown();
        }
    }
}

在这里,我将CountDownLatch作为成员变量传入各个线程中,用于countdown,然后在done中写好对应逻辑:

  1. 成功:在finallycountdown一次
  2. 失败:调用afterFailcountdown两次,并且在finallycountdown一次

核心:跳回主线程后调用cancel

        for(FutureTask task : tasks){
            executor.execute(task);
        }
        try {
            latch.await();

            for(FutureTask task : tasks){
                task.cancel(true);
            }

            for(FutureTask task : tasks){
                if(!task.get()){
                    return false;
                }
            }

        } catch (Exception e) {
            return false;
        }

        return true;

execute之后,调用latch.await()使主线程等待,当countdown结束后,不管线程是否还在运作,都调用cancel方法停止所有线程,这点就是问题的核心

如果没有线程被强制停止等等异常的话,就不会被try-catch处理,那么肯定是验证成功,其他情况都是验证失败

运行结果

黑名单 - 验证开始
不良贷款 - 验证开始
银行信用 - 验证开始
不良贷款 - 验证失败
银行信用 - 验证终止
验证失败
黑名单 - 验证终止

至此,整个解题完毕,学到了不少Java多线程的知识,感谢这道题。

完整源码:ThreadPoolExecutor_Learn

你可能感兴趣的:(Java多线程实战:FutureTask与CountDownLatch的完美结合)