内容简介
最近接触的项目,数据量都大的离谱,一些复杂的算法功能运行起来简直是灾难。话不多说,下面提供一种用线程池加快程序运行的样例参考
结果
直接放图:
计算结果相同,单独计算耗时948ms,用线程池4个线程计算耗时276ms,速度变快了四倍甚至三倍!
完整代码
public static void main(String[] args) throws ParseException, InterruptedException, ExecutionException {
// 初始化一个巨大数组,可以想象为我们需要处理的业务数据,上万甚至上亿条记录
int[] a = new int[360000];
// 随机放入100内随机数
rand(a);
long beginTime = System.currentTimeMillis();
long b = 0L;
// 传统单线程计算,10000*360000次运算,模拟对业务数据的处理过程
for (int k = 0; k<10000 ; k++) {
for (int i : a) {
b += i;
}
}
System.out.println("总和"+b);
System.out.println("单独计算耗时"+(System.currentTimeMillis()-beginTime)+"毫秒");
System.out.println("_______________________________________________________");
// 使用线程池计算
beginTime = System.currentTimeMillis();
// 定义线程池,各参数含义可以看一下官方文档说明
ExecutorService pool = new ThreadPoolExecutor(4,8,6
,TimeUnit.SECONDS,new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
// 将业务数据切割为4份,分为4个线程处理,实际使用中可能不清楚数据量,用for循环就好
// 此处子线程可用runable或callable,如果需要回调信息,则必须使用callable,返回值在自定义的线程类中编写
Callable<Long> runnable = new sumThread(a,0,90000);
Callable<Long> runnable1 = new sumThread(a,90000,180000);
Callable<Long> runnable2 = new sumThread(a,180000,270000);
Callable<Long> runnable3 = new sumThread(a,270000,360000);
System.out.println("准备启动");
// 四个线程初始化完成,提交给线程池
Future<Long> num = pool.submit(runnable);
Future<Long> num1 = pool.submit(runnable1);
Future<Long> num2 = pool.submit(runnable2);
Future<Long> num3 = pool.submit(runnable3);
System.out.println("启动完成");
// 调用callable的get()方法,从各子线程获取返回值,由于callable.get()方法特性,子线程运行完毕后才会调用这一行
System.out.println("总和"+(num.get()+num1.get()+num2.get()+num3.get()));
long endTime = System.currentTimeMillis();
System.out.println("线程池总耗时"+(endTime-beginTime)+"毫秒");
//关闭线程池,已提交的任务继续执行,不再接收新的任务
pool.shutdown();
//等待所有的任务都结束(实时判断是否全完成),若所有任务都已完成,则返回true,若超时未完成,则返回false
if (!pool.awaitTermination(6, TimeUnit.SECONDS)) {
//超时的时候向线程池中所有的线程发出中断(interrupted)。
pool.shutdownNow();
}
}
// 向数组中填充100内随机数
private static void rand(int[] a) {
for (int i = 0; i < a.length; i++) {
a[i] = new Random().nextInt(100);
}
}
// 编写自定义线程类,可以实现runable或callable,如需调用返回值,则必须实现callable
class sumThread implements Callable<Long> {
// 存放的业务数据
int[] a ;
// 定义一个返回值,可按需求修改
long result;
// 初始化线程实例时,从业务数据中根据下标取出 一小段数据
public sumThread(int[] b,int start,int end) {
a=Arrays.copyOfRange(b,start,end);
System.out.println("初始化一个线程实例");
}
// 执行方法,模拟对业务数据的处理过程,运行完成后return指定的返回值
@Override
public Long call() throws Exception {
System.out.println("启动一个线程");
// a = rand1(a);
long beginTime = System.currentTimeMillis();
long b = 0L;
for (int k = 0; k<10000 ; k++) {
for (int j = 0; j < a.length; j++) {
b += a[j];
}
}
long endTime = System.currentTimeMillis();
System.out.println("线程耗时"+(endTime-beginTime)+"毫秒");
System.out.println("线程计算结果"+b);
System.out.println("结束一个线程");
result = b;
return result;
}
}
题外话
为什么线程池更快呢?
线程池提高程序运行效率的原理较为复杂。类比来说,如果把CPU核心比作程序员(悲),单线程就像是一个程序员闷头写代码,另外的项目经理和程序员在喝茶摸鱼。对比多线程一个项目经理指挥N个程序员同时写代码。虽然项目经理(主线程)在创建工位、开会交代任务、监督管理项目进度会耗费一点时间,但N个人一起做项目,总归是要比一个人闷头蛮干要快得多的,这也是四个线程处理时间要比单线程处理时间的四分之一要多一点的原因。(铁汁们干活的时候,也要及时交流,三个臭皮匠顶个诸葛亮,三个程序员起码能顶个冯诺依曼
实际的业务场景中,要比这复杂得多,例如处理业务数据后需要调用接口,或者从磁盘读取数据。CPU速度是比磁盘速度和网速快得多的,这会导致程序实际计算的时间,远远小于空闲等待的时间,CPU绝大多数时间是在摸鱼的。
同样还拿程序员来举例,小明接到工单编写业务功能A,写完代码(CPU运算)耗时5分钟,项目编译打包(调取接口)5分钟(有些夸张,但在计算机的视角,CPU运算速度是比网速快得多的),后5分钟小明基本就是在喝茶等待,我们当然希望小明后5分钟也要有事情干。第二天,小明面前摆了两台电脑,在业务功能A的代码编译时间,去写业务功能B的代码,交替往复,时间利用率大大提高。
多线程为什么推荐用线程池呢?我每次都直接写thread线程不可以吗?
从功能实现的角度也不是不行。但上面有说,项目经理给程序员安排任务,需要给他们提供工位(创建新线程),程序员的代码写完后,项目经理会把工位拆掉(悲),创建新工位和拆除工位会耗费无谓的资源。而线程池则会保留这些空着的工位,线程池参数中的corePoolSize常驻核心线程数定义了可常驻保留的工位数量,程序员工作做完仍会保留工位,下次有任务直接坐下干活就好,节省资源消耗。通过调整线程池的其它参数,可以对多线程处理的程序进行更多更精细的调优和控制。
使用多线程的目的就是为了使程序更快、体验更好,既然用线程池可以更进一步节省资源,提高速度,又何尝不用呢。线程池用完记得对线程池进行关闭操作,不再使用的工位拆除掉就好了。
多线程任何时候都是更快的吗?我可以无脑用吗?
这当然不是,因为线程池需要对各子线程进行管理调度,也会消耗一定的时间和资源。如果数据量小、计算简单,使用多线程反而会拖累程序运行速度。而且简单的小数据功能,也没必要用那么多技术调优(写代码也累),这也是符合直觉的。
当然,多线程的使用场景,仅限于业务数据之间处理不受影响的场景:业务数据的任一部分的处理,不依赖于其它部分的处理结果,或没有严格的前后计算关系。个人观点是,更加倾向于大量数据的重复性简单运算。