小结篇:并发编程的问题

·  阅读 248
小结篇:并发编程的问题

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第22天,点击查看活动详情

本系列专栏 Java并发编程专栏 - 元浩875的专栏 - 掘金 (juejin.cn)

前言

在前面几篇文章中,我们说了并发编程问题:可见性、原子性和有序性问题,然后通过Java内存模型和synchronized关键字来解决这些问题,后面又介绍了使用"等待-通知"机制来优化代码。那这篇文章我们来做个小结。

正文

这里还是主要来说一下并发编程中会遇到的问题,很庆幸的是有人帮我们总结了,主要是三个方面:安全性问题,活跃性问题和性能问题。我们挨个来说一下。

安全性问题

我们经常听见这样的描述,比如这个方法不是线程安全,这个类不是线程安全,那啥是线程安全呢?本质就是正确性,而正确性的含义就是程序按照我们期望的执行,不要出现意外

而这个安全性问题在之前的文章我们已经说了并发Bug的三个主要源头:原子性问题、可见性问题和有序性问题,也就是理论上说避免这3个问题,这个程序就是安全的。

那是不是所有的代码都要认真分析一遍是否存在安全性问题呢 当然不是,其实只有一种情况需要:存在共享数据并且该数据会发生变化,通俗地讲就是多个线程会同时读写同一个数据。那如果做到不共享数据或者数据状态不发生变化,不就可以保证线程的安全性了嘛。有不少方案还真是基于这个理论,例如线程本地存储、不变模式等。

多个线程同时访问同一数据,并且至少有一个线程会写这个数据的时候,如果我们不采取防护措施,那么就会导致并发Bug,对这种情况有个专业术语叫做数据竞争

比如下面之前说过代码,给一个共享变量自增1万次:

public class Test {
  private long count = 0;
  void add10K() {
    int idx = 0;
    while(idx++ < 10000) {
      count += 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变量的读和写都加了锁,那add10K方法能按照预期的执行吗,这个方法也不是线程安全的。假设count=0,这时有2个线程先后执行get()方法,获取的值都是0,然后先后写入内存,这样结果还是1,但是我们期望的是2。

这种问题,有个官方的称呼,叫做竞态条件,即指的是程序的执行结果依赖线程执行的顺序。而解决这个问题,之前的文章,我们都仔细介绍过,就不再赘述。

活跃性问题

所谓活跃性问题指的就是在某些情况下,操作无法执行下去,比如前面2篇文章说的死锁,就是典型的活跃性问题。除了死锁外,还有俩种情况,分别是"活锁"和"饥饿"。

活锁

前面文章我们知道死锁发生后,线程会互相等待,而且会一直等下去,在原理上我们知道就是线程一直阻塞下去了。但是在有时虽然没有发生阻塞,但仍然会出现执行不下去的情况,这就是所谓的活锁

这个可以类比到现实世界中的情况,路人甲从左手出门,路人乙从右手进门,俩人为了不相撞,互相谦让,路人甲让路走右手边,路人乙也让路走左手边,结果俩人又相撞了。在现实世界中,俩个人会交流一下即可,但是在编程世界中,2个线程就有可能一直谦让下去,造成活锁。

而解决这个问题也非常简单,就等待一个随机时间即可。比如上面路人甲和路人乙都等待一个随机时间去换边走,由于随机数相同的概率极低,所以就不会再相撞了。

饥饿

所谓饥饿指的是线程因无法访问所需的资源而无法执行下去的情况,比如线程优先级不均匀,在CPU繁忙的情况下,优先级较低的线程得到执行的概率就很小;或者持有锁的线程,执行时间很长,也可能导致饥饿问题。

解决饥饿问题大概方案有3个:

  1. 保证资源充足,但是这种场景有限,一般资源的稀缺无法解决;
  2. 公平地分配资源,也就是公平锁,是一种先来后到地方案,线程等待是有顺序地,排在前面地线程会优先获得资源。
  3. 避免持有锁地线程长时间运行,这个就和具体业务有关了,进行优化;

性能问题

虽然使用锁可以解决并发的问题,但是锁使用过多的话就会导致性能问题,因为会导致串行化的范围过大,就发挥不出来多线程的优势了。

而解决这个问题,大致可以从2个方面来解决:

  1. 使用无锁的算法和数据结构,比如线程本地存储、写入时复制、乐观锁等,还有Java原子类也是一种无锁的数据结构等。
  2. 减少锁持有的时间,而这个方案最常见的措施就是使用细粒度锁,比如Java并发包中的ConcurrentHashMap,就是使用了分段锁的技术。

总结

并发编程是个很复杂的计算,微观上涉及原子性问题、可见性问题和有序性问题宏观上则表现为安全性、活跃性以及性能问题,所以在代码设计时,要时刻注意数据竞争和竞态条件,活跃性要注意死锁、活锁和饥饿,以及性能。

分类:
Android
标签:
分类:
Android
标签:
收藏成功!
已添加到「」, 点击更改