设计模式:基于线程池的并发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 国际许可协议发布,转载必须保留署名及链接。
设计模式:基于线程池的并发Visitor模式的更多相关文章
- python 并发编程 基于线程池实现并发的套接字通信
		
不应该让服务端随着 并发的客户端数量增多,而无数起线程,应该用线程池,限制线程数量,控制最大并发数 io密集型程序,最大并发数是2 客户端 from socket import * client = ...
 - 基于线程池的多并发Socket程序的实现
		
Socket“服务器-客户端”模型的多线程并发实现效果的大体思路是:首先,在Server端建立“链接循环”,每一个链接都开启一个“线程”,使得每一个Client端都能通过已经建立好的线程来同时与Ser ...
 - Java线程池(Callable+Future模式)
		
转: Java线程池(Callable+Future模式) Java线程池(Callable+Future模式) Java通过Executors提供四种线程池 1)newCachedThreadPoo ...
 - 基于线程池的多线程售票demo2.0(原创)
		
继上回基于线程池的多线程售票demo,具体链接: http://www.cnblogs.com/xifenglou/p/8807323.html以上算是单机版的实现,特别使用了redis 实现分布式锁 ...
 - 基于线程池的多线程售票demo(原创)
		
废话不多说,直接就开撸import org.springframework.util.StopWatch;import java.util.concurrent.*;/** * 基于线程池实现的多线程 ...
 - requests模块session处理cookie  与基于线程池的数据爬取
		
引入 有些时候,我们在使用爬虫程序去爬取一些用户相关信息的数据(爬取张三“人人网”个人主页数据)时,如果使用之前requests模块常规操作时,往往达不到我们想要的目的,例如: #!/usr/bin/ ...
 - 【Java TCP/IP Socket】基于线程池的TCP服务器(含代码)
		
了解线程池 在http://blog.csdn.net/ns_code/article/details/14105457(读书笔记一:TCP Socket)这篇博文中,服务器端采用的实现方式是:一个客 ...
 - Python网络爬虫之cookie处理、验证码识别、代理ip、基于线程池的数据爬去
		
本文概要 session处理cookie proxies参数设置请求代理ip 基于线程池的数据爬取 引入 有些时候,我们在使用爬虫程序去爬取一些用户相关信息的数据(爬取张三“人人网”个人主页数据)时, ...
 - Java多线程设计模式(4)线程池模式
		
前序: Thread-Per-Message Pattern,是一种对于每个命令或请求,都分配一个线程,由这个线程执行工作.它将“委托消息的一端”和“执行消息的一端”用两个不同的线程来实现.该线程模式 ...
 
随机推荐
- 201521123097《Java程序设计》第九周学习总结
			
1. 本周学习总结 1.1 以你喜欢的方式(思维导图或其他)归纳总结异常相关内容. 2. 书面作业 本次PTA作业题集异常 1.常用异常 题目5-1 1.1 截图你的提交结果(出现学号) 1.2 自己 ...
 - java 程序编写规则(自己总结)
			
1.命名规范 (1)所有的标示符都只能用ASCⅡ字母(A-Z或a-z).数字(0-9)和下划线"_". (2)类名是一个名词,采用大小写混合的方式,每个单词的首字母大写.例如:Us ...
 - Java:静态内部类的使用目的、使用限制、与非静态内部类的对比
			
Java之静态内部类(static class) 在一个类中创建另外一个类,叫做成员内部类.这个成员内部类可以静态的(利用static关键字修饰),也可以是非静态的. 一.静态内部类的使用目的. 在 ...
 - 【java】聊聊java里的接口
			
接口的概念 java中的接口用于描述类应该具备什么样的功能,而不给出具体的实现,一个类可以“实现”多个接口 [注意]接口不是类,而是对类的一组描述 还是让我们通过一个例子来看看接口如何运作吧! ...
 - grep与正则表达式基础
			
目录 grep 正则表达式 grep用法简介 我们介绍GREP的用法,主要用于匹配行,我们借助下面的正则表达式来介绍如何使用grep,还有就是正则表达式在linux中是极为重要的一部分. 命令:gre ...
 - 浅谈SQL优化入门:2、等值连接和EXPLAIN(MySQL)
			
1.等值连接:显性连接和隐性连接 在<MySQL必知必会>中对于等值连接有提到两种方式,第一种是直接在WHERE子句中规定如何关联即可,那么第二种则是使用INNER JOIN关键字.如下例 ...
 - mysql  truncate、delete与drop区别
			
相同点: 1.truncate和不带where子句的delete.以及drop都会删除表内的数据. 2.drop.truncate都是DDL语句(数据定义语言),执行后会自动提交. 不同点: 1. t ...
 - 监控-CPU使用率
			
原始脚本来自TG,自己对部分脚本做了调整,分享出来仅供参考,请勿整篇Copy! 使用以下语句获取[CPU使用率] USE [DBA_Monitor] GO /****** 对象: StoredProc ...
 - Local Binary Convolutional Neural Networks ---卷积深度网络移植到嵌入式设备上?
			
前言:今天他给大家带来一篇发表在CVPR 2017上的文章. 原文:LBCNN 原文代码:https://github.com/juefeix/lbcnn.torch 本文主要内容:把局部二值与卷积神 ...
 - Beauty Contest    凸包+旋转卡壳法
			
Beauty Contest Time Limit: 3000MS Memory Limit: 65536K Total Submissions: 27507 Accepted: 8493 D ...