通过单线程/线程池/分治算法三种方式实现1亿个数字的累加

news2024/12/24 11:39:16

一、任务类型

我们在做项目的时候,都需要考虑当前的项目或者某一个功能主要的核心是什么?是CPU密集计算型,还是IO密集型任务。我们调整线程池中的线程数量的最主要的目的是为了充分并合理地使用 CPU 和内存等资源,从而最大限度地提高程序的性能。在实际工作中,我们需要根据任务类型的不同选择对应的策略。

1-1、CPU密集型任务

CPU密集型任务也叫计算密集型任务,比如加密、解密、压缩、计算等一系列需要大量耗费 CPU 资源的任务。对于这样的任务最佳的线程数为 CPU 核心数的 1~2 倍,如果设置过多的线程数,实际上并不会起到很好的效果。此时假设我们设置的线程数量是 CPU 核心数的 2 倍以上,因为计算任务非常重,会占用大量的 CPU 资源,所以这时 CPU 的每个核心工作基本都是满负荷的,而我们又设置了过多的线程,每个线程都想去利用 CPU 资源来执行自己的任务,这就会造成不必要的上下文切换,此时线程数的增多并没有让性能提升,反而由于线程数量过多会导致性能下降。

1-2、IO密集型任务

IO密集型任务,比如数据库、文件的读写,网络通信等任务,这种任务的特点是并不会特别消耗 CPU 资源,但是 IO 操作很耗时,总体会占用比较多的时间。对于这种任务最大线程数一般会大于 CPU 核心数很多倍,因为 IO 读写速度相比于 CPU 的速度而言是比较慢的,如果我们设置过少的线程数,就可能导致 CPU 资源的浪费。而如果我们设置更多的线程数,那么当一部分线程正在等待 IO 的时候,它们此时并不需要 CPU 来计算,那么另外的线程便可以利用 CPU 去执行其他的任务,互不影响,这样的话在工作队列中等待的任务就会减少,可以更好地利用资源。

1-3、线程数计算方法

《Java并发编程实战》的作者 Brain Goetz 推荐的计算方法:

线程数 = CPU 核心数 *1+平均等待时间/平均工作时间)

通过这个公式,我们可以计算出一个合理的线程数量,如果任务的平均等待时间长,线程数就随之增加,而如果平均工作时间长,也就是对于我们上面的 CPU 密集型任务,线程数就随之减少。

太少的线程数会使得程序整体性能降低,而过多的线程也会消耗内存等其他资源,所以如果想要更准确的话,可以进行压测,监控 JVM 的线程情况以及 CPU 的负载情况,根据实际情况衡量应该创建的线程数,合理并充分利用资源。

1-4、算法示例

比如我们想计算1亿个数组内数字的和,应该怎么计算呢?

1-4-1、单线程计算

1、准备一个读取数组并计算的方法

通过下面的方法,就可以传入一个数组,同时传入开始计算的下标和结束计算的下标。然后通过for就可以依次获取数组中的值进行累加计算

/**
 * 数组求和
 * @param arr
 * @param lo
 * @param hi
 * @return
 */
public static long sumRange(int[] arr, int lo, int hi) {
    long result = 0;

    for (int j = lo; j < hi; j++) {
        result += arr[j];
    }
    return result;
}
2、准备一个生成1亿随机数的数组

通过如下方法,可以根据传入的值,生成对应大小的数组,然后生成随机数放入数组中。

public static int[] buildRandomIntArray(final int size) {
   int[] arrayToCalculateSumOf = new int[size];
   Random generator = new Random();
   for (int i = 0; i < arrayToCalculateSumOf.length; i++) {
      arrayToCalculateSumOf[i] = generator.nextInt(1000);
   }
   return arrayToCalculateSumOf;
}
3、调用测试方法进行计算
public class SumSequential {
    public static long sum(int[] arr){
        return SumUtils.sumRange(arr, 0, arr.length);
    }

    public static void main(String[] args) {
        // 准备数组
        int[] arr = Utils.buildRandomIntArray(100000000);
        System.out.printf("The array length is: %d\n", arr.length);
        Instant now = Instant.now();
        //数组求和
        long result = sum(arr);
        System.out.println("执行时间:"+ Duration.between(now,Instant.now()).toMillis());

        System.out.printf("The result is: %d\n", result);
    }
}

执行结果:

如下是我电脑CPU的逻辑数量
在这里插入图片描述
最终执行的时间为62ms,这个值也不是固定不变的
在这里插入图片描述
通过上面这种单线程计算大约时间为62ms,那有没有办法让计算提速呢?

1-4-2、多线程计算

利用多线程将任务拆分,最终再将拆分的结果再合并,这样理论上是不是就可以提高速度了?
在这里插入图片描述
使用多线程,我们就需要考虑使用线程池,这样可以一定程度上减少线程的创建和销毁带来的损耗

1、定义一个计算的任务,并实现Callable接口

计算方式,依然还是单线程的计算方式

public class SumTask implements Callable<Long> {
    int lo;
    int hi;
    int[] arr;

    public SumTask(int[] a, int l, int h) {
        lo = l;
        hi = h;
        arr = a;
    }

    @Override
    public Long call() { //override must have this type
        //System.out.printf("The range is [%d - %d]\n", lo, hi);
        long result = SumUtils.sumRange(arr, lo, hi);
        return result;
    }
}

2、根据拆分粒度,将任务拆解,并用线程池计算,然后合并计算结果

将1亿的数组进行拆分,然后定义为子任务,然后通过线程池进行计算,合并结果

//拆分的粒度
public final static int NUM = 10000000;

public static long sum(int[] arr, ExecutorService executor) throws Exception {
    long result = 0;
    int numThreads = arr.length / NUM > 0 ? arr.length / NUM : 1;
    //任务分解
    SumTask[] tasks = new SumTask[numThreads];
    Future<Long>[] sums = new Future[numThreads];
    for (int i = 0; i < numThreads; i++) {
        tasks[i] = new SumTask(arr, (i * NUM),
                ((i + 1) * NUM));
        sums[i] = executor.submit(tasks[i]);
    }
    //结果合并
    for (int i = 0; i < numThreads; i++) {
        result += sums[i].get();
    }

    return result;
}

3、执行运算

根据拆分的粒度,构建线程池,然后调用计算方法

public static void main(String[] args) throws Exception {
    // 准备数组
    int[] arr = Utils.buildRandomIntArray(100000000);
    //获取线程数
    int numThreads = arr.length / NUM > 0 ? arr.length / NUM : 1;

    System.out.printf("The array length is: %d\n", arr.length);
    // 构建线程池
    ExecutorService executor = Executors.newFixedThreadPool(numThreads);
    //预热
    //((ThreadPoolExecutor)executor).prestartAllCoreThreads();

    Instant now = Instant.now();
    // 数组求和
    long result = sum(arr, executor);
    System.out.println("执行时间:"+Duration.between(now,Instant.now()).toMillis());

    System.out.printf("The result is: %d\n", result);

    executor.shutdown();

}

执行结果:

如下结果不一定准确,我本地执行有时候也会超过单线程执行的时间。即使给线程池预热的情况下也是如此,那这是什么原因呢?
在这里插入图片描述
当粒度拆分到10000的时候,会发现执行时间更久了(线程的建立也是有损耗的,而且还涉及到上下文线程的切换),如下:
在这里插入图片描述

1-4-3、分治算法

分治算法的基本思想是将一个规模为N的问题分解为K个规模较小的子问题,这些子问题相互独立且与原问题性质相同。求出子问题的解,就可得到原问题的解。

分治算法的步骤如下:

  1. 分解:将要解决的问题划分成若干规模较小的同类问题;
  2. 求解:当子问题划分得足够小时,用较简单的方法解决;
  3. 合并:按原问题的要求,将子问题的解逐层合并构成原问题的解。

在这里插入图片描述
在分治法中,子问题一般是相互独立的,因此,经常通过递归调用算法来求解子问题
在这里插入图片描述
1、使用递归任务分解计算

主要思想就是将任务使用递归的方式进行拆解,最终再通过线程池进行计算,最终合并

public static class RecursiveSumTask implements Callable<Long> {
        //拆分的粒度
        public static final int SEQUENTIAL_CUTOFF = 100000;
        int lo;
        int hi;
        int[] arr; // arguments
        ExecutorService executorService;

        RecursiveSumTask(ExecutorService executorService, int[] a, int l, int h) {
            this.executorService = executorService;
            this.arr = a;
            this.lo = l;
            this.hi = h;
        }

        @Override
        public Long call() throws Exception {
            System.out.format("%s range [%d-%d] begin to compute %n",
                    Thread.currentThread().getName(), lo, hi);
            long result = 0;
            //最小拆分的阈值
            if (hi - lo <= SEQUENTIAL_CUTOFF) {
                for (int i = lo; i < hi; i++) {
                    result += arr[i];
                }
//                System.out.format("%s range [%d-%d] begin to finished %n",
//                        Thread.currentThread().getName(), lo, hi);
            } else {
                RecursiveSumTask left = new RecursiveSumTask(
                        executorService, arr, lo, (hi + lo) / 2);
                RecursiveSumTask right = new RecursiveSumTask(
                        executorService, arr, (hi + lo) / 2, hi);
                Future<Long> lr = executorService.submit(left);
                Future<Long> rr = executorService.submit(right);

                result = lr.get() + rr.get();
//                System.out.format("%s range [%d-%d] finished to compute %n",
//                        Thread.currentThread().getName(), lo, hi);
            }

            return result;
        }
    }

2、创建线程池、调用递归任务

public static long sum(int[] arr) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
         //递归任务 求和
        RecursiveSumTask task = new RecursiveSumTask(executorService, arr, 0, arr.length);
        //返回结果
        long result = executorService.submit(task).get();

        executorService.shutdown();
        return result;
    }

3、执行计算

public static void main(String[] args) throws Exception {
    //准备数组
    int[] arr = Utils.buildRandomIntArray(100000000);
    System.out.printf("The array length is: %d\n", arr.length);
    Instant now = Instant.now();
    //数组求和
    long result = sum(arr);
    System.out.println("执行时间:"+ Duration.between(now,Instant.now()).toMillis());
    System.out.printf("The result is: %d\n", result);

}

执行结果

通过任务拆分 分治算法效果也还不错
在这里插入图片描述

1-4-3-1、应用场景

分治思想在很多领域都有广泛的应用,例如算法领域有分治算法(归并排序、快速排序都属于分治算法,二分法查找也是一种分治算法);大数据领域知名的计算框架 MapReduce 背后的思想也是分治。既然分治这种任务模型如此普遍,那 Java 显然也需要支持,Java 并发包里提供了一种叫做 Fork/Join 的并行计算框架,就是用来支持分治这种任务模型的。

1-4-3-2、饥饿死锁

上面需要注意的是,我们将粒度拆分为1千万,计算1亿也就是需要10个线程进行计算,如果使用固定大小的线程池,并且小于10个线程是什么样呢?

可以看到使用了newFixedThreadPool,设置了固定的核心线程数和最大线程数

ExecutorService executorService = Executors.newFixedThreadPool(8);

在这里插入图片描述
通过上图可以看到,因为设置了固定大小的线程池,我们的线程池都被任务拆分占据了,这样最终执行任务的时候,线程池已经无可用空闲的线程可以使用,这样就会造成饥饿死锁

使用线程池的时候建议根据实际场景然后使用ThreadPoolExcutor,使用符合当前业务场景的队列,创建线程池。

ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
        5, 5, 1000, TimeUnit.SECONDS,
        new ArrayBlockingQueue<>(100),
        (r) -> new Thread(r, counter.addAndGet(1) + " 号 "),
        new ThreadPoolExecutor.AbortPolicy());

1-5、小结

虽然理论上使用线程池的方式可以将任务拆解,但是也会因为多线程的情况下,程序在运行的时候,CPU会进行上线文切换,这样也会增加执行时间。

使用线程池主要目的线程的复用,我们上面的案例,执行了一个简单的逻辑计算,如果是执行一个复杂的逻辑计算,在定义了合适数量的线程池的情况下,会比单线程效率高。

使用分治算法,需要注意线程池的容量,防止造成饥饿死锁。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/437651.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

AIGC潮水中,重新理解低代码

如果将一句话生成应用形容成L4级的“无人驾驶”&#xff0c;伙伴云的「AI搭建」则更像L2级的“辅助驾驶”。 作者|斗斗 出品|产业家 2023年&#xff0c;AIGC下的低代码赛道“暗流涌动”。 “对于「AI搭建」的搭建效果&#xff0c;尤其是在场景覆盖的广度上&#xff0c;连…

正式开赛|2023年“桂林银行杯”数据建模大赛暨全国大学生数学建模竞赛广西赛区热身赛

为学习贯彻党的二十大工作报告中关于加快发展数字经济、促进数字经济和实体经济深度融合的重要指示&#xff0c;不断推进数字化转型与金融科技创新&#xff0c;桂林银行联合全国大学生数学建模竞赛广西赛区组委会、广西应用数学中心&#xff08;广西大学&#xff09;共同主办20…

如何选择CDN厂商

如果您的在线业务面临着流量和访客数量的增加&#xff0c;如果您想提高网站速度和用户体验&#xff0c;选择合适的CDN提供商是朝着正确方向迈出的一步&#xff0c;那么如何来选择最合适的CDN厂商呢&#xff0c;火伞云小编今天为您解答&#xff1a; 一、测试潜在的CDN提供商 对…

centos7.6非默认端口的ssh免密登录(centos7.6指定端口的ssh免密登录)

非默认端口号&#xff08;以6622端口号示例&#xff09;的免密登录 1.1. 修改/etc/ssh/sshd_config Port 6622 1.2. 重启sshd服务 service sshd restart 1.3. 创建用户ds(可选&#xff0c;这里以ds用户做免密为示例) adduser ds&#xff1b; 1.4. 查看ds用户(可选) id ds; …

HBase高手之路6—HBase高可用

文章目录 HBase的高可用一、HBase高可用简介二、搭建HBase的高可用1.在HBase的conf文件夹中创建一个backup-masters的文件2.修改backup-masters&#xff0c;添加作为备份master的节点信息3.分发backup-masters文件到其他的服务器4.重新启动HBase5.查看web ui 三、测试高可用1.尝…

辉煌优配|黄金价格创近两年半新高!2只黄金股一季度预增

黄金板块早盘走强。 4月14日早盘&#xff0c;黄金板块团体走高&#xff0c;次新股四川黄金开盘半小时内拉升至涨停&#xff0c;封单资金到达7279.78万元&#xff0c;中润资源、晓程科技涨幅居前&#xff0c;分别为8.96%、8.48% 消息面上来看&#xff0c;近期全球黄金期货价格节…

Matlab进阶绘图第17期—气泡热图

气泡热图是一种特殊的热图&#xff08;Heatmap&#xff09;。 与传统热图相比&#xff0c;气泡热图利用不同颜色、不同大小的圆形表示数据的大小&#xff0c;可以更加直观地对矩阵数据进行可视化表达。 本文使用自制的bubbleheatmap小工具进行气泡热图的绘制&#xff0c;先来…

AttributeError: ‘LTP‘ object has no attribute ‘init_dict‘解决方案

大家好,我是爱编程的喵喵。双985硕士毕业,现担任全栈工程师一职,热衷于将数据思维应用到工作与生活中。从事机器学习以及相关的前后端开发工作。曾在阿里云、科大讯飞、CCF等比赛获得多次Top名次。现为CSDN博客专家、人工智能领域优质创作者。喜欢通过博客创作的方式对所学的…

【刷题】小技巧

好久没更了 写天梯模拟L1都有题不能AC&#xff0c;是什么品种的蒟蒻 L1-7 谷歌的招聘 题目详情 - L1-7 谷歌的招聘 (pintia.cn) 自己写半天都是Segmentation Fault&#xff0c; 学习一下几个函数叭// 1.substr&#xff08;&#xff09;函数 获取子串 #include<bits/st…

OpenCV 安卓编程示例:1~6 全

原文&#xff1a;OpenCV Android Programming By Example 协议&#xff1a;CC BY-NC-SA 4.0 译者&#xff1a;飞龙 本文来自【ApacheCN 计算机视觉 译文集】&#xff0c;采用译后编辑&#xff08;MTPE&#xff09;流程来尽可能提升效率。 当别人说你没有底线的时候&#xff0c;…

国产AI软件,10年前已出现,Excel表格变软件,用友用户:有救了

10年前&#xff0c;国产AI软件已经出现 在国内&#xff0c;我们早在10年就已经有AI软件&#xff0c;而且现在还在使用。 10年前&#xff0c;这款软件跟现在市面上流行的ChatGPT和文心一言相比&#xff0c;最为先进的是&#xff1a;不根本用写代码&#xff0c;只要会画表格就可…

【Docker01】入门

目录 概述 Docker平台 Docker可以做什么 快速、一致地交付应用程序 响应式部署和扩展 在同一硬件上运行更多工作负载 Docker架构 Docker守护程序&#xff08;The Docker daemon&#xff09; Docker客户端&#xff08;The Docker client&#xff09; Docker桌面&#x…

KingSCADA3.8保姆级安装教程

大家好&#xff0c;我是雷工&#xff01; 最近开始学习KingSCADA&#xff0c;今天这篇详细记录安装KingSCADA3.8的过程。 首先下载需要的安装版本&#xff0c;此处以从官网下载的最新版本KingSCADA3.8为例&#xff0c;双击&#xff1a;Setup.exe ; 一、安装主程序 1、点击“…

电脑端(PC)按键精灵2013——入门小白 详细 教程

电脑端(PC)按键精灵——1.入门详细说明&#xff1a; 本篇幅介绍的按键精灵的下载和安装&#xff1b;如果已经安装则直接看下面命令内容 电脑端(PC)按键精灵——2.键盘命令和鼠标命令 电脑端(PC)按键精灵——3其他命令 电脑端(PC)按键精灵——4.控制命令&#xff08;判断、循…

使用华为云免费资源训练Paddle UIE模型

一、创建虚拟环境 好习惯&#xff0c;首先创建单独的运行环境 conda create -n uie python3.10.9 conda activate uie 二、安装paddle框架及paddlenlp 2.1 参考官方文档安装paddle 开始使用_飞桨-源于产业实践的开源深度学习平台 首先查看自己服务器cuda版本&#xff0c;…

redis_5种数据结构及其底层实现原理详解

1、 redis中的数据结构 Redis支持五种数据类型&#xff1a;string&#xff08;字符串&#xff09;&#xff0c;hash&#xff08;哈希&#xff09;&#xff0c;list&#xff08;列表&#xff09;&#xff0c;set&#xff08;无序集合&#xff09;及zset(有序集合) 在秒杀项目里…

LED显示屏有色差要怎么处理?

LED显示屏在销售的时候不可避免的会产生尾货。这些尾货由于是不同批次的产品&#xff0c;亮度不可避免的有差异&#xff0c;拼装之后显示效果不佳&#xff0c;这时候就必须使用逐点校正技术。你知道LED显示屏的亮度和对比度是如何调节的吗&#xff1f; 消除差异逐点校正是一项用…

winForm常用控件

一般控件 Label TextBox&#xff1a;文本框 Button RadioButton CheckBox ComboBox:下拉框 CheckedListBox:带复选框的列表项 DateTimePicker:日期时间选择控件 ListBox:列表项 ListView:以五种不同视图显示项的集合 MaskedTextBox:格式化文本框 MonthCalendar:月历 NumberIcUp…

选择Zoho CRM的三大原因

上周&#xff0c;美国IT杂志PCMag发布了关于CRM系统的新评价&#xff0c;Salesforce Sales Cloud Lightning Professional、Zoho CRM、HubSpot CRM、Zendesk、SugarCRM等多个CRM品牌上榜。借此机会&#xff0c;我们来说说Zoho CRM为什么值得推荐&#xff1f; PCMag&#xff0c…

答对这道面试题,直接原地入职:说一下公司常用MySQL分库分表方案

一、数据库瓶颈 不管是IO瓶颈&#xff0c;还是CPU瓶颈&#xff0c;最终都会导致数据库的活跃连接数增加&#xff0c;进而逼近甚至达到数据库可承载活跃连接数的阈值。在业务Service来看就是&#xff0c;可用数据库连接少甚至无连接可用。接下来就可以想象了吧&#xff08;并发…