多线程编程核心技术(七)安全性、活跃性以及性能问题

多线程的思想是不让CPU空跑。JVM的特点之一是堆可共享——>安全问题,CPU,L1,L2,L3闪电缓存——>不一致。

所以并发编程中需要注意的问题: 安全性问题、活跃性问题和性能问题。

安全性问题指:可见性,原子性,有序性

其实只有一种情况需要:存在共享数据并且该数据会发生变化,通俗地讲就是有多个线程会同时读写同一数据。那如果能够做到不共享数据或者数据状态不发生变化,不就能够保证线程的安全性了嘛。有不少技术方案都是基于这个理论的,例如线程本地存储(Thread Local Storage,TLS)、不变模式等等。

当多个线程同时访问同一数据,并且至少有一个线程会写这个数据的时候,如果我们不采取防护措施,那么就会导致并发 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)      
    }
  }
}

我们加个锁保护一下就能解决所有的并发问题了呢?显然没有这么简单。例如,对于上面示例,我们稍作修改,增加两个被 synchronized 修饰的 get() 和 set() 方法, add10K() 方法里面通过 get() 和 set() 方法来访问 value 变量,修改后的代码如下所示。对于修改后的代码,所有访问共享变量 value 的地方,我们都增加了互斥锁,此时是不存在数据竞争的。但很显然修改后的 add10K() 方法并不是线程安全的。

这种问题,有个官方的称呼,叫竞态条件(Race Condition)。所谓竞态条件,指的是程序的执行结果依赖线程执行的顺序。例如上面的例子,如果两个线程完全同时执行,那么结果是 1;如果两个线程是前后执行,那么结果就是 2。

关于这个问题在看一部书的时候,提到过,这属于不安全的发布。

安全发布对象有以下四种方法:
1、在静态初始化函数中初始化一个对象引用
2、将对象的引用保存到volatile类型域或者AtomicReference对象中
3、将对象的引用保存到某个正确构造的final类型域中
4、将对象的引用保存到一个由锁保护的域中

不安全的意思就是没有使得对象得到线程安全的保证,无法时时刻刻满足可见性。

逻辑上可以这么做

if (状态变量 满足 执行条件) {
  执行操作
}

例如上面的代码可以进行double check来提高对状态变量的满足。

当某个线程发现状态变量满足执行条件后,开始执行操作;可是就在这个线程执行操作的时候,其他线程同时修改了状态变量,导致状态变量不满足执行条件了。

当然最简单的还是使用互斥的方式来进行,CPU 提供了相关的互斥指令,操作系统、编程语言也会提供相关的 API。从逻辑上来看,我们可以统一归为:锁。

锁可以解决的一个是竞争问题,一个是顺序问题,通过隔离性就可以完成安全性问题。

活跃性问题就是死锁问题只需要破坏掉条件就行了

1.互斥

2.持有并等待

3.等待不释放

4.不可抢占

性能问题

锁带来的问题比如线程之间的协调、上下文切换、线程的创建和销毁、线程调度等问题

不过从方案层面,我们可以这样来解决这个问题。

第一,既然使用锁会带来性能问题,那最好的方案自然就是使用无锁的算法和数据结构了。在这方面有很多相关的技术,例如线程本地存储 (Thread Local Storage, TLS)、写入时复制 (Copy-on-write)、乐观锁等;Java 并发包里面的原子类也是一种无锁的数据结构;Disruptor 则是一个无锁的内存队列,性能都非常好……

第二,减少锁持有的时间。互斥锁本质上是将并行的程序串行化,所以要增加并行度,一定要减少持有锁的时间。这个方案具体的实现技术也有很多,例如使用细粒度的锁,一个典型的例子就是 Java 并发包里的 ConcurrentHashMap,它使用了所谓分段锁的技术(这个技术后面我们会详细介绍);还可以使用读写锁,也就是读是无锁的,只有写的时候才会互斥。

性能方面的度量指标有很多,我觉得有三个指标非常重要,就是:吞吐量、延迟和并发量。吞吐量:指的是单位时间内能处理的请求数量。吞吐量越高,说明性能越好。延迟:指的是从发出请求到收到响应的时间。延迟越小,说明性能越好。并发量:指的是能同时处理的请求数量,一般来说随着并发量的增加、延迟也会增加。所以延迟这个指标,一般都会是基于并发量来说的。例如并发量是 1000 的时候,延迟是 50 毫秒。

原文地址:https://www.cnblogs.com/SmartCat994/p/14201774.html