并发编程问题为什么都很诡异

时间:2022-07-28
本文章向大家介绍并发编程问题为什么都很诡异,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

并发编程对于很多人说都是比较难的,总是出现一些莫名其妙的bug,让我们很是苦恼,那么他到底是难在哪里呢,今天就带大家看看引起并发bug的根源

缓存引起的可见性问题

在单核时代,所有的线程都操作在一个cpu上执行,因此cpu缓存和内存的可见性很容易解决,如下图,线程A对变量V的改变之后,线程B是可以之间看到的,因此不存在可见性问题

但是在现如今的多cpu时代,往往就不那么容易了,如下图,当线程A修改了变量V之后,对于线程B是不可见的,两个线程各自操作不同的CPU缓存,比如,初始值V的值为0,当线程A修改了V=V+1,此时CPU1里面的变量的V的值是1,同时同步到主内存中,此时线程B获取到CPU2缓存的值是V=0,此时就会引起数据不一致,为什么线程A已经修改了V的值,而线程B看到的值还是V=0,这个就是可见性的问题

线程切换到时的原子性问题

在早期的时候,由于IO性能太差,就引进了多进程的操作,正如操作系统允许某个程序执行一小段时间,比如50毫秒,过了50毫秒就会执行另外一段程序,这里的50毫秒就是我们说的时间分。

在一个时间片中,如果一个进程执行IO操作,此时就可以把CPU让出来,让CPU执行其他程序,当到上一个Io读取操作完成,再次唤醒进程,就可以再次获取CPU的执行权限.由于早期这种多进程是不进行共享内存的,因此各自执行各自的互不影响,任务的切换仅仅切换内存映射地址,而现在的并发编程中,我们使用的多线程,进行任务调度,多线程任务切换成本比较低,但是多个线程之间是共享内存的,因此会带来一系类问题.这也是并发编程出现问题的源头之一.

正如我们在代码中写count+=1操作,他的完成并不是一条指令完成的,会分成三步,

  1. 首先,先要把内存的count加载到CPU寄存器中
  2. 然后在CPU寄存器中计算加1
  3. 然后同步到内存中

如上操作,在操作系统中,我们执行代码,一句代码并不是真正的一条指令,可能分了很多指令,如下图,线程A和线程B 同时执行代码count+=1,由于线程切换导致计算错误,我们期望的是2,但是实际上计算出来的值却是1.

线程切换的发生可以在count+=1之前也可以在之后,但是不可能发生在中间,我们往往把一个或多个操作在CPU指令中不被中断的特性叫做原子性,但是我们潜意识中任务一句代码就是一个原子操作,因此就会造成我们意想不到的问题。

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

我们程序中写的代码顺序往往并不是真正执行的顺序,如下声明变量

int a=7
int b=6

在代码编译之后的顺序就是下面这种

int b=6
int a=7

上面虽然顺序改变了,其实是并不影响结果,但是这种编译优化也会带来我们一向不到的问题,如下面代码,经典的单例模式代码

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

正常单线程并不会发生什么问题,但是在多线程中,就会产生意想不到的问题,正如两个线程A和B,同时调用getInstance(),两个线程同时到了第一个instance=null.且同时满足,然后到了synchroized中,此时只有一个线程能够获取到锁,假设是线程A获取到,然后线程A实例化instance,在释放锁,此时线程B获取到锁,然后发现实例instance已经不为null,就会直接返回,整体流程很是完美,实际上是有很大的问题,

首先,我们看一下new一个对象的正常过程

  1. 分配一块内存M
  2. 把内存M中初始化Singleton
  3. 把内存地址M复制给instance变量

但是经过编译优化之后,他的流程可能就是下面这样

  1. 分配一块内存M
  2. 把内存地址M复制给instance变量
  3. 把内存M中初始化Singleton

如果是优化后的流程,会引起什么问题呢,当我们的线程A在执行完第二步的时候,线程切换到了线程B,此时instance!=null,线程直接返回线程A创建的实例,但是此时的实例并没有进行初始化,因此在后面我们使用实例的属性的时候,就可能导致空指针。

最后我们把正确的单例模式的代码切出来(仅仅是在变量上加了一个volatile)

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

下次我们继续讲解有什么办法避免上面的问题,如果文章对你有一丝丝帮助麻烦点击关注,也欢迎转发或点赞,谢谢