java并发编程实战从因数分解看多线程(下)
java并发编程实战之从因数分解看多线程(上)
从之前的那篇文章中我们已经验证了多线程可以显著提高程序的效率,但并非没有限制。那么要如何才能更进一步的提升这个程序的效率,答案是利用缓存。
我们将已经进行过因数分解的数字和它分解之后的结果存起来,如果下次任务中发现这个数字已经被分解过了,就直接从缓存里拿,而没有必要继续去计算。
我们选用map做为缓存,改造后的因数分解的代码如下:
public class factorsService {
//用map作为缓存
private Map<Long,List<Long>> cache=new HashMap<Long, List<Long>>();
//建立一个因式分解服务
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>();
//先从缓存中去取数据 如果为空则进入计算,否则直接取缓存
if (cache.get(i) == null) {
while (j <= i) {
if (i % j == 0) {
i = i / j;
factors.add(j);
} else {
j++;
}
k++;
}
//计算完之后放回缓存中
cache.put(start, factors);
} else {
System.out.print("由于已经有缓存,所以从缓存中取数据: ");
factors = cache.get(i);
}
synchronized (this) {
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,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)+"毫秒");
}
输出结果:
1323999999999的因数分解为: 3X3X3X131X374328527 耗时7214毫秒
由于已经有缓存,所以从缓存中取数据: 1323999999999的因数分解为: 3X3X3X131X374328527 耗时0毫秒
由于已经有缓存,所以从缓存中取数据: 1323999999999的因数分解为: 3X3X3X131X374328527 耗时0毫秒
由于已经有缓存,所以从缓存中取数据: 1323999999999的因数分解为: 3X3X3X131X374328527 耗时0毫秒
由于已经有缓存,所以从缓存中取数据: 1323999999999的因数分解为: 3X3X3X131X374328527 耗时0毫秒
5个任务计算完成,耗时:7214毫秒
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();
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) + "毫秒");
}
输出结果:
多线程的方式启动计算:
1323999999999的因数分解为: 3X3X3X131X374328527 耗时11607毫秒
1323999999999的因数分解为: 3X3X3X131X374328527 耗时11633毫秒
1323999999999的因数分解为: 3X3X3X131X374328527 耗时11675毫秒
1323999999999的因数分解为: 3X3X3X131X374328527 耗时11693毫秒
1323999999999的因数分解为: 3X3X3X131X374328527 耗时11701毫秒
多线程任务计算完成!耗时11642毫秒
我们可以看到,按照我们刚刚写法,多线程反而比单线程还慢了,耗费了11642毫秒。仔细观察,问题在于单线程走了缓存,而多线程没有,为什么会出现这种情况?
观察这两次控制台输出,结合代码
//先从缓存中去取数据 如果为空则进入计算,否则直接取缓存
if (cache.get(i) == null) { //竞态条件
while (j <= i) {
if (i % j == 0) {
i = i / j;
factors.add(j);
} else {
j++;
}
k++;
}
//计算完之后放回缓存中
cache.put(start, factors);
} else {
factors = cache.get(i);
System.out.print("由于已经有缓存,所以从缓存中取数据: ");
}
问题就出在 if (cache.get(i) == null) 这个先判断再操作的竞态条件上,这个类已经不是线程安全的了。
线程安全
当一个类的对象在多个线程中表现均一致时候可以称为线程安全,比如说不可变类,是绝对的线程安全。或者
线程中不存在共享的可以同时被被其他多个线程修改的变量,也不存在竞态条件。
刚刚那个类中的map已经是线程间共享的可以被多个线程操作和修改的变量,且存在竞态条件,因为在多线程下可能出现不可预知的情况。
这里就是多个线程同时进入到map.get(i)==null这里判断,发现是null,就都进去进行计算了,于是缓存失效
解决方案一 synchronized同步?
synchronized (this) {
if (cache.get(i) == null) { //竞态条件
while (j <= i) {
if (i % j == 0) {
i = i / j;
factors.add(j);
} else {
j++;
}
k++;
}
//计算完之后放回缓存中
cache.put(start, factors);
} else {
factors = cache.get(i);
System.out.print("由于已经有缓存,所以从缓存中取数据: ");
}
}
简单粗暴的用synchronized将这段代码包起来,结果就是虽然缓存生效了,但是实际上这个代码已经退化成了单线程的代码,所有线程到了这一步关键的运算过程里,就都堵塞了,和单线程没有什么两样
解决方案二 map.get(i) 换成原子操作?*
//用map作为缓存
private Map<Long,List<Long>> cache=new ConcurrentHashMap<>();
使用ConcurrentHashMap代替hashMap, 这样map.get(i)操作将会加锁,同一时刻只有一个线程可以进去
但是事实上可以看到
if (cache.get(i) == null) { //get为原子操作的情况下,竞态条件消除
//但是下面的代码是一个非常耗费时间的运算
while (j <= i) {
if (i % j == 0) {
i = i / j;
factors.add(j);
} else {
j++;
}
k++;
}
//计算完之后放回缓存中
cache.put(start, factors);
}
get为原子操作情况下,竞态条件是消除了,但是下面的运算是个非常耗费时间的操作,因此,当A线程进入发现缓存为空,于是自己去计算,但是实际上很可能这个计算任务已经有别的线程在执行了,A的最优策略应该是等待这个任务结束 ,然后去缓存里拿,而不是自己继续去执行。
但是这个也比之前的情况下好。
理想状态,判断条件同一个时刻只有一个线程在执行,并且如果发现任务已经在执行了,则等待该任务,而不是自己去计算,因此使用furture是非常合适的
Furture
在并发编程中,我们经常用到非阻塞的模型,在之前的多线程的三种实现中,不管是继承thread类还是实现runnable接口,都无法保证获取到之前的执行结果。通过实现Callback接口,并用Future可以来接收多线程的执行结果。
Future表示一个可能还没有完成的异步任务的结果,针对这个结果可以添加Callback以便在任务执行成功或失败后作出相应的操作。
Furture的demo
建立实现了callable接口的任务类
public class FutureTest {
//实现Callable<T>接口 T为call方法的返回值
public static class Task implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("execute!!!");
return "complete";
}
}
使用如下
public class FutureTest {
public static void main(String[] args) throws InterruptedException,
ExecutionException {
System.out.println("futrue任务开始");
//建立实现了callable接口的任务
Task task=new Task();
//将实现了callble接口的任务放到Future里面
//FutureTask<T> T是实现了Callable<T>接口的call() 方法的返回值T
FutureTask<String> futureTask=new FutureTask<String>(task);
//将future放到线程thread里面
Thread t=new Thread(futureTask);
//启动线程
t.start();
System.out.println("线程启动");
//获取线程执行之后结果输出
System.out.println("future任务结束,输出:"+futureTask.get());
}
}
输出如下
futrue任务开始
线程启动
execute!!!
future任务结束,输出:complete
可以看到通过FutureTask.get()获取到了线程里面call方法的返回值
因为计算因数分解的任务耗时长,所以我们可以在map中缓存这个任务,当新的线程进来,看到map中已经有这个任务了,则等待这个任务结算,而不是继续自己开始一个计算任务。
具体实现:
private Map<Long, Future<List<Long>>> cache = new ConcurrentHashMap<Long, Future<List<Long>>>();
优化之后的代码如下
1.首先实现callble接口的task,这部分负责因数分解的任务核心计算
public class factorsTask implements Callable<List<Long>> {
private Long i = 0L; //要进行分解的数
public factorsTask(Long i) {
this.i = i;
}
@Override
public List<Long> call() throws Exception {
List<Long> factors = new ArrayList<Long>();
Long j = 2L;
while (j <= i) {
if (i % j == 0) {
i = i / j;
factors.add(j);
} else {
j++;
}
}
return factors;
}
}
2.调用callble任务的service
public class factorsService {
private Map<Long, Future<List<Long>>> cache = new ConcurrentHashMap<Long, Future<List<Long>>>();
//建立一个因式分解服务
public List<Long> Service(Long i) {
List<Long> factors = factor(i);
return factors;
}
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>();
if (cache.get(i) == null) {
//如果缓存为空,则建立一个新的计算任务,并放到缓存中
//然后启动新的线程去进行计算
factorsTask factorsTask = new factorsTask(i);
FutureTask<List<Long>> future = new FutureTask<List<Long>>(factorsTask);
Thread t1 = new Thread(future);
t1.start();
System.out.println("放入到缓存中");
cache.put(start, future);
}
//从缓存中取出该任务
Future<List<Long>> futureR = cache.get(i);
try {
//获取该任务的结果
factors = futureR.get();
System.out.println("从缓存中取值");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
//同步下输出 这部分花不了什么时间
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);
}
System.out.println();
System.out.println("验证:分解出来因数相乘结果为:" + var);
long endtime = System.currentTimeMillis();
System.out.println("此线程耗时" + (endtime - begintime) + "毫秒");
}
return factors;
}
}
3.启动service的thread
public class factorsThread implements Runnable {
private CountDownLatch latch;
public factorsThread(CountDownLatch latch) {
this.latch = latch; //初始化闭锁
}
demo2.demo1Modify.factorsService factorsService;
public void setFactorsService(factorsService factorsService) {
this.factorsService = factorsService;
}
Long num = 0L;
public void setNum(Long num) {
this.num = num;
}
@Override
public void run() {
factorsService.Service(num);
latch.countDown(); //执行完run后减1
}
}
4.启动thread的main
public static void main(String[] args) {
factorsService factorsService = new factorsService();
List<Long> nums = Arrays.asList(1323999999999L, 1323999999999L, 1323999999999L, 1323999999999L, 1323999999999L,13239999999979L,13239999999979L); //51323999999999L, 51323999999999L, 51323999999998L);
final CountDownLatch latch = new CountDownLatch(7);
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) + "毫秒");
//多线程执行完成 5个任务 无缓存 10299毫秒
}
运行结果
多线程的方式启动计算:
放入到缓存中
放入到缓存中
放入到缓存中
放入到缓存中
放入到缓存中
放入到缓存中
放入到缓存中
从缓存中取值
13239999999979的因数分解为: 139X2909X32743829 此线程耗时1871毫秒
从缓存中取值
13239999999979的因数分解为: 139X2909X32743829 此线程耗时1875毫秒
从缓存中取值
13239999999979的因数分解为: 139X2909X32743829 此线程耗时1888毫秒
从缓存中取值
1323999999999的因数分解为: 3X3X3X131X374328527 此线程耗时7834毫秒
从缓存中取值
1323999999999的因数分解为: 3X3X3X131X374328527 此线程耗时7841毫秒
从缓存中取值
1323999999999的因数分解为: 3X3X3X131X374328527 此线程耗时7841毫秒
此线程从缓存中取值
1323999999999的因数分解为: 3X3X3X131X374328527 此线程耗时7849毫秒
多线程任务计算完成!共耗时7852毫秒
从上面我们可以看到,对13239999999979和1323999999999进行因数分解,两个都有重复的,结果仍然没有走缓存。
理想的情况下,每个数字是只放入一次缓存。
这里这都放了。总共耗时7852秒
问题出在
if (cache.get(i) == null) {
//如果缓存为空,则建立一个新的计算任务,并放到缓存中
//然后启动新的线程去进行计算
factorsTask factorsTask = new factorsTask(i);
FutureTask<List<Long>> future = new FutureTask<List<Long>>(factorsTask);
Thread t1 = new Thread(future);
t1.start();
System.out.println("放入到缓存中");
cache.put(start, future);
}
(cache.get(i) == null这里虽然是ConcurrentHashMap,get操作是原子的,一次只能一个线程进去,但是后面的操作,仍然是需要耗费时间的,当线程切换的时候,新的任务还没被放入到cache里面
解决方法1 synchronized同步
代码如下
synchronized (this) {
if (cache.get(i) == null) {
factorsTask factorsTask = new factorsTask(i);
FutureTask<List<Long>> future = new FutureTask<List<Long>>(factorsTask);
Thread t1 = new Thread(future);
t1.start();
System.out.println("放入到缓存中");
cache.put(start, future);
}
}
输出结果
多线程的方式启动计算:
放入到缓存中
放入到缓存中
从缓存中取值
13239999999979的因数分解为: 139X2909X32743829 耗时1042毫秒
从缓存中取值
从缓存中取值
13239999999979的因数分解为: 139X2909X32743829 此线程耗时1047毫秒
13239999999979的因数分解为: 139X2909X32743829 此线程耗时1048毫秒
从缓存中取值
从缓存中取值
从缓存中取值
1323999999999的因数分解为:
从缓存中取值
3X3X3X131X374328527 耗时4964毫秒
1323999999999的因数分解为: 3X3X3X131X374328527 此线程耗时4964毫秒
1323999999999的因数分解为: 3X3X3X131X374328527 此线程耗时4962毫秒
1323999999999的因数分解为: 3X3X3X131X374328527 此线程耗时4963毫秒
多线程任务计算完成!耗时共4965毫秒
效果很显然,耗时4965秒 ,每个数字只有一次放入缓存,并且存在多种不同的数字的时候也很效 ,(不同于demo1的串行,如果是多个不同的数字时候就成了串行)
解决方法2 使用putIfAbsent
putIfAbsent()是ConcurrentHashMap的方法,是一个原子操作,判断是否为空,为空则放入值,并返回null ,这样就消除了上面的竞态条件
//---方案二--使用putIfAbsent
factorsTask factorsFuture = new factorsTask(i);
FutureTask<List<Long>> future = new FutureTask<List<Long>>(factorsFuture);
if (cache.putIfAbsent(i,future) == null) {
Thread t1 = new Thread(future);
t1.start();
System.out.println("放入到缓存中");
}
运行结果
多线程的方式启动计算:
放入到缓存中
放入到缓存中
从缓存中取值
从缓存中取值
从缓存中取值
13239999999979的因数分解为: 139X2909X32743829 此线程耗时928毫秒
13239999999979的因数分解为: 139X2909X32743829 此线程耗时929毫秒
13239999999979的因数分解为: 139X2909X32743829 此线程耗时928毫秒
从缓存中取值
从缓存中取值
从缓存中取值
从缓存中取值
1323999999999的因数分解为: 3X3X3X131X374328527 此线程耗时5390毫秒
1323999999999的因数分解为: 3X3X3X131X374328527 此线程耗时5393毫秒
1323999999999的因数分解为: 3X3X3X131X374328527 此线程耗时5393毫秒
1323999999999的因数分解为: 3X3X3X131X374328527 此线程耗时5392毫秒
多线程任务计算完成!耗时共5393毫秒
耗时和方案1差不多(些许时间差别与当前的系统情况有关),然后也是每个数字只走了一次缓存
多线程初级入门大概如上,实现一个线程有三种方式 实现runable接口,callable接口,以及继承Thread类。

本文探讨了多线程在因数分解任务中的应用,通过引入缓存机制和优化并发策略,显著提高了程序效率。分析了线程安全问题及解决方案,演示了Future和Callable接口的使用。
9374

被折叠的 条评论
为什么被折叠?



