在知乎上看到一道Java多线程
的笔试题,心想我多线程只会Thread
和Runable
,在写WebFluxTest
用过一次CountDownLatch
,除此之外还没怎么去看cocurrent
包下的类,所以就想试试。
知乎传送门:某大型电商Java面试题:一主多从多线程协作
客户请求下单服务(OrderService),服务端会验证用户的身份(RemotePassportService),用户的银行信用(RemoteBankService),用户的贷款记录(RemoteLoanService)。为提高并发效率,要求三项服务验证工作同时进行,如其中任意一项验证失败,则立即返回失败,否则等待所有验证结束,成功返回
。要求Java实现。
知乎中使用CountDownLatch
解决了这个问题,不过CountDownLatch
本来就是用来解决异步转同步
问题的,不过作者给的代码还是感觉不太优雅
,评论中提到了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
,他同时实现了Future
和Runable
两个接口,然后有两个构造方法:
public FutureTask(Runnable runnable, V result)
public FutureTask(Callable callable)
因为用来管理多线程的ThreadPoolExecutor
只能execute
实现Runable
的类,所以到这里已经有个大概的思路了,那么就开始撸代码。
说到多线程,那么肯定得要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);
RemotePassportService
:Thread.sleep(3000);
RemoteBankService
: Thread.sleep(5000);
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
的抽象类,由于三个线程返回的都是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
,并且要设计两种状态:
三个线程都成功,返回成功
其中一个失败,终止其他两个线程,返回失败
那么设置countdown
次数为3次
即可:
成功就正常的,每个线程countdown
一次
失败就countdown
三次,强行结束
CountDownLatch latch = new CountDownLatch(taskNumber);
在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
中写好对应逻辑:
finally
中countdown
一次afterFail
去countdown
两次,并且在finally
中countdown
一次 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