安全性、活跃性以及性能问题
安全性
线程安全
程序按照我们期望的执行
并发 Bug 的三个主要源头
- 原子性
- 可见性
- 有序性
只有存在共享数据并且该数据会发生变化,通俗地讲就是有多个线程会同时读写同一数据时,要考虑是否线程安全。
数据竞争
当多个线程同时访问同一数据,并且至少有一个线程会写这个数据的时候,如果我们不采取防护措施,那么就会导致并发 Bug,对此还有一个专业的术语,叫做数据竞争(Data Race)
竞态条件
public class Test {
private long count = 0;
synchronized long get(){
return count;
}
synchronized void set(long v){
count = v;
}
void add10K() {
int idx = 0;
while(idx++ < 10000) {
set(get()+1)
}
}
}
上述代码中,add10K仍然时非线程安全的。
当线程A调用add10K(),通过get()或得到count=0,然后释放锁。线程B也在这时候调用add10K(),通过get()也会得到count=0。那么最终执行两个add10K()但是结果count还是等于1.
这种问题官方的称呼叫竞态条件(Race Condition)。所谓竞态条件,指的是程序的执行结果依赖线程执行的顺序。
活跃性问题
除了死锁外,还有两种情况,分别是“活锁”和“饥饿”。
活锁
有时线程虽然没有发生阻塞,但仍然会存在执行不下去的情况,这就是所谓的“活锁”,例如两个线程不断互相让锁
解决方法就是再让锁的时候赋予随机时间
饥饿
饥饿”指的是线程因一直无法访问所需资源而无法执行下去的情况
可能是优先级太低,其他线程优先级高并且源源不断,则低优先级线程饥饿。
解决方法:
- 保证资源充足
- 公平地分配资源
- 避免持有锁的线程长时间执行
第一点和第三点很少情况能解决,公平地分配资源较多。
主要使用公平锁:来后到的方案,线程的等待是有顺序的,排在等待队列前面的线程会优先获得资源。
性能问题
锁范围太大,会影响性能
锁用太多,也会影响性能
阿姆达尔(Amdahl)定律
用来代表处理器并行运算之后效率提升的能力
S=1 / [(1−p) + p/n]
公式里的 n 可以理解为 CPU 的核数,p 可以理解为并行百分比,那(1-p)就是串行百分比了,也就是我们假设的 5%。我们再假设 CPU 的核数(也就是 n)无穷大,那加速比 S 的极限就是 20。也就是说,如果我们的串行率是 5%,那么我们无论采用什么技术,最高也就只能提高 20 倍的性能。
提高性能的方法
- 减少锁:使用无锁算法和数据结构。
例如:线程本地存储 (Thread Local Storage, TLS)、写入时复制 (Copy-on-write)、乐观锁等;Java 并发包里面的原子类也是一种无锁的数据结构;Disruptor 则是一个无锁的内存队列,性能都非常好
- 减少锁持有的时间
使用细粒度的锁,一个典型的例子就是 Java 并发包里的 ConcurrentHashMap,它使用了所谓分段锁的技术;还可以使用读写锁,也就是读是无锁的,只有写的时候才会互斥。
度量指标
- 吞吐量:指的是单位时间内能处理的请求数量。吞吐量越高,说明性能越好。
- 延迟:指的是从发出请求到收到响应的时间。延迟越小,说明性能越好。
- 并发量:指的是能同时处理的请求数量,一般来说随着并发量的增加、延迟也会增加。所以延迟这个指标,一般都会是基于并发量来说的。例如并发量是 1000 的时候,延迟是 50 毫秒。
github: https://github.com/Hikiy
作者:Hiki
创建日期:2019.07.10
更新日期:2019.07.10
(转载本站文章请注明作者和出处 Hiki)