java并发编程实战之从因数分解看多线程
这篇博文主要通过对多个大数的因数分解进行实践,进而引出初步的多线程的一些用法和思考。
1.因数分解
因数分解是对一个数字m,找出它所有的因数的过程,比如说数字6 因数分解之后为 2x3
2.为何选择因数分解
因为因数分解是一个很费事的计算过程,目前没有很好的算法可以提高效率,可以很好的用来模拟复杂计算处理过程,以便于我们明显的去观察在不同的情况下线程的状态
- 因数分解的算法代码如下
具体就是先从2开始除,看2能不能被整除,如果可以则继续除以2,如果不能,则除以3 以此类推,直到除数不在小于m
代码如下
public class factorsService {
//建立一个因式分解服务
public void Service(Long i) {
List<Long> factors = factor(i);
}
private List<Long> factor(Long i) {
//时间起点
long begintime = System.currentTimeMillis();
Long j = 2L;
Long k = 0L;
Long start = i;
List<Long> factors = new ArrayList<Long>();
while (j <= i) {
if (i % j == 0) {
i = i / j;
factors.add(j);
} else {
j++;
}
k++;
}
System.out.println("计算结束,循环了" + k + "次");
System.out.print(start + "的因数分解为: ");
Long var = 1L;
for (int i1 = 0; i1 < factors.size(); i1++) {
System.out.print(factors.get(i1));
if (i1 < factors.size() - 1) {
System.out.print("X");
}
var *= factors.get(i1);
}
System.out.println();
System.out.println("验证:分解出来因数相乘结果为:" + var);
long endtime = System.currentTimeMillis();
System.out.println("耗时" + (endtime - begintime) + "毫秒");
return factors;
}
}
main方法如下
public static void main(String[] args) {
factorsService factorsService = new factorsService();
List<Long> nums = Arrays.asList(1323999999999L, 1323999999999L);
Long begin=System.currentTimeMillis();
for (Long num : nums) {
factorsService.Service(num);
}
Long end=System.currentTimeMillis();
System.out.println("2个任务计算完成,耗时:"+(end-begin)+"毫秒");
}
我们来看下输出, 这是对两个Long型的整数进行因数分解,此时没有使用多线程,串行执行
运行结果如下
计算结束,循环了374328530次
1323999999999的因数分解为: 3X3X3X131X374328527
验证:分解出来因数相乘结果为:1323999999999
耗时5374毫秒
计算结束,循环了374328530次
1323999999999的因数分解为: 3X3X3X131X374328527
验证:分解出来因数相乘结果为:1323999999999
耗时4698毫秒
2个任务计算完成,耗时:10073毫秒
我们可以看到,两个数的因数分解 总计花了10073秒
那么有什么更好的方法可以提升速度吗?答案是从算法来说,其实是没有 的,大数的因数分解很难做,因此奠定了非对称加密的基石(非对称加密的秘钥生成利用是利用到了这点特性,已知积很难算出因数,但是已知因数却很容易算出积) 好了,算法考虑优化不是我们现在这个博文的主题,接下来进入正题,我们使用多线程解决
3.什么是多线程,为什么要用多线程
通俗的来说线程是比进程更轻量级的调度单元,一个进程里面可以包含多个线程。
举个例子,我们常说的八核cpu 指定的就是1个cpu有8个物理处理单元,也就是最多可以同时跑8个物理线程。 (cpu还有超线程技术,可以用一个物理线程模拟多个线程)
既然cpu如此强大,但是如果我们的程序是单线程的,那么cpu的强大功能就浪费了,单线程用不上多核。
疑问1:我们用单线程做了两个因数分解,耗时10073毫秒,那么我们起两个线程,同时做这个因数分解的任务,那是不是只需要一半的时间?
疑问2:如果我有5个数要做因数分解,那我起五个线程,是不是也可以缩小5倍时间?
4.多线程跑因数分解的任务代码如下
public class factorsThread implements Runnable {
private CountDownLatch latch;
public factorsThread(CountDownLatch latch) {
this.latch = latch; //初始化闭锁
}
factorsService factorsService;
public void setFactorsService(factorsService factorsService) {
this.factorsService = factorsService;
}
Long num = 0L;
public void setNum(Long num) {
this.num = num;
}
public void run() {
factorsService.Service(num);
latch.countDown(); //CountDownLatch在执行完run后减1
}
}
CountDownLatch 闭锁,一开始维护一个初始变量值,执行countDown()之后变量值减1
引入这个CountDownLatch是为了在某一个点同步线程,让线程在同一个地方停下来,可以想象为一道门,然后多个线程都在这个门前停下来,好方便我们统计多线程的执行的时间。
CountDownLatch
的用法就是,先在要使用CountDownLatch的线程类中声明一个CountDownLatch属性,并在run方法执行完之后调用 latch.countDown();
将计数器减1:
例如
public class factorsThread implements Runnable {
private CountDownLatch latch;
public factorsThread(CountDownLatch latch) {
this.latch = latch; //初始化闭锁
}
....
public void run() {
factorsService.Service(num);
latch.countDown(); //CountDownLatch在执行完run后减1
}
然后在需要使用这个Thread类的地方,创建一个CountDownLatch的对象,并传入需要同步的线程数量,然后将CountDownLatch的象
传入到Thread类latch字段值里,然后启动这个线程
//创建CountDownLatch对象,2表示最终要同步的是2个线程
final CountDownLatch latch = new CountDownLatch(2);
factorsThread factorsThread = new factorsThread(latch);
factorsThread.setFactorsService(factorsService);
System.out.println("多线程的方式启动计算:");
Long beginT = System.currentTimeMillis();
for (Long num : nums) {
factorsThread.setNum(num);
Thread thread = new Thread(factorsThread);//新建线程
thread.start();
}
...
最后在需要等待所有线程完成任务的地方
try {
//多线程运行结束前一直等待
latch.await(); //闭锁的作用,阻塞等待 直到countDown为0
} catch (InterruptedException e) {
e.printStackTrace();
}
具体如下:
比如说,我们并发了2个线程,要怎么统计这两个线程执行结束后花了多少时间?可以在线程启动之前,读取到当前系统时间,然后在 latch.await()之后,所有多线程执行完毕之后,再读取一次时间,时间差即是多线程执行期间的总时间。
public static void main(String[] args) {
factorsService factorsService = new factorsService();
List<Long> nums = Arrays.asList(1323999999999L, 1323999999999L);
//开始时间
Long begin=System.currentTimeMillis();
//创建CountDownLatch对象,2表示最终要同步的是2个线程
final CountDownLatch latch = new CountDownLatch(2);
factorsThread factorsThread = new factorsThread(latch);
factorsThread.setFactorsService(factorsService);
System.out.println("多线程的方式启动计算:");
Long beginT = System.currentTimeMillis();
for (Long num : nums) {
factorsThread.setNum(num);
Thread thread = new Thread(factorsThread);//新建线程
thread.start();
}
try {
//多线程运行结束前一直等待
latch.await(); //闭锁的作用,阻塞等待 直到countDown为0
} catch (InterruptedException e) {
e.printStackTrace();
}
//结束时间
Long endT = System.currentTimeMillis();
System.out.println("多线程任务计算完成!耗时" + (endT - beginT) + "毫秒");
}
输出结果:
多线程的方式启动计算:
计算结束,循环了374328530次
1323999999999的因数分解为: 3X3X3X131X374328527
验证:分解出来因数相乘结果为:1323999999999
耗时5721毫秒
计算结束,循环了374328530次
1323999999999的因数分解为: 3X3X3X131X374328527
验证:分解出来因数相乘结果为:1323999999999
耗时5750毫秒
多线程任务计算完成!耗时5751毫秒
结论:两个线程并发运行的时候,时间缩小了接近一半,解决疑问1
疑问二,当我们5个线程的时候并发会怎样?
代码如下
main代码
public static void main(String[] args) {
factorsService factorsService = new factorsService();
List<Long> nums = Arrays.asList(1323999999999L, 1323999999999L,1323999999999L, 1323999999999L, 1323999999999L); //51323999999999L, 51323999999999L, 51323999999998L);
Long begin=System.currentTimeMillis();
for (Long num : nums) {
factorsService.Service(num);
}
Long end=System.currentTimeMillis();
System.out.println("5个任务计算完成,耗时:"+(end-begin)+"毫秒");
final CountDownLatch latch = new CountDownLatch(2);
factorsThread factorsThread = new factorsThread(latch);
factorsThread.setFactorsService(factorsService);
System.out.println("多线程的方式启动计算:");
Long beginT = System.currentTimeMillis();
for (Long num : nums) {
factorsThread.setNum(num);
Thread thread = new Thread(factorsThread);
thread.start();
}
try {
//多线程运行结束前一直等待
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
Long endT = System.currentTimeMillis();
System.out.println("多线程任务计算完成!耗时" + (endT - beginT) + "毫秒");
}
service代码
public class factorsService {
//建立一个因式分解服务
public void Service(Long i) {
List<Long> factors = factor(i);
}
private List<Long> factor(Long i) {
//时间起点
long begintime = System.currentTimeMillis();
Long j = 2L;
Long k = 0L;
Long start = i;
List<Long> factors = new ArrayList<Long>();
while (j <= i) {
if (i % j == 0) {
i = i / j;
factors.add(j);
} else {
j++;
}
k++;
}
synchronized (this) { //主要是新增了这块的同步,以便于打印控制台消息的时候顺序不会乱
//因为线程是交替执行的,这部分不同步的话打印会乱序。
//这部分代码的同步不会影响什么时间,因为主要的时间消耗不在这部分逻辑上
System.out.print(start + "的因数分解为: ");
Long var = 1L;
for (int i1 = 0; i1 < factors.size(); i1++) {
System.out.print(factors.get(i1));
if (i1 < factors.size() - 1) {
System.out.print("X");
}
var *= factors.get(i1);
}
long endtime = System.currentTimeMillis();
System.out.println("耗时" + (endtime - begintime) + "毫秒");
}
return factors;
}
}
输出结果
1323999999999的因数分解为: 3X3X3X131X374328527 耗时7032毫秒
1323999999999的因数分解为: 3X3X3X131X374328527 耗时6666毫秒
1323999999999的因数分解为: 3X3X3X131X374328527 耗时5614毫秒
1323999999999的因数分解为: 3X3X3X131X374328527 耗时4303毫秒
1323999999999的因数分解为: 3X3X3X131X374328527 耗时4295毫秒
5个任务计算完成,耗时:27910毫秒
多线程的方式启动计算:
1323999999999的因数分解为: 3X3X3X131X374328527 耗时9757毫秒
1323999999999的因数分解为: 3X3X3X131X374328527 耗时9777毫秒
1323999999999的因数分解为: 3X3X3X131X374328527 耗时9798毫秒
1323999999999的因数分解为: 3X3X3X131X374328527 耗时9815毫秒
1323999999999的因数分解为: 3X3X3X131X374328527 耗时9807毫秒
多线程任务计算完成!耗时9779毫秒
疑问2:5个线程是否可以缩短五倍时间,答案是不能
27910/9779 大概就是2.7倍的差距,并没有5倍
因此提升线程并不是线性的提升效率,反而随着线程数量的增加,cpu会花费大量的时间用于处理各个线程之间的切换上
切换时候需要保存各个线程的状态,又要恢复,而且各个线程之间彼此还会竞争cpu
合理的线程数目约为N 或者N+1 N为cpu核心数