SpringBoot开发案例之多任务并行+线程池处理

前言

前几篇文章着重介绍了后端服务数据库和多线程并行处理优化,并示例了改造前后的伪代码逻辑。当然了,优化是无止境的,前人栽树后人乘凉。作为我们开发者来说,既然站在了巨人的肩膀上,就要写出更加优化的程序。

改造

理论上讲,线程越多程序可能更快,但是在实际使用中我们需要考虑到线程本身的创建以及销毁的资源消耗,以及保护操作系统本身的目的。我们通常需要将线程限制在一定的范围之类,线程池就起到了这样的作用。

程序逻辑

多任务并行+线程池处理.png

一张图能解决的问题,就应该尽可能的少BB,当然底层原理性的东西还是需要大家去记忆并理解的。

Java 线程池

Java通过Executors提供四种线程池,分别为:

  • newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
  • newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
  • newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
  • newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

优点

  • 重用存在的线程,减少对象创建、消亡的开销,性能佳。
  • 可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。
  • 提供定时执行、定期执行、单线程、并发数控制等功能。

代码实现

方式一(CountDownLatch)
/**
 * 多任务并行+线程池统计
 * 创建者 科帮网  https://blog.52itstyle.com
 * 创建时间    2018年4月17日
 */
public class StatsDemo {
    final static SimpleDateFormat sdf = new SimpleDateFormat(
            "yyyy-MM-dd HH:mm:ss");
    
    final static String startTime = sdf.format(new Date());
    
    /**
	 * IO密集型任务  = 一般为2*CPU核心数(常出现于线程中:数据库数据交互、文件上传下载、网络数据传输等等)
	 * CPU密集型任务 = 一般为CPU核心数+1(常出现于线程中:复杂算法)
	 * 混合型任务  = 视机器配置和复杂度自测而定
	 */
	private static int corePoolSize = Runtime.getRuntime().availableProcessors();
	/**
	 * public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,
	 *                           TimeUnit unit,BlockingQueue<Runnable> workQueue)
	 * corePoolSize用于指定核心线程数量
	 * maximumPoolSize指定最大线程数
	 * keepAliveTime和TimeUnit指定线程空闲后的最大存活时间
	 * workQueue则是线程池的缓冲队列,还未执行的线程会在队列中等待
	 * 监控队列长度,确保队列有界
	 * 不当的线程池大小会使得处理速度变慢,稳定性下降,并且导致内存泄露。如果配置的线程过少,则队列会持续变大,消耗过多内存。
	 * 而过多的线程又会 由于频繁的上下文切换导致整个系统的速度变缓——殊途而同归。队列的长度至关重要,它必须得是有界的,这样如果线程池不堪重负了它可以暂时拒绝掉新的请求。
	 * ExecutorService 默认的实现是一个无界的 LinkedBlockingQueue。
	 */
	private static ThreadPoolExecutor executor  = new ThreadPoolExecutor(corePoolSize, corePoolSize+1, 10l, TimeUnit.SECONDS,
			new LinkedBlockingQueue<Runnable>(1000));
	
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(5);
        //使用execute方法
  		executor.execute(new Stats("任务A", 1000, latch));
  		executor.execute(new Stats("任务B", 1000, latch));
  		executor.execute(new Stats("任务C", 1000, latch));
  		executor.execute(new Stats("任务D", 1000, latch));
  		executor.execute(new Stats("任务E", 1000, latch));
        latch.await();// 等待所有人任务结束
        System.out.println("所有的统计任务执行完成:" + sdf.format(new Date()));
    }

    static class Stats implements Runnable  {
        String statsName;
        int runTime;
        CountDownLatch latch;

        public Stats(String statsName, int runTime, CountDownLatch latch) {
            this.statsName = statsName;
            this.runTime = runTime;
            this.latch = latch;
        }

        public void run() {
            try {
                System.out.println(statsName+ " do stats begin at "+ startTime);
                //模拟任务执行时间
                Thread.sleep(runTime);
                System.out.println(statsName + " do stats complete at "+ sdf.format(new Date()));
                latch.countDown();//单次任务结束,计数器减一
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
方式二(Future)
/**
 * 多任务并行+线程池统计
 * 创建者 科帮网 https://blog.52itstyle.com
 * 创建时间    2018年4月17日
 */
public class StatsDemo {
    final static SimpleDateFormat sdf = new SimpleDateFormat(
            "yyyy-MM-dd HH:mm:ss");
    
    final static String startTime = sdf.format(new Date());
    
    /**
	 * IO密集型任务  = 一般为2*CPU核心数(常出现于线程中:数据库数据交互、文件上传下载、网络数据传输等等)
	 * CPU密集型任务 = 一般为CPU核心数+1(常出现于线程中:复杂算法)
	 * 混合型任务  = 视机器配置和复杂度自测而定
	 */
	private static int corePoolSize = Runtime.getRuntime().availableProcessors();
	/**
	 * public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,
	 *                           TimeUnit unit,BlockingQueue<Runnable> workQueue)
	 * corePoolSize用于指定核心线程数量
	 * maximumPoolSize指定最大线程数
	 * keepAliveTime和TimeUnit指定线程空闲后的最大存活时间
	 * workQueue则是线程池的缓冲队列,还未执行的线程会在队列中等待
	 * 监控队列长度,确保队列有界
	 * 不当的线程池大小会使得处理速度变慢,稳定性下降,并且导致内存泄露。如果配置的线程过少,则队列会持续变大,消耗过多内存。
	 * 而过多的线程又会 由于频繁的上下文切换导致整个系统的速度变缓——殊途而同归。队列的长度至关重要,它必须得是有界的,这样如果线程池不堪重负了它可以暂时拒绝掉新的请求。
	 * ExecutorService 默认的实现是一个无界的 LinkedBlockingQueue。
	 */
	private static ThreadPoolExecutor executor  = new ThreadPoolExecutor(corePoolSize, corePoolSize+1, 10l, TimeUnit.SECONDS,
			new LinkedBlockingQueue<Runnable>(1000));
	
    public static void main(String[] args) throws InterruptedException {
    	List<Future<String>> resultList = new ArrayList<Future<String>>(); 
        //使用submit提交异步任务,并且获取返回值为future
    	resultList.add(executor.submit(new Stats("任务A", 1000)));
    	resultList.add(executor.submit(new Stats("任务B", 1000)));
    	resultList.add(executor.submit(new Stats("任务C", 1000)));
    	resultList.add(executor.submit(new Stats("任务D", 1000)));
    	resultList.add(executor.submit(new Stats("任务E", 1000)));
  	   //遍历任务的结果
        for (Future<String> fs : resultList) { 
            try { 
                System.out.println(fs.get());//打印各个线任务执行的结果,调用future.get() 阻塞主线程,获取异步任务的返回结果
            } catch (InterruptedException e) { 
                e.printStackTrace(); 
            } catch (ExecutionException e) { 
                e.printStackTrace(); 
            } finally { 
                //启动一次顺序关闭,执行以前提交的任务,但不接受新任务。如果已经关闭,则调用没有其他作用。
            	executor.shutdown(); 
            } 
        } 
        System.out.println("所有的统计任务执行完成:" + sdf.format(new Date()));
    }

    static class Stats implements Callable<String>  {
        String statsName;
        int runTime;

        public Stats(String statsName, int runTime) {
            this.statsName = statsName;
            this.runTime = runTime;
        }

        public String call() {
            try {
                System.out.println(statsName+ " do stats begin at "+ startTime);
                //模拟任务执行时间
                Thread.sleep(runTime);
                System.out.println(statsName + " do stats complete at "+ sdf.format(new Date()));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return call();
        }
    }
}

执行时间

以上代码,均是伪代码,下面是2000+个学生的真实测试记录。

2018-04-17 17:42:29.284 INFO   测试记录81e51ab031eb4ada92743ddf66528d82-单线程顺序执行,花费时间:3797
2018-04-17 17:42:31.452 INFO   测试记录81e51ab031eb4ada92743ddf66528d82-多线程并行任务,花费时间:2167
2018-04-17 17:42:33.170 INFO   测试记录81e51ab031eb4ada92743ddf66528d82-多线程并行任务+线程池,花费时间:1717

项目源码:https://gitee.com/52itstyle/spring-data-jpa

作者: 小柒

出处: https://blog.52itstyle.com

分享是快乐的,也见证了个人成长历程,文章大多都是工作经验总结以及平时学习积累,基于自身认知不足之处在所难免,也请大家指正,共同进步。

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏草根专栏

从头编写 asp.net core 2.0 web api 基础框架 (3)

Github源码地址:https://github.com/solenovex/Building-asp.net-core-2-web-api-starter-...

50770
来自专栏圣杰的专栏

Asp.net mvc 知多少(十)

本系列主要翻译自《ASP.NET MVC Interview Questions and Answers 》- By Shailendra Chauhan,想...

216100
来自专栏积累沉淀

CXF 框架拦截器

CXF的拦截器 •为什么设计拦截器? 1.为了在webservice请求过程中,能动态操作请求和响应数据, CXF设计了拦截器. •拦截器分类: 1...

20560
来自专栏拭心的安卓进阶之路

Android 性能优化:使用 Lint 优化代码、去除多余资源

*本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布 前言 在保证代码没有功能问题,完成业务开发之余,有追求的程序员还要追求代码的规范、可维护性...

56960
来自专栏FreeBuf

腾讯御见捕获Flash 0day漏洞(CVE-2018-5002)野外攻击

腾讯御见威胁情报中心近日监控到一例使用Adobe Flash 0day漏洞(CVE-2018-5002)的APT攻击,攻击者疑通过即时聊天工具和邮箱等把恶意Ex...

12200
来自专栏高爽的专栏

再聊线程池

引言 最近恰好在组内分享线程池,又看了看四年前自己写的线程池文章,一是感叹时光荏苒,二是感叹当时的理解太浅薄了,三是感叹自己这么多年依然停留在浅薄的理解当中,没...

26200
来自专栏安恒网络空间安全讲武堂

赛前福利①最新2018HITB国际赛writeup

FIRST 距离“西湖论剑杯”全国大学生网络空间安全技能大赛只有10天啦! 要拿大奖、赢offer,那必须得来点赛前练习定定心啊~这不,讲武堂就拿到了2018H...

47750
来自专栏菩提树下的杨过

spring集成kafka

一、添加依赖项 compile 'org.springframework.kafka:spring-kafka:1.2.2.RELEASE' 二、发消息(生产者...

23580
来自专栏GopherCoder

『No18: Go 实现世界杯后台管理系统』

趁着周末更新一期,上一期讲到 如何快速熟悉一个项目, 文章的最后讲到,最好的方法是借用相同的技术栈重新实现一个项目。

19910
来自专栏禹都一只猫博客

Linux 性能检测常用的 10 个基本命令

18220

扫码关注云+社区

领取腾讯云代金券