并发理论:并发编程会遇到哪些问题?

本文探讨了现代CPU架构中的缓存带来的内存可见性问题,解释了线程切换引发的原子性问题,以及编译优化导致的有序性问题。通过Java内存模型(JMM)和单例模式示例,阐述了volatile关键字如何解决多线程环境中的可见性和有序性问题。
摘要由CSDN通过智能技术生成

请添加图片描述

缓存带来的可见性问题

在这里插入图片描述

在现代的CPU架构中,CPU并不是直接和内存打交道的,因为CPU对内存读取和写入的速度很慢,因此在CPU的基础上加了多级缓存,用来加快读取和写入的速度。

为了实现缓存之间的相互同步,CPU会遵循缓存一致性协议,保证CPU缓存之间不会出现不同步问题,因此不会有内存可见性问题

在这里插入图片描述

但是缓存一致性协议对性能的损耗较大,因此在L1缓存的基础上又增加了Store Buffer,Load Buffer等Buffer。需要注意的是L1,L2,L3和主内存之间因为缓存一致性的保证是同步的。但是Store Buffer,Load Buffer等和L1之间却是异步的,所以会出现内存可见性问题

例如往内存中写入一个变量,这个变量会保存在Store Buffer里面,稍后才会异步写到L1中,同时同步写入主内存。

我们把这个模型抽象一下就得到了JMM(Java内存模型),后续我们分析并发问题都是基于这个模型
在这里插入图片描述

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

在这里插入图片描述
CPU在执行任务时,并不是执行完一个线程再执行另一个线程,而是在执行期间会发生线程切换。这样宏观看起来就是多个线程在同时运行。

假设sum=0,执行如下代码

sum++

sum++其实是如下3步操作

读取sum=0
计算sum+=11赋值给sum

线程切换就会带来原子性问题。2个线程同时执行sum++时,执行顺序有可能如下所示

时间线程A线程B
t1读取sum=0
t2读取sum=0
t3计算sum+1=1
t4计算sum+1=1
t5将1赋值给sum
t6将1赋值给sum

sum被2个线程加了2次,但是值却只增加了1

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

当我们想泡茶的时候可能会经历如下几个步骤烧水壶->烧开水->洗茶壶->洗茶杯->拿茶叶->泡茶。

为了更快的获得结果,我们可以在烧开水的时候去洗茶壶和洗茶杯

在这里插入图片描述
程序也是同样的道理,假如说有个代码段是执行IO操作时,如果后面的代码段对IO操作没有依赖关系是,我们完全可以在IO操作这段时间执行后面的代码段,此时代码就被执行了重排序

在这里插入图片描述
编译器和CPU都会对代码进行重排序。用单例模式演示一下重排序带来的问题

public class Singleton {

    private static Singleton uniqueInstance;

    private Singleton() {}

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

uniqueInstance = new Singleton()可以分解为如下三行伪代码

memory = allocate();    // 1:分配对象的内存空间
ctorInstance(memory);   // 2:初始化对象
uniqueInstance = memory;// 3:设置uniqueInstance指向刚分配的内存地址

3行伪代码中的2和3之间,可能会被重排序,重排序后执行时序如下

memory = allocate();    // 1:分配对象的内存空间
uniqueInstance = memory;// 3:设置uniqueInstance指向刚分配的内存地址
                        // 注意,此时对象还没有被初始化
ctorInstance(memory);   // 2:初始化对象

多个线程访问时可能出现如下情况

时间线程A线程B
t1A1:分配对象的内存空间
t2A3:设置uniqueinstance指向内存空间
t3B1:判断uniqueinstance是否为空
t4B2:由于uniqueinstace不为null,线程B间访问uniqueinstance引用的对象
t5A2:初始化对象
t6A4:访问instace引用的对象

这样会导致线程B访问到一个还未初始化的对象,此时可以用volatile来修饰Singleton,这样3行伪代码中的2和3之间的重排序,在多线程环境中将会被禁止

public class Singleton {

    private volatile static Singleton uniqueInstance;

    private Singleton() {}

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

参考博客

[1]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Java识堂

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值