安全性
并发bug三大源头
源头
- 原子性问题
- 可见性问题
- 有序性问题
bug风险点
存在共享数据并且该数据会发生变化(即多个线程会同时读写同一数据)
分类
-
数据竞争
当多个线程同时访问同一数据,并且至少有一个线程会写这个数据。假设 count=0,当两个线程同时执行 get() 方法时,get() 方法会返回相同的值 0,两个线程执行 get()+1 操作,结果都是 1,之后两个线程再将结果 1 写入了内存。你本来期望的是 2,而结果却是 1。
-
竞态条件
程序的执行结果依赖线程执行的顺序。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); } } }
假设 count=0,如果两个线程完全同时执行,那么结果是 1;如果两个线程是前后执行,那么结果就是 2。
在并发环境里,线程的执行顺序是不确定的,如果程序存在竞态条件问题,那就意味着程序执行的结果是不确定的,而执行结果不确定这可是个大 Bug。
活跃性
指某个操作无法执行下去。
死锁
线程会互相等待,而且会一直等待下去。
活锁
线程间资源冲突激烈,引起线程不断的尝试获取资源,不断的失败。活锁有可能自己解开。
解决方案:等待一个随机时间。
饥饿
线程因无法访问所需资源而无法执行下去的情况。
在 CPU 繁忙的情况下,优先级低的线程得到执行的机会很小,就可能发生线程“饥饿”;持有锁的线程,如果执行的时间过长,也可能导致“饥饿”问题。
3种解决方案:
- 保证资源充足
- 公平分配资源
- 避免持有锁的线程长时间执行
方案1和3的适用场景有限,一般2多点,如公平锁。
公平锁:先来后到的方案,线程的等待是有顺序的,排在等待队列前面的线程会优先获得资源
性能问题
“锁”的过度使用可能导致串行化的范围过大,降低性能。
阿姆达尔(Amdahl)定律:处理器并行运算之后效率提升的能力
n 可以理解为 CPU 的核数,p 可以理解为并行百分比,那(1-p)就是串行百分比。
假设 CPU 的核数(也就是 n)无穷大,那加速比 S 的极限就是 20。
性能解决方案
-
使用无锁的算法和数据结构
如线程本地存储 (Thread Local Storage, TLS)、写入时复制 (Copy-on-write)、乐观锁等;Java 并发包里面的原子类也是一种无锁的数据结构;Disruptor 则是一个无锁的内存队列 -
减少锁持有的时间
互斥锁本质上是将并行的程序串行化,所以要增加并行度,一定要减少持有锁的时间。如使用细粒度的锁,一个典型的例子就是 Java 并发包里的 ConcurrentHashMap,它使用了所谓分段锁的技术;还可以使用读写锁,也就是读是无锁的,只有写的时候才会互斥。
性能指标
-
吞吐量:指的是单位时间内能处理的请求数量。吞吐量越高,说明性能越好。
-
延迟:指的是从发出请求到收到响应的时间。延迟越小,说明性能越好。
-
并发量:指的是能同时处理的请求数量,一般来说随着并发量的增加、延迟也会增加。所以延迟这个指标,一般都会是基于并发量来说的。例如并发量是 1000 的时候,延迟是 50 毫秒。