• 设计模式:基于线程池的并发Visitor模式


    1.前言

    第二篇设计模式的文章我们谈谈Visitor模式。

    当然,不是简单的列个的demo,我们以电商网站中的购物车功能为背景,使用线程池实现并发的Visitor模式,并聊聊其中的几个关键点。

    • 一,基于线程池的实现并发Visitor模式。
    • 二,讨论下并发场景下的一些细节处理。
    • 三,用模拟数据测试并做补充说明。

    2.背景

    当从网站的某个页面进入购物车时,服务端需要做各种数据处理,比如刷新商品价格,计算促销价、校验库存等等。这些操作会随着业务的增加不断扩展,那么Visitor模式就适合这种场景,这也是它的优点之一,易于新增操作。

    3.实现

    上图是书上的UML原图,原生实现这里就不重复了,我们直接进入正题。

    3.1 并发的visit操作

    先来看关键的visit方法

    public void visit(Cart cart) {
        // 当商品数量不超过两个时,不做并发处理,直接主线程执行,避免浪费线程资源
        if (cart.getItems().size() <= 2) {
            for (AbstractItem item : cart.getItems()) {
                item.accept(this);
            }
        } else {
            //CountDownLatch 用来保证并发执行之后的同步,计数器为商品数量,只有所有visit操作完成后才继续主流程
            CountDownLatch countDownLatch = new CountDownLatch(cart.getItems().size());
            // 提交task到线程池
            cart.getItems().forEach(item -> {
                threadPoolExecutor.submit(new SubVisitor(item, countDownLatch));
            });
            try {
                // 等待所有子线程执行完毕
                countDownLatch.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    

    SubVisitor很简单,实现Runnalbe接口,负责执行具体的visit逻辑,并在执行完成之后同步操作。

    private class SubVisitor implements Runnable {
        CountDownLatch countDownLatch;
        AbstractItem abstractItem;
        public SubVisitor(AbstractItem abstractItem, CountDownLatch countDownLatch) {
            this.abstractItem = abstractItem;
            this.countDownLatch = countDownLatch;
        }
        @Override
        public void run() {
            abstractItem.accept(AbstractCartVisitor.this);
            // 执行完毕之后同步countDownLatch,即计算器减一
            countDownLatch.countDown();
        }
    }
    

    这里只需注意两点,

    • 在商品数量不超过两个时不使用线程池,以避免浪费资源。
    • 通过并发包中CountDownLatch来同步所有操作。

    3.2 线程池配置

    另外一个核心的配置是线程池,代码如下。

        int availableProcessors = Runtime.getRuntime().availableProcessors();
        threadPoolExecutor = new ThreadPoolExecutor(availableProcessors, availableProcessors,
                10, TimeUnit.MINUTES, new SynchronousQueue<Runnable>(), new ThreadPoolExecutor.CallerRunsPolicy());
    

    corePoolSize和maximumPoolSize配置为当前cpu核数,之所以一致是因为这样可以让线程池一直处于最大运行状态,我们追求的是最大效率,不需要销毁一部分来达到减少内存占用。

    接下来的参数workQueue是线程池的缓冲队列,超出容量的任务会被放入此队列,这里使用SynchronousQueue,它是一个零容量队列,意味着线程池没有缓冲区。

    RejectedExecutionHandler是则是拒绝策略,当线程池和缓冲区都满了后将会执行拒绝策略。这里我们使用CallerRunsPolicy,翻译过来就是主线程直接执行,我们可以看下它的实现。

    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
                if (!e.isShutdown()) {
                    r.run();
                }
            }
    

    这样的RejectedExecutionHandler和workQueue的配置可以达到我们想要的一个效果:

    如果线程池未满,则将任务提交给线程池执行;

    线程池满了后,不缓冲,由主线程直接执行任务,不阻塞请求。

    3.3 测试

    我们模拟一个刷新价格的测试场景来看一下效果。

    为了简单起见,我们将corePoolSize设为4。

    new ThreadPoolExecutor(4, 4 ...
    

    并假设价格查询接口耗时20ms:

    public double queryPriceByItemId(int id) {
        //模拟调用时间20毫秒
        try {
            Thread.sleep(20);
        } catch (InterruptedException e) {
        }
        return 1;
    }
    

    以下为1~6个商品时执行结果

    数量 1 金额 1.0 耗时  23 
    数量 2 金额 2.0 耗时  40 
    数量 3 金额 3.0 耗时  20 
    数量 4 金额 4.0 耗时  20 
    数量 5 金额 5.0 耗时  21 
    数量 6 金额 6.0 耗时  40 
    

    2个及以下商品时耗时为线性增长,这是由于我们对这种场景没做并发处理。

    4个商品时耗时为20ms,符合我们的预期。

    为什么5个的时候也是20ms?

    其原因在于我们的执行策略是SynchronousQueue+CallerRunsPolicy,第5个商品被主线程直接处理了,因此相当于我们实际的执行线程是5个,也达到了预期效果。

    #
    以上为并发的Visitor模式的全部内容,完成demo见Github:

    https://github.com/wchukai/basic-practice

    作者:初开

    原文链接:https://wchukai.com/article/design-patterns-concurrent-visitor-patterns-based-on-thread-pools

    本文由MetaCLBlog于2017-10-24 09:00:51自动同步至cnblogs

    本文基于 知识共享-署名-非商业性使用-禁止演绎 4.0 国际许可协议发布,转载必须保留署名及链接。

  • 相关阅读:
    PE文件解析器的编写(二)——PE文件头的解析
    PE解析器的编写(一)——总体说明
    PE文件详解(九)
    PE文件详解(八)
    06_建造者模式
    05_模板方法模式
    04_抽象工厂模式
    03_简单工厂模式(静态工厂模式)
    02_工厂方法模式
    01_单例模式
  • 原文地址:https://www.cnblogs.com/wchukai/p/7721832.html
Copyright © 2020-2023  润新知