• 【JAVA并发】一、并发问题产生的根源


    一个问题的产生,往往是各种原因导致的。如果不了解问题的源头,我们就没办法给出合理的解决方案,再说源头都不了解,还谈什么解决问题啊。今天,我们来浅析一下并发问题产生的原因。

    一、CPU缓存导致的可见性问题

    先说下题外话:缓存这个东西就是个双刃剑,合理运用缓存很重要!
    为了平衡CPU、内存之间的速度差,CPU的硬件设计师引用了CPU缓存这个东西。其实倒退个几十年,大部分服务器还是单CPU的情况下,这么设计问题也不大。但是在现在起步都是双核的情况下,问题就来了,我先来手工画一幅多CPU跟内存的交互模型图(个人理解,纯手工制作,大家将就看吧)
    在这里插入图片描述
    CPU在读数据的时候,会先去CPU缓存里面找,如果没有,再去内存里面取。可是CPU之间的缓存却是相对隔离的,这就导致了各个缓存之间缓存的数据并不是可见的。因此,并发问题的第一大坑就来了。
    顺带提一嘴这个问题的解决办法:就是禁用缓存,让每次都直接从内存里面去存取数据,不走缓存,不就可以解决这个问题了吗,java的具体解决方案留着后面再说。

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

    public class Singleton {
      static Singleton instance;
      static Singleton getInstance(){
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    看上面的代码是一个单例模式的懒汉式实现方式,如果现在有两个线程同时调用getInstanc()方法,就有可能会发生线程安全问题。为什么会产生这样的问题呢,是因为在我们正常的概念中,一个方法应该执行完毕才会发生线程切换,可是实际上却并不是这样的。甚至对于高级语言来说,一行代码的执行,实际上会解析成多条指令,CPU只会保证单条指令的原子性。一句话来说线程随时可能切换
    当线程A调用getInstance的时候,由于instance == null,会进行instance = new Singleton();的初始化,但是这个时候发生了线程切换,线程B也调用了 getInstance(),也判断出instance == null,这个时候线程A拿到的对象实例就跟线程B拿到的对象实例就不是同一个实例了。

    三、编译优化导致的有序性问题

    接着第二点的代码,咱们给他优化一下成如下代码。

    public class Singleton {
      static Singleton instance;
      static Singleton getInstance(){
        if (instance == null) {
          synchronized(Singleton.class) {
            if (instance == null)
              instance = new Singleton();
            }
        }
        return instance;
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    这样的代码看着就无懈可击了,但是智者千虑终有一失,这里我就摘抄一下别人的文章的片段了。

    以下文章节选自JAVA并发编程实战:

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

    这看上去一切都很完美,无懈可击,但实际上这个 getInstance() 方法并不完美。问题出在哪里呢?出在 new 操作上,我们以为的 new 操作应该是:

    分配一块内存 M;
    在内存 M 上初始化 Singleton 对象;
    然后 M 的地址赋值给 instance 变量。
    但是实际上优化后的执行路径却是这样的:

    分配一块内存 M;
    将 M 的地址赋值给 instance 变量;
    最后在内存 M 上初始化 Singleton 对象。
    优化后会导致什么问题呢?我们假设线程 A 先执行 getInstance() 方法,当执行完指令 2 时恰好发生了线程切换,切换到了线程 B 上;如果此时线程 B 也执行 getInstance() 方法,那么线程 B 在执行第一个判断时会发现 instance != null ,所以直接返回 instance,而此时的 instance 是没有初始化过的,如果我们这个时候访问 instance 的成员变量就可能触发空指针异常。
    在这里插入图片描述

    第三点总结一下,编译器的优化工作,可能会导致我们的代码前后顺序紊乱,虽然他保证了不会影响程序最终结果。但是偶尔还是可能导致意向不到的BUG产生。

  • 相关阅读:
    跟羽夏学 Ghidra ——数据
    供水管网监测系统
    [ALI-签约代扣] 小程序环境下的签约代扣
    【Leetcode hot 100】96. 不同的二叉搜索树
    qt-mvvm代码分析
    vue3 Component API
    Spire.PDF for .NET【文档操作】演示:将新的 PDF 页面插入到指定索引处的现有 PDF 中
    c++异常处理
    旋转数组的最小值
    集采报告丨国家药品带量采购政策及趋势分析
  • 原文地址:https://blog.csdn.net/qq_29611427/article/details/126101198