目录
项目背景
硬件环境
自定义线程类
自定义线程类分析
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 rArray;
private final double[] lngs;// 经度
private final double[] lats;// 纬度
private final int insertPoints;// 插值点数
private int latIndexGlobal = 0;
private int latCount = 0;//用于表示完成进度
public InterpolationRunnable(ArrayList 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)可以理解成是在按行遍历,多线程就是把每一行作为一个独立的线程,让多行同时计算,提高运行速度。
在构造函数中主要用于初始化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(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;
返回的是可用的计算资源,不是CPU物理核心数,对于支持超线程的CPU来说,单个物理处理器相当于拥有两个逻辑处理器,能够同时执行两个线程。根据前面的硬件配置,本机返回的是CPU逻辑单元数16。
Java多线程采用CPU的抢占调度,直观的理解是,当CPU逻辑单元数大于线程数,则这些线程能实现完全并行;当CPU逻辑单元数小于线程数,则多出的线程会参与抢占,一个逻辑单元上多个线程高速切换,一个单元同一时刻只执行一个线程。
关于线程池的使用,百度有挺多的,贴出一个:线程池不允许使用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 3.2、当前poolSize=maximumPoolSize,那么意味着线程池的处理能力已经达到了极限,此时需要拒绝新增加的任务。至于如何拒绝处理新增的任务,取决于线程池的饱和策略RejectedExecutionHandler。 同时运行的线程数并不是越多越好,过多的线程数会导致耗费太多的时间在线程调度上,因此需要选择合适的线程数,才能充分发挥多线程的优势。该问题同样贴出一个参考链接: 探讨多线程数量的选择_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密集型,如果分不清是哪种密集型,可以都试一下,测下时间,选用时间少的那个即可。 shutdown方法和awaitTermination方法配合使用,用于让主线程阻塞等待,所有新建的多线程跑完后再继续执行后续操作。 shutdown方法:当此方法被调用时,线程池状态将被置为SHUTDOWN,SHUTDOWN状态下线程池停止接收新的任务并且等待已经提交的任务(包含提交正在执行和提交未执行)执行完成。当所有提交任务执行完毕,线程池即被关闭,线程池彻底关闭的状态为TERMINATED。该方法只是让线程池转入SHUTDOWN状态,并不会阻塞主线程,会继续向下执行,需要配合awaitTermination使用。 awaitTermination方法:接收timeout和TimeUnit两个参数,用于设定超时时间及单位。当等待超过设定时间时,会监测ExecutorService是否已经关闭,若关闭则返回true,否则返回false。一般情况下会和shutdown方法组合使用。 如果超时时间设定为100秒,在40秒时,所有线程就结束了,线程池关闭,那程序会继续阻塞,等待100秒结束吗?网上查了很多资料,没有找到这个问题解答,于是在源码中找到了答案。 查看awaitTermination方法源码,已经加上了部分注释: awaitTermination中有个while循环,在不断的的判断线程池是否已经达到彻底关闭的状态TERMINATED,一旦达到就不再倒计时,直接返回true。 awaitTermination方法返回true后,表示所有线程已经执行完毕,线程池关闭,主线程继续执行,从静态类InterpolationRunnable中获取已经计算完成的二维数组resultData。 至此多线程完成二维数组计算加速。 补充:Java学习(十六)-线程与线程池学习(Thread与ThreadPoolExecutor)_chuhan_19930314的博客-CSDN博客 1、线程池中核心线程是复用的(用完阻塞,等待下一次任务),非核心线程用完会销毁; 2、四种常见线程池:多线程数量的选择(corePoolSize的大小):
3、executorService.shutdown()和.awaitTermination()
while (true) {
try {
if (executorService.awaitTermination(100, TimeUnit.SECONDS)) break;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
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();
}
}
double[][] resultData = InterpolationRunnable.resultData;
核心线程数为0,非核心线程数为Integer.MAX_VALUE,适用于短时间高并发的处理业务,而在峰值过后并不会占用系统资源。
核心线程数量和总线程数量相等,这是一个池中都是核心线程的线程池,所有线程都不会销毁。执行任务的全是核心线程,当没有空闲的核心线程时,任务会进入到阻塞队列,直到有空闲的核心线程才会去从阻塞队列中取出任务并执行,也导致该线程池基本不会发生使用拒绝策略拒绝任务。还有因为LinkedBlockingQueue阻塞队列的大小默认是Integer.MAX_VALUE,如果使用不当,很可能导致内存溢出
有且仅有一个核心线程的线程池( corePoolSize == maximumPoolSize=1),使用了LinkedBlockingQueue(容量很大),所以,不会创建非核心线程。所有任务按照先来先执行的顺序执行。如果这个唯一的线程不空闲,那么新来的任务会存储在任务队列里等待执行。
创建一个定长线程池,支持定时及周期性任务执行,这是一个支持延时任务执行的线程池。