从理解Future模式到仿写JUC的Future模式

    技术2022-07-11  92

    1、Future模式

    过生日,在线上定蛋糕的例子可以形象的解释Future模式。

    两个步骤: 1、下单 - 委托制作蛋糕:你在线定下单订蛋糕,将制作蛋糕的工作委托给了蛋糕店。 2、取蛋糕:凭着在线支付的凭证取蛋糕

    Future模式的本质就是将耗时操作委托给其他线程,主线程快速获取Future,后续获取执行结果。

    NO.Future模式订蛋糕1耗时操作制作蛋糕2其他线程蛋糕店3主线程订蛋糕的客户4Future支付凭证5执行结果蛋糕

    我总结了一下Future中的各个角色,如下图

    注意: Future模式是需要拿到执行结果的,如果不需要拿到执行结果,程序就会很简单,直接开个线程去执行耗时操作就可以了。

    1.1、Future模式前生

    下面是拿不到耗时执行结果的程序,比较简单

    public class MakeCakeTest { public static void main(String[] args) { Customer dougLea = new Customer("Doug Lea"); new Thread(new Runnable() { public void run() { new CakeRoom().make();// make很耗时,开一个线程处理 } }).start(); dougLea.doOtherThing(); } } class Customer { String name; Customer(String name){ this.name = name; } public void doOtherThing() { System.out.println("doOtherThing...."); ThreadUtils.sleep(CakeConstant.OTHER_THING_SPEND); } public void eatCake(Cake cake){ System.out.println(this.name+" 吃着美味的蛋糕: "+cake); } @Override public String toString() { return name; } } class CakeRoom { public Cake make(String cakeName) { ThreadUtils.sleep(CakeConstant.MAKE_CAKE_SPEND); return new Cake(cakeName); } } public class CakeConstant { static long OTHER_THING_SPEND = 1000L; static long MAKE_CAKE_SPEND = 5000L; }

    程序比较简单,但做了蛋糕却拿不到,这不是很坑爹么。如果想拿到蛋糕,我们可以修改一下程序:

    public static void main(String[] args) { Customer dougLea = new Customer("Doug Lea"); final Cake[] cake = new Cake[1]; new Thread(new Runnable() { @Override public void run() { cake[0] = new CakeRoom().make(); } }).start(); dougLea.doOtherThing(); dougLea.eatCake(cake[0]); }

    但上面的程序是有问题的,我们来看看输出结果:

    doOtherThing… Doug Lea 吃着美味的蛋糕: null

    拿到的蛋糕是空的(null),大家是不是已经发现了问题所在,制作蛋糕太慢了,所以拿到的是空的,我们再修改一下程序

    public static void main(String[] args) { Customer dougLea = new Customer("Doug Lea"); final Cake[] cake = new Cake[1]; new Thread(new Runnable() { @Override public void run() { cake[0] = new CakeRoom().make(); } }).start(); dougLea.doOtherThing(); while (true) { Cake cake1 = cake[0]; if (cake1 == null) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } else { dougLea.eatCake(cake1); break; } } }

    程序可以正常输出了。

    这么写程序也未尝不可,但是我们的客户端需要处理很多与业务无关的逻辑,客户端程序(MakeCakeTest )变得复杂。

    1.2、Future模式

    其实我们理想的客户端程序应该不关心业务无关的代码,理想客户端程序如下:

    public class MakingCakeFutureTest { public static void main(String[] args) { Customer customer = new Customer("Doug Lea"); Voucher voucher = customer.orderCake("chocolate cake");// 下单订蛋糕,拿到下单凭证 customer.doOtherThing(); customer.eatCake(voucher.getCake());// 凭着下单凭证取蛋糕,吃蛋糕 } }

    上面的客户端程序只需要关注订蛋糕、拿蛋糕、吃蛋糕这些具体的业务,不需要自己处理与业务无关的 “等待” 操作。

    我们参照先前总结的Future模式结构图,尝试把业务无关的逻辑封装起来,客户端程序只需要关心业务逻辑。

    为了方便大家复制运行程序,下面给出了完整的程序

    class Customer { String name; Customer(String name){ this.name = name; } public Voucher orderCake(String cakeName) { Voucher voucher = new Voucher(); CakeRoom cakeRoom = new CakeRoom(); new Thread(new Runnable() { public void run() { voucher.setCake(cakeRoom.make(cakeName)); } }).start(); return voucher; } public void doOtherThing() { System.out.println("doOtherThing...."); ThreadUtils.sleep(CakeConstant.OTHER_THING_SPEND); } public void eatCake(Cake cake){ System.out.println(this.name+" 吃着美味的蛋糕: "+cake); } @Override public String toString() { return name; } } class Voucher { Cake cake; boolean isCakeReady = false; public synchronized void setCake(Cake cake) { this.cake = cake; isCakeReady = true; notifyAll(); } public synchronized Cake getCake(){ if(!isCakeReady){ try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } return cake; } } class CakeRoom { public Cake make(String cakeName) { ThreadUtils.sleep(CakeConstant.MAKE_CAKE_SPEND); return new Cake(cakeName); } } public class Cake { String name; public Cake(String name) { this.name = name; } @Override public String toString() { return name; } }

    Future模式关键代码在 Voucher 中,在蛋糕未做好的情况下需要等待 wait(),做好了以后 notifyAll()。

    这里说一句题外话,文中的图是我学习Future模式中总结出来的,大家在学习过程中也应该不断的总结和抽象,形成自己的方法论。不知道大家看了图是什么感觉,反正我总结出来这个图以后,写Future模式的程序觉得得心应手。

    我们可以将上面的代码进行进一步抽象并进行总结

    1.3、通用Futrue模式

    参考上面的图,尝试实现一下通用的Future模式,理解了上面的内容,实现起来没那么难的,使用了通用的Future模式,在开发中就不需要自己再去实现一套了。

    客户端程序如下:

    public class ThreadPoolMakeCakeTest { public static void main(String[] args){ ThreadPoolExecutor_<Cake> executor = new ThreadPoolExecutor_(); Customer doug_lea = new Customer("Doug Lea"); Future_<Cake> cakeFuture = executor.submit(new Callable<Cake>() { public Cake call() throws Exception { return new CakeRoom().make("chocolate cake"); } }); doug_lea.doOtherThing(); Cake cake = cakeFuture.get(); doug_lea.eatCake(cake); } } public class ThreadPoolExecutor_<V> { ConcurrentLinkedQueue<Future_<V>> futureClq = new ConcurrentLinkedQueue(); ConcurrentLinkedQueue<Thread> freeThreads = new ConcurrentLinkedQueue(); ConcurrentLinkedQueue<Thread> busyThreads = new ConcurrentLinkedQueue(); AtomicInteger threadSize = new AtomicInteger(0); public Future_<V> submit(Callable<V> callable) { FutureTask_ future = new FutureTask_(); new Thread(new Runnable() { public void run() { try { V v = callable.call(); future.setResult(v); freeThreads.add(Thread.currentThread()); } catch (Exception e) { e.printStackTrace(); } } }).start(); return future; } private Thread getThread() { Thread thread = freeThreads.poll(); if(thread==null){ thread = new Thread(); busyThreads.add(thread); threadSize.incrementAndGet(); } return thread; } } public interface Future_<V> { boolean cancel(); V get(); } public class FutureTask_<V> implements Future_<V> { private V result; private volatile boolean isResultReady = false; synchronized void setResult(V result){ this.result = result; isResultReady = true; notifyAll(); } @Override public boolean cancel() { return false; } @Override public synchronized V get() { if(!isResultReady){ try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } return result; } }
    1.4、总结

    正在看懂了上面的图,就可以理解Future模式,所以大家一定要记住上面的图。

    Future模式使用场景的几个关键点:1、需要快速响应;2、有耗时的任务;3、需要拿到耗时任务的结果;

    2、多线程如何提升系统吞吐量

    我们看看下面的8个任务,白色部分为CPU执行、灰色部分为IO,假设每个任务执行200ms = CPU执行100ms + IO执行100ms

    2.1、串行执行

    在串行执行8个任务的情况下,系统的响应时间1600ms,吞吐率 = 8/1600ms = 0.5任务/100ms

    2.2、并行执行

    在并行执行8个任务的情况下,系统的响应时间800ms,吞吐率 =8/(800*2)ms= 0.5任务/100ms 。 响应时间变少了,但吞吐率没有变

    2.2、Future模式并行执行

    在Future模式并行执行8个任务的情况下,系统的响应时间500ms,吞吐率 = 8/500*2 = 0.8任务/100ms ,提升了60%

    Processed: 0.015, SQL: 9