Java并发:安全性、活跃性以及性能问题

2019/07/10

安全性、活跃性以及性能问题

安全性

线程安全

程序按照我们期望的执行

并发 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

Post Directory