Java 多线程 Runnable / 线程池 ThreadPoolExecutor 的应用——加速二维矩阵的计算

目录

项目背景

硬件环境

自定义线程类

自定义线程类分析

1、构造函数

2、run()

如何规定当前线程应该计算哪一行? 

如何保证线程安全?

多线程/线程池使用

1、Runtime.getRuntime().availableProcessors()

2、ExecutorService executorService = new ThreadPoolExecutor()

几个线程池重点参数的关系:

多线程数量的选择(corePoolSize的大小):

3、executorService.shutdown()和.awaitTermination()


项目背景

本文主要讲述了一个多线程在实际开发中的处理案例。

现有一个 m*n 的矩阵数据待处理,每个矩阵元素都需要进行某种复杂的运算,串行遍历时间长,速度慢,考虑加入多线程加快运算速度。

思路:将这个 m 行 n 列的矩阵(二维数组)拆分,将每一行的运算作为一个线程,最终等所有行(线程)运算结束后,汇总结果。

当然,前提是,该二维数组中,每个元素的运算结果都相互独立,每个元素的结果不依赖于其他元素的结果,每个元素得出结果的先后顺序不影响最终结果,该项目需求满足前提,才考虑使用多线程。

硬件环境

机带 RAM:32GB

处理器:Intel(R) Core(TM) i7-10875H CPU @ 2.30GHz 2.30 GHz

CPU插槽:1

CPU内核:8

CPU逻辑处理器:16

自定义线程类

摘出某项目中,自定义线程类的部分代码:

package test_Interpolation;

import java.util.ArrayList;

public class InterpolationRunnable implements Runnable {
    public static double[][] resultData = null;
    private final ArrayList<Receiver> rArray;
    private final double[] lngs;// 经度
    private final double[] lats;// 纬度
    private final int insertPoints;// 插值点数
    private int latIndexGlobal = 0;
    private int latCount = 0;//用于表示完成进度

    public InterpolationRunnable(ArrayList<Receiver> rArray, double[] lngs, double[] lats, int insertPoints) {
		//初始化数据
        this.rArray = rArray;
        this.lngs = lngs;
        this.lats = lats;
        this.insertPoints = insertPoints;
        resultData = new double[lats.length][lngs.length];
    }

    @Override
    public void run() {
        double lat;
        int latIndex;
        synchronized (this) {
            lat = lats[latIndexGlobal];
            //算法中需要使用到latIndexGlobal的值
			//但不能直接使用,考虑线程安全,需要用中间变量latIndex
            latIndex = latIndexGlobal;
            latIndexGlobal++;
        }
        int lngIndex = -1;
        for (double lng : lngs) {
            lngIndex++;
			
			//中间为某种算法运算过程,此处省略
			...
            
			//result为该算法运算结果
            resultData[latIndex][lngIndex] += result;
        }
        synchronized (this) {
            latCount++;
            System.out.println(latCount + "/" + lats.length);
        }
    }
}

单线程开发,对应代码:

double[][] resultData = new double[lats.length][lngs.length];
int latIndex = -1;
int lngIndex;
for (double lat : lats) {
	latIndex++;
	lngIndex = -1;
	for (double lng : lngs) {
		lngIndex++;
		
		//中间为某种算法运算过程,此处省略
		...
		
		//result为该算法运算结果
		resultData[latIndex][lngIndex] += result;
	}
}

主要变量说明:

resultData[][]:是整个算法的运算结果,是一个lats.length行,lngs.length列,的二维数组。

对比多线程开发和单线程开发,主要区别在于,单线程 for (double lat : lats) 循环内的部分被作为多线程的run()。for (double lat : lats)可以理解成是在按行遍历,多线程就是把每一行作为一个独立的线程,让多行同时计算,提高运行速度。

自定义线程类分析

1、构造函数

在构造函数中主要用于初始化run()中会用到的一些数据。

2、run()

在run()中实现二维数组每一行元素的计算过程,开发中遇到的问题如下:

如何规定当前线程应该计算哪一行? 

由于run()无法传入参数,因此,当前线程应该计算哪一行,无法通过在线程启动时传参来实现。

只能通过线程类的 成员变量 来实时统计当前线程应该处理第几行,作为多个线程共享的数据,统计过程就需要用 同步锁 来保证线程安全。

如何保证线程安全?

截取代码片段如下:

double lat;
int latIndex;
synchronized (this) {
	lat = lats[latIndexGlobal];
	latIndex = latIndexGlobal;
	latIndexGlobal++;
}

latIndexGlobal是成员变量,用于标记当前线程应该处理哪一行,因为后续算法中会使用latIndexGlobal的值,此时必须用一个 局部变量 latIndex来传递成员变量latIndexGlobal的值,供后续算法安全使用,否则会出现latIndexGlobal已经被其他线程修改,在该线程中处理的行号发生变化,使得线程不安全。(当然,也能把整个算法加上同步锁,这样就能直接使用成员变量latIndexGlobal了,但这样就失去了多线程加速的意义)

多线程/线程池使用

//多线程
//创建线程对象,传入后续需要操作的对象,因为在启动时无法传参,所以需要在定义时先传好
InterpolationRunnable interpolationRunnable = new InterpolationRunnable(rArray, lngs, lats, InsertPoints);
int cpu = Runtime.getRuntime().availableProcessors();
//创建线程池
ExecutorService executorService = new ThreadPoolExecutor(cpu + 1, cpu + 1,
		1000, TimeUnit.MILLISECONDS,
		new ArrayBlockingQueue<Runnable>(lats.length),
		Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
//创建线程
for (double ignored : lats) {
	executorService.execute(new Thread(interpolationRunnable));
}
//线程池不再加入新线程,并等待已有线程执行结束,与awaitTermination搭配使用
executorService.shutdown();
while (true) {
	try {
		if (executorService.awaitTermination(100, TimeUnit.SECONDS)) break;
	} catch (InterruptedException e) {
		e.printStackTrace();
	}
}
double[][] resultData = InterpolationRunnable.resultData;

1、Runtime.getRuntime().availableProcessors()

返回的是可用的计算资源,不是CPU物理核心数,对于支持超线程的CPU来说,单个物理处理器相当于拥有两个逻辑处理器,能够同时执行两个线程。根据前面的硬件配置,本机返回的是CPU逻辑单元数16。

Java多线程采用CPU的抢占调度,直观的理解是,当CPU逻辑单元数大于线程数,则这些线程能实现完全并行;当CPU逻辑单元数小于线程数,则多出的线程会参与抢占,一个逻辑单元上多个线程高速切换,一个单元同一时刻只执行一个线程。

2、ExecutorService executorService = new ThreadPoolExecutor()

关于线程池的使用,百度有挺多的,贴出一个:线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式 - 雪山上的蒲公英 - 博客园1. 通过Executors创建线程池的弊端 在创建线程池的时候,大部分人还是会选择使用Executors去创建。 下面是创建定长线程池(FixedThreadPool)的一个例子,严格来说,当使用如https://www.cnblogs.com/zjfjava/p/11227456.html

几个线程池重点参数的关系:

poolSize:线程池中当前线程的数量

corePoolSize:线程池的基本大小

maximumPoolSize:线程池中允许的最大线程数

workQueue:线程阻塞队列

1、如果当前线程池的线程数还没有达到基本大小(poolSize < corePoolSize),无论是否有空闲的线程新增一个线程处理新提交的任务;

2、如果当前线程池的线程数大于或等于基本大小(poolSize >= corePoolSize) 且任务队列未满时,就将新提交的任务提交到阻塞队列排队,等候处理workQueue.offer(command);

3、如果当前线程池的线程数大于或等于基本大小(poolSize >= corePoolSize) 且任务队列满时;

        3.1、当前poolSize<maximumPoolSize,那么就新增线程来处理任务;

        3.2、当前poolSize=maximumPoolSize,那么意味着线程池的处理能力已经达到了极限,此时需要拒绝新增加的任务。至于如何拒绝处理新增的任务,取决于线程池的饱和策略RejectedExecutionHandler。

多线程数量的选择(corePoolSize的大小):

同时运行的线程数并不是越多越好,过多的线程数会导致耗费太多的时间在线程调度上,因此需要选择合适的线程数,才能充分发挥多线程的优势。该问题同样贴出一个参考链接:

探讨多线程数量的选择_eternal_yy-CSDN博客_多线程数量文章目录1. 操作系统相关知识概述2. 使用多线程的目的3. 如何利用多线程提升CPU和IO的综合利用效率4. 理论上如何创建合适数量的线程1. I/O密集型2. CPU密集型5. 实际中线程数的分析1. 操作系统相关知识概述首先介绍一下操作系统中CPU和核心数的概念,在每个计算机中,单核或者多核都是针对单个CPU而言,即这个多核或者单核已经集成在CPU内部了,不要理解成每个CPU中只有一个核...https://blog.csdn.net/eternal_yangyun/article/details/103236125结论如下:

I/O密集型:线程数 = CPU逻辑单元数 * [1 + (I/O耗时 / CPU耗时)]

CPU密集型:线程数 = CPU逻辑单元数 + 1

本案例是CPU密集型,如果分不清是哪种密集型,可以都试一下,测下时间,选用时间少的那个即可。

3、executorService.shutdown()和.awaitTermination()

shutdown方法和awaitTermination方法配合使用,用于让主线程阻塞等待,所有新建的多线程跑完后再继续执行后续操作。

shutdown方法:当此方法被调用时,线程池状态将被置为SHUTDOWN,SHUTDOWN状态下线程池停止接收新的任务并且等待已经提交的任务(包含提交正在执行和提交未执行)执行完成。当所有提交任务执行完毕,线程池即被关闭,线程池彻底关闭的状态为TERMINATED。该方法只是让线程池转入SHUTDOWN状态,并不会阻塞主线程,会继续向下执行,需要配合awaitTermination使用。

awaitTermination方法:接收timeout和TimeUnit两个参数,用于设定超时时间及单位。当等待超过设定时间时,会监测ExecutorService是否已经关闭,若关闭则返回true,否则返回false。一般情况下会和shutdown方法组合使用。

while (true) {
	try {
		if (executorService.awaitTermination(100, TimeUnit.SECONDS)) break;
	} catch (InterruptedException e) {
		e.printStackTrace();
	}
}

如果超时时间设定为100秒,在40秒时,所有线程就结束了,线程池关闭,那程序会继续阻塞,等待100秒结束吗?网上查了很多资料,没有找到这个问题解答,于是在源码中找到了答案。

查看awaitTermination方法源码,已经加上了部分注释:

public boolean awaitTermination(long timeout, TimeUnit unit)
	throws InterruptedException {
	long nanos = unit.toNanos(timeout);
	final ReentrantLock mainLock = this.mainLock;
	mainLock.lock();
	try {
        // 判断线程池的状态是否还未达到TERMINATED
		while (runStateLessThan(ctl.get(), TERMINATED)) {
			if (nanos <= 0L)
				return false;
            // 剩余等待时间 = 计划等待时间 - 已经等待的时间
			nanos = termination.awaitNanos(nanos);
		}
        // 线程池的状态达到TERMINATED,直接返回true,不用等待超时时间到达
		return true;
	} finally {
		mainLock.unlock();
	}
}

 awaitTermination中有个while循环,在不断的的判断线程池是否已经达到彻底关闭的状态TERMINATED,一旦达到就不再倒计时,直接返回true。

awaitTermination方法返回true后,表示所有线程已经执行完毕,线程池关闭,主线程继续执行,从静态类InterpolationRunnable中获取已经计算完成的二维数组resultData。

double[][] resultData = InterpolationRunnable.resultData;

至此多线程完成二维数组计算加速。 


补充:Java学习(十六)-线程与线程池学习(Thread与ThreadPoolExecutor)_chuhan_19930314的博客-CSDN博客

1、线程池中核心线程是复用的(用完阻塞,等待下一次任务),非核心线程用完会销毁;

2、四种常见线程池:

  • newCachedThreadPool
    核心线程数为0,非核心线程数为Integer.MAX_VALUE,适用于短时间高并发的处理业务,而在峰值过后并不会占用系统资源。
  • newFixedThreadPool
    核心线程数量和总线程数量相等,这是一个池中都是核心线程的线程池,所有线程都不会销毁。执行任务的全是核心线程,当没有空闲的核心线程时,任务会进入到阻塞队列,直到有空闲的核心线程才会去从阻塞队列中取出任务并执行,也导致该线程池基本不会发生使用拒绝策略拒绝任务。还有因为LinkedBlockingQueue阻塞队列的大小默认是Integer.MAX_VALUE,如果使用不当,很可能导致内存溢出
  • newSingleThreadExecutor
    有且仅有一个核心线程的线程池( corePoolSize == maximumPoolSize=1),使用了LinkedBlockingQueue(容量很大),所以,不会创建非核心线程。所有任务按照先来先执行的顺序执行。如果这个唯一的线程不空闲,那么新来的任务会存储在任务队列里等待执行。
  • newScheduledThreadPool
    创建一个定长线程池,支持定时及周期性任务执行,这是一个支持延时任务执行的线程池。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

cyc头发还挺多的

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值