我要学并发-并发问题产生的源头

时间:2019-10-23 00:32:31   收藏:0   阅读:123

本文从计算机系统层面来讲述在提升性能的过程中,引发的一系列问题。读完本文你将get到并发编程过程中的原子性,可见性,有序性三大问题的来源。

随着硬件发展速度的放缓,摩尔定律已经不在生效,各个硬件似乎已经到了瓶颈;然而随着互联网的普及,网民数量不断增加,对系统的性能带来了巨大的挑战。因此我们要通过各种方式来压榨硬件的性能,从而提高系统的性能进而提升用户体验,提升企业的竞争力。

由于CPU,内存,IO三者之间速度差异,为了提高系统性能,计算机系统对这三者速度进行平衡。

缓存导致得可见性的问题

一个线程对共享变量的修改,另外一个线程能够立刻看到,我们称为可见性。
技术分享图片

多核时代,每颗 CPU 都有自己的缓存,这时 CPU 缓存与内存的数据一致性就没那么容易解决了,当多个线程在不同的 CPU 上执行时,这些线程操作的是不同的 CPU 缓存。 技术分享图片

线程切换带来的原子性问题

由于IO和cpu执行速度的差别巨大,所以在早期操作系统中就发明的多线程,即使在单核的cpu上我们也可以一遍听着歌,一边写着bug,这就是多线程。
todo 图例
早期操作系统基于进程来调度cpu, 不同进程间是不共享内存空间的,所以进程要做任务切换要切换内存映射地址,而一个进程创建的所有线程都是共享一个内存空间,所以线程做任务切换成本很低。现代操作系统都基于更轻量级的线程来调度,现在我们提到的“任务切换”都是指“线程切换”。

因为式单核cpu,所以同一时刻只能执行一个任务,所以多线程通常使用抢占的方式来获取操作系统的时间片。
技术分享图片

Java的并发编程中都是基于多线程,线程的切换时机通常在一条cpu指令执行完毕之后,而Java作为一门高级编程语言,通常一条语句可能由多个cpu指令来完成。例如:count += 1, 至少需要三条指令。

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

为了提高程序的执行效率,编译器有时会在编译过程中对程序的进行优化,从而改变程序的执行顺序。如程序“a = 4; b = 5”,在优化后执行顺序可能变成“b = 5; a = 4”。通常进行一项优化过程中可能会带来另一项问题,改变程序的执行顺序通常也会导致让人意想不到的bug。

Java领域中一个经典的案例就是利用双重检查创建单例对象,代码如下:在获取实例getInstance()方法中,我们首先判断instance是否为空,如果为空则锁住Singleton.class对象并再次检查instance是否为空,如果仍然为空则创建Singleton的一个实例。

public class Singleton {
static Singleton instance;
/**
  * 获取Singleton对象
  */
public static Singleton getInstance(){
    if (instance == null) {
        synchronized(Singleton.class) {
            if (instance == null)
                instance = new Singleton();
            }
        }
    return instance;
    }
}

假设有两个线程 A、B 同时调用 getInstance() 方法,他们会同时发现?instance == null,于是同时对 Singleton.class 加锁,此时 JVM 保证只有一个线程能够加锁成功(假设是线程 A),另外一个线程则会处于等待状态(假设是线程 B);线程 A 会创建一个 Singleton 实例,之后释放锁,锁释放后,线程 B 被唤醒,线程 B 再次尝试加锁,此时是可以加锁成功的,加锁成功后,线程 B 检查?instance == null?时会发现,已经创建过 Singleton 实例了,所以线程 B 不会再创建一个 Singleton 实例。

以上过程仅仅是我们的理想情况下,但是实际过程中往往会创建多次Singleton实例。原因是创建一个对象需要多条cpu指令,且编译器可能对这几条指令进行了排序。在执行new语句创建一个对象时,通常会包含一下三个步骤(此处进行了简化,实际实现过程会比此过程复杂):

  1. 在堆内存为对象分配一块内存M
  2. 在内存M区域进行Singleton对象的初始化
  3. 将内存M地址赋值给instance变量。
    但是实际优化后的执行顺序可能时以下这种情况:
  4. 在堆内存为对象分配一块内存M
  5. 将内存M地址赋值给instance变量。
  6. 在内存M区域进行Singleton对象的初始化
    假设A,B线程同时执行到了getInstance()方法,线程A执行完instance = $M(将内存M地址赋值给instance变量,但是未将对象进行初始化)后切换到B线程,当B线程执行到instance == null时,由于instance已经指向了内存M的地址,所以会返回false,直接返回instance,如果我们这是访问instance中的成员变量或者方法时就可能会出现NullPointException。
    技术分享图片

总结

在操作系统平衡CPU,内存,IO三者速度差异过程中进行了一系列的优化。

这三个不同方面的优化也带来了可见性,原子性,有序性等问题,他们通常是并发程序的bug的源头。

原文:https://www.cnblogs.com/liqiangchn/p/11723602.html

评论(0
© 2014 bubuko.com 版权所有 - 联系我们:wmxa8@hotmail.com
打开技术之扣,分享程序人生!