并发编程中我们注意的方面有很多,但是最主要的还是这三个方面:安全性问题、活跃性问题、性能问题。
安全性问题
线程安全是什么?其实简单来说就是**线程能按照我们的期望来执行。**但是在并发编程中却没那么美好,在并发编程学习(2) —— 并发编程Bug源头中我们介绍了并发Bug的主要源头:
- 可见性
- 原子性
- 有序性
一般在存在共享变量并且该变量会改变,同时有至少一个线程写这个变量这种情况下才会考虑这三种情况,也可以称这种情况为数据竞争。除此之外,程序的执行结果是依赖于线程执行的顺序,也就是所谓的竞态条件,下面用代码来举例:
public class ThreadTest {
int values = 0;
sychronized int getValues(){
return values;
}
sychronized void addOne(){
values += 1;
}
}
如果有两个线程同时调用get()+1,最终values的结果不是2,而是1,因为synchronized只能保证get()这个操作的安全性,不能保证get()+1这个组合操作的安全性,因此只有当线程串行化时就能解决这些问题,JAVA的解决方法里就提供了锁。
活跃性问题
活跃性问题指的是线程因为某种原因而导致无法执行下去。线程的活跃性问题除了前面介绍的死锁外,还有活锁和饥饿。
活锁
比起死锁那种你我互不相让的情况,活锁就是双方互相其谦让引起的冲突。现实中有甲乙两个人,做什么都互相谦让,你从东门出我在西门出,但有一天,在甲乙两人在路上碰到,甲往左侧挪想让乙先过,乙往右侧挪想让甲先过,这样就会造成相撞,在现实中甲乙能够通过沟通来解决这种情况,但在程序中,只会继续相撞下去,造就成所谓的活锁。
那么怎么解决活锁?最好的方法就是设置一个等待随机时间,甲乙相撞各自设置一个等待随机时间,甲乙在各自的随机时间结束后挪到另一方,这样就能解决活锁问题。
饥饿
引起饥饿问题最主要的是线程的优先级。优先级低的线程很多时候会被优先级高的线程取代,造成优先级低的永远无法执行,造成“饥饿”。另外长时间持有锁也会造成“饥饿”问题。
那么解决饥饿的方法有以下三种:
- 避免持有锁长时间的执行。
- 公平的分配资源。
- 保证资源充足
性能问题
性能问题主要考察的是程序三个方面:
- 并发量:同一时刻处理多个请求,并发量越多,延迟也越多。
- 吞吐量:单位时间内处理的请求数量越高性能越好。
- 延迟:从发出请求到响应的时间。一般时间越短性能越好。
总结
在一开始只想提高CPU使用率,到后来优化后带来的种种问题,说明很多东西都有两面性,并不是所有优化后带来的效果会比优化前好,同时还要考量优化后可能带来问题,如何规避这些问题都是非常重要的。