前言
我们都知道要想提高系统的吞吐量,就一定离不开异步编程的思想,无论是平时项目中接触到的MQ、响应式编程还是操作系统里的AIO、epoll本质上都是在利用异步的方式,提升系统的并行度,本文通过一个简单的小案例来看看如何利用Java中的Futrue完成从同步到异步的转变。
场景介绍
我们举一个简单的小场景,用户购物,假设就分为三步:1、查看库存是否充足 2、下单支付 3、扣减库存
第一版
这是一种典型的同步阻塞流程,整个流程执行时间等于所有流程的总和。
/**
* DEMO01:完全基于同步阻塞的方式完成
*/
public class Demo_01 {
public static void main(String[] args) {
long s = System.currentTimeMillis();
// 查询库存是否充足
checkStoreEnough();
// 生成订单
createOrder();
// 扣减库存
updateStore();
long e = System.currentTimeMillis();
System.out.println("耗时:" + (e - s));
}
private static void checkStoreEnough() {
System.out.println("查询库存是否充足");
ThreadUtils.sleep(1000);
}
private static void createOrder() {
System.out.println("生成订单");
ThreadUtils.sleep(2000);
}
private static void updateStore() {
System.out.println("更新库存");
ThreadUtils.sleep(1000);
}
}
第二版
对于第一版的存在的问题,我们很快想到了使用多线程的方式,这样整个流程的执行时间就等于子流程中执行时间最长的。
/**
* DEMO02: 开始基于多线程的方式,整个过程都异步化,但是并不满足业务上的要求,库存充足与生成订单应该有依赖关系,只当库存充足时才能生成订单
*
* 为了演示方式,没有用线程池
*/
public class Demo_02 {
public static void main(String[] args) throws InterruptedException {
final CountDownLatch countDownLatch = new CountDownLatch(3);
long s = System.currentTimeMillis();
// 查询库存是否充足
new Thread(() -> {
checkStoreEnough();
countDownLatch.countDown();
}).start();
// 生成订单
new Thread(() -> {
createOrder();
countDownLatch.countDown();
}).start();
// 扣减库存
new Thread(() -> {
updateStore();
countDownLatch.countDown();
}).start();
countDownLatch.await();
long e = System.currentTimeMillis();
System.out.println("耗时:" + (e - s));
}
private static void checkStoreEnough() {
System.out.println("查询库存是否充足");
ThreadUtils.sleep(1000);
}
private static void createOrder() {
System.out.println("生成订单");
ThreadUtils.sleep(2000);
}
private static void updateStore() {
System.out.println("更新库存");
ThreadUtils.sleep(1000);
}
}
第三版
第二版虽然通过多线程的方式实现了流程异步化,但却不能满足业务上要求,能够生成订单应该要根据前一步查询库存是否充足的结果来判断,如果库存足够才会生成订单,否则就不生成订单。如果我们引入了Future,通过get方法,等待结果的返回。
/**
* DEMO03: 使用Future的方式,在Future.get()的时候阻塞等待返回结果,但似乎又回到了同步模式
*/
public class Demo_03 {
public static void main(String[] args) throws Exception {
final CountDownLatch countDownLatch = new CountDownLatch(2);
long s = System.currentTimeMillis();
// 查询库存是否充足
Callable<Integer> ca1 = () -> {
checkStoreEnough();
return 1;
};
FutureTask<Integer> ft1 = new FutureTask<>(ca1);
new Thread(ft1).start();
// 库存充足,调用生成订单的方法
if (ft1.get() == 1) {
new Thread(() -> {
// 生成订单
createOrder();
countDownLatch.countDown();
}).start();
new Thread(() -> {
// 扣减库存
updateStore();
countDownLatch.countDown();
}).start();
countDownLatch.await();
long e = System.currentTimeMillis();
System.out.println("耗时:" + (e - s));
}
}
private static void checkStoreEnough() {
System.out.println("查询库存是否充足");
ThreadUtils.sleep(1000);
}
private static void createOrder() {
System.out.println("生成订单");
ThreadUtils.sleep(2000);
}
private static void updateStore() {
System.out.println("更新库存");
ThreadUtils.sleep(1000);
}
}
第四版
假设我现在第三步更新库存也要依赖生成订单成功,那就变成了现在这样,你会发现我们用到future,但依然是同步阻塞的模式。
/**
* DEMO04:如果更新库存也依赖生成订单,也要等待future.get()方法,那就完全回到了同步阻塞的方式。
*/
public class Demo_04 {
public static void main(String[] args) throws Exception {
long s = System.currentTimeMillis();
// 查询库存是否充足
Callable<Integer> c1 = () -> {
checkStoreEnough();
return 1;
};
FutureTask<Integer> f1 = new FutureTask<>(c1);
new Thread(f1).start();
// 库存充足,调用生成订单的方法
if (f1.get() == 1) {
Callable<Integer> c2 = () -> {
createOrder();
return 1;
};
FutureTask<Integer> f2 = new FutureTask<>(c2);
new Thread(f2).start();
// 生成订单成功,调用更新库存的方法
if (f2.get() == 1) {
updateStore();
}
}
long e = System.currentTimeMillis();
System.out.println("耗时:" + (e - s));
}
private static void checkStoreEnough() {
System.out.println("查询库存是否充足");
ThreadUtils.sleep(1000);
}
private static void createOrder() {
System.out.println("生成订单");
ThreadUtils.sleep(2000);
}
private static void updateStore() {
System.out.println("更新库存");
ThreadUtils.sleep(1000);
}
}
第五版
我们在主线程开始就让整个流程异步出去,基于一种通知、回调的思想,过程中的每一步都以通知的方式通知下游接口,上下游之间没有强依赖的关系,这样就使的主流程(也就是接用户请求的入口)不受限于每一步的处理时间。
/**
* DEMO05:主线程异步处理,相当于发出一个通知后,对于主线程而言就结束了,接下来的事情让各个消费者去消费就行了。
* 之后每一步过程也是如此,从局部看每一步耗时并不会减少,但从整体上看,整个过程的吞吐量将会大大提升。
*/
public class Demo_05 {
public static void main(String[] args) {
long s = System.currentTimeMillis();
// 主线程收到请求后,执行异步出去,对于主线程来说就结束了。
new Thread(() -> checkStoreEnough()).start();
long e = System.currentTimeMillis();
System.out.println("耗时:" + (e - s));
}
private static void checkStoreEnough() {
Callable<Integer> c1 = () -> {
System.out.println("查询库存是否充足");
ThreadUtils.sleep(1000);
return 1;
};
FutureTask<Integer> f1 = new FutureTask<>(c1);
new Thread(f1).start();
// 将Future传给生成订单的逻辑,自己就结束了
createOrder(f1);
}
private static void createOrder(FutureTask<Integer> f1) {
try {
if (f1.get() == 1) {
Callable<Integer> c2 = () -> {
System.out.println("生成订单");
ThreadUtils.sleep(2000);
return 1;
};
FutureTask<Integer> f2 = new FutureTask<>(c2);
new Thread(f2).start();
// 将Future传给更新库存的逻辑,自己就结束了
updateStore(f2);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private static void updateStore(FutureTask<Integer> f2) {
try {
if (f2.get() == 1) {
System.out.println("更新库存");
ThreadUtils.sleep(1000);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
总结
第五版的功能实现只是为了提现异步的实现逻辑,对于先判断库存再生成订单再更新库存肯定会出现问题,但是不影响异步化的思想。比如我们可以先创建一个预生成的订单,然后判断库存充足后,再通过乐观锁的方式更新库存,最后确认库存更新成功后再通知订单更改状态。
总之,通过异步的方式不但能够大大提升系统的整体吞吐量,还能实现业务的解耦,当我们遇到类似上面这样的业务场景,应该要有异步化的思维方式。