并发编程下的三大问题根源(原子性,可见性,有序性)和各种并发问题

本篇不讲解决方式,只是提出并发编程的问题,下面看一下并发问题的结构
在这里插入图片描述

问题根源:可见性,原子性,有序性。

缓存导致的可见性问题

结合上图来看一下可见性问题

int count=0
Thread th1 = new Thread(()->{ count+=1000; });
Thread th1 = new Thread(()->{ count+=1000; });
count=?

直觉告诉我们应该是2000,但是不对,可能是2000,也有可能是1000.
下面我们来结合下图解释一下,其实他是cpu缓存导致的问题,当线程A从内存中读取count值到自己cpu的缓存中,对缓存中的count+1000,但是还没写回内存,此时如果线程B进行+1000操作,从内存中读到自己所在cpu的缓存中,因为内存没有变,还是1000,然后+1000,然后两个线程在进行写回内存的操作,就变成覆盖(count=2000执行两遍)了
在这里插入图片描述

线程切换导致的原子性问题

线程切换:CPU通过给每个线程分配CPU时间片来执行,时间片是CPU分配给每个线程执行的时间,时间片结束,可能该线程还没有执行完,就会保存该线程的当前状态,把cpu的资源给其他线程,让其他线程执行。

执行一条计算机指令是原子操作的,但是java语言一条语句可能包含多条指令,比如count+=1.至少需要三条指令:

  1. 首先将count从内存中加载到cpu的寄存器中。
  2. 在寄存器中+1操作
  3. 把修改后的结果写入内存(因为可见性问题可能写入缓存,没有写入内存)
    如果cpu在执行线程A的第一条指令后,进行线程切换,执行线程B的count+=1的三条指令后,在切换回线程A完成2和3的指令,会发现又是覆盖。
    在这里插入图片描述

编译优化带来的有序性问题

其实对于java语言,我们平时写的java代码的执行顺序有可能并不是按照我们写的顺序来执行的,有可能编译器为了提升执行效率,会进行重排序,处理器也有可能为了提升性能,也会对编译器生成的指令做重排序。

比如利用双重检查单例模式

public class Singleton {
  static Singleton instance;
  static Singleton getInstance(){
    if (instance == null) {
      synchronized(Singleton.class) {
        if (instance == null)
          instance = new Singleton();
        }
    }
    return instance;
  }
}

假设两个线程同时访问getInstance方法,都通过第一个if语句,第二个因为加锁synchronized,所以只能一个线程进入,创建对象。释放锁,然后当第二个线程获得锁,进入一看instance已经!=null了,所以就不再实例化了,看起来完美。但是其中new Singleton 有个问题。
new 对象设计到三个操作:

  1. 分配一个内存M
  2. 在内存M上初始化对象Singleton
  3. 把内存M的地址赋给instance
    按照我们想象的执行顺序是这这样123步骤执行的,其实真正优化后的顺序是132.这样就产生问题了。
    假如在线程A在执行13后释放cpu资源执行其他线程,因为此时instance已经赋值,但是Singleton没有初始化。if检验到instance!=null,然后拿着instace去用,因为此时此时内存还没有初始化,所以用的时候会报空指针异常。
    在这里插入图片描述

并发问题

安全性

多个线程同时读写同一数据,就会存在安全性问题

当多个线程访问同一共享变量,并且存在存在线程修改这个变量,如果不加保护就会导致bug,叫做数据竞争

程序的执行结果依赖线程执行的顺序,叫竞态条件
因为线程执行顺序本身就是不确定的,所以如果不同的执行顺序(同时执行,一先一后)造成不同的结果。

扫描二维码关注公众号,回复: 10816371 查看本文章

面对数据竞争和竞态条件问题,使用,保持互斥。

活跃性

所谓活跃性问题,就是指某个操作无法执行下去,比如死锁,活锁,饥饿
死锁:就是两个线程执行一个操作,执行前需要同时获得两个锁才能执行方法,如果出现一个线程拿到一个锁,而另外一个锁别其他线程拿到了,因此两个线程互相等待对方锁,而产生的死锁问题。

死锁是因为获得锁不能在一定时间释放锁造成的,如果在死锁的情况下,一个线程拿到一个锁,在一定时间内没有获得另外一个锁,就释放当前拥有的锁。就可以解决死锁问题,但随之带来的就是活锁。

活锁:就是线程A拿到A锁,需要B锁,线程B拿到B锁,需要A锁,5秒钟拿不到对方的锁就释放当前拿到的锁,重新拿锁,就有可能又发生原来的情况,导致锁利用率低,这就是活锁。那如何解决呢?

解决活锁问题很简单,1.就是等待一个随机时间获得不了对方的锁再释放自己的锁 2.等待一个随机时间获得锁。

饥饿:就是有些线程优先级分配不均,就比如有些锁是非公平性的,造成某些线程可能一直得不到cpu的执行权,这就是饥饿。

解决方法:主要是使用公平性锁(所谓公平就是线程的等待顺序是有序的,先来后到)

性能

其实为了提高性能在保证安全性的前提下

  1. 可以使用无锁机制,比如MVCC(数据库事务的并发版本机制) ,TLS(本地线程存储),COW(copyandwrite,读不加锁,写时,复制修改返回)。JAVA SDK中的好多工具类都使用的无锁原理。
  2. 根据计算机的cpu和io性能创建合理的线程数,比如根据常见服务器可以分为io密集性和cpu密集性,
    对于io密集型(io时间执行时间相比cpu执行时间长):最佳线程数 =1 +(I/O 耗时 / CPU 耗时)
    对于cpu密集型(纯cpu计算):理论上 线程的数量 =CPU 核数
发布了34 篇原创文章 · 获赞 0 · 访问量 1089

猜你喜欢

转载自blog.csdn.net/qq_42634696/article/details/104727475
今日推荐