操作系统和并发的爱恨纠葛(二)

简介: 我一直没有急于写并发的原因是我参不透操作系统,如今,我已经把操作系统刷了一遍,这次试着写一些并发,看看能不能写清楚,卑微小编在线求鼓励...... 我打算采取操作系统和并发同时结合讲起来的方式。

线程带来的安全性问题


线程安全性是非常复杂的,在没有采用同步机制的情况下,多个线程中的执行操作往往是不可预测的,这也是多线程带来的挑战之一,下面我们给出一段代码,来看看安全性问题体现在哪

public class TSynchronized implements Runnable{
    static int i = 0;
    public void increase(){
        i++;
    }
    @Override
    public void run() {
        for(int i = 0;i < 1000;i++) {
            increase();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        TSynchronized tSynchronized = new TSynchronized();
        Thread aThread = new Thread(tSynchronized);
        Thread bThread = new Thread(tSynchronized);
        aThread.start();
        bThread.start();
        System.out.println("i = " + i);
    }
}

这段程序输出后会发现,i 的值每次都不一样,这不符合我们的预测,那么为什么会出现这种情况呢?我们先来分析一下程序的运行过程。

TSynchronized 实现了 Runnable 接口,并定义了一个静态变量 i,然后在 increase 方法中每次都增加 i 的值,在其实现的 run 方法中进行循环调用,共执行 1000 次。


可见性问题


在单核 CPU 时代,所有的线程共用一个 CPU,CPU 缓存和内存的一致性问题容易解决,CPU 和 内存之间

如果用图来表示的话我想会是下面这样

16.png

在多核时代,因为有多核的存在,每个核都能够独立的运行一个线程,每颗 CPU 都有自己的缓存,这时 CPU 缓存与内存的数据一致性就没那么容易解决了,当多个线程在不同的 CPU 上执行时,这些线程操作的是不同的 CPU 缓存

17.png

因为 i 是静态变量,没有经过任何线程安全措施的保护,多个线程会并发修改 i 的值,所以我们认为 i 不是线程安全的,导致这种结果的出现是由于 aThread 和 bThread 中读取的 i 值彼此不可见,所以这是由于 可见性 导致的线程安全问题。


原子性问题


看起来很普通的一段程序却因为两个线程 aThreadbThread 交替执行产生了不同的结果。但是根源不是因为创建了两个线程导致的,多线程只是产生线程安全性的必要条件,最终的根源出现在 i++ 这个操作上。

这个操作怎么了?这不就是一个给 i 递增的操作吗?也就是 「i++ => i = i + 1」,这怎么就会产生问题了?

因为 i++ 不是一个 原子性 操作,仔细想一下,i++ 其实有三个步骤,读取 i 的值,执行 i + 1 操作,然后把 i + 1 得出的值重新赋给 i(将结果写入内存)。

当两个线程开始运行后,每个线程都会把 i 的值读入到 CPU 缓存中,然后执行 + 1 操作,再把 + 1 之后的值写入内存。因为线程间都有各自的虚拟机栈和程序计数器,他们彼此之间没有数据交换,所以当 aThread 执行 + 1 操作后,会把数据写入到内存,同时 bThread 执行 + 1 操作后,也会把数据写入到内存,因为 CPU 时间片的执行周期是不确定的,所以会出现当 aThread 还没有把数据写入内存时,bThread 就会读取内存中的数据,然后执行 + 1操作,再写回内存,从而覆盖 i 的值,导致 aThread 所做的努力白费。

18.png

为什么上面的线程切换会出现问题呢?

我们先来考虑一下正常情况下(即不会出现线程安全性问题的情况下)两条线程的执行顺序

19.png

可以看到,当 aThread 在执行完整个 i++ 的操作后,操作系统对线程进行切换,由 aThread -> bThread,这是最理想的操作,一旦操作系统在任意 读取/增加/写入 阶段产生线程切换,都会产生线程安全问题。例如如下图所示


20.png

最开始的时候,内存中 i = 0,aThread 读取内存中的值并把它读取到自己的寄存器中,执行 +1 操作,此时发生线程切换,bThread 开始执行,读取内存中的值并把它读取到自己的寄存器中,此时发生线程切换,线程切换至 aThread 开始运行,aThread 把自己寄存器的值写回到内存中,此时又发生线程切换,由 aThread -> bThread,线程 bThread 把自己寄存器的值 +1 然后写回内存,写完后内存中的值不是 2 ,而是 1, 内存中的 i 值被覆盖了。

我们上面提到 原子性 这个概念,那么什么是原子性呢?

并发编程的原子性操作是完全独立于任何其他进程运行的操作,原子操作多用于现代操作系统和并行处理系统中。

原子操作通常在内核中使用,因为内核是操作系统的主要组件。但是,大多数计算机硬件,编译器和库也提供原子性操作。

在加载和存储中,计算机硬件对存储器字进行读取和写入。为了对值进行匹配、增加或者减小操作,一般通过原子操作进行。在原子操作期间,处理器可以在同一数据传输期间完成读取和写入。这样,其他输入/输出机制或处理器无法执行存储器读取或写入任务,直到原子操作完成为止。

简单来讲,就是「原子操作要么全部执行,要么全部不执行」。数据库事务的原子性也是基于这个概念演进的。


有序性问题


在并发编程中还有带来让人非常头疼的 有序性 问题,有序性顾名思义就是顺序性,在计算机中指的就是指令的先后执行顺序。一个非常显而易见的例子就是 JVM 中的类加载

21.png

这是一个 JVM 加载类的过程图,也称为类的生命周期,类从加载到 JVM 到卸载一共会经历五个阶段 「加载、连接、初始化、使用、卸载」。这五个过程的执行顺序是一定的,但是在连接阶段,也会分为三个过程,即 「验证、准备、解析」 阶段,这三个阶段的执行顺序不是确定的,通常交叉进行,在一个阶段的执行过程中会激活另一个阶段。

有序性问题一般是编译器带来的,编译器有的时候确实是 「好心办坏事」,它为了优化系统性能,往往更换指令的执行顺序。

            </div>
目录
相关文章
|
存储 安全 Java
操作系统和并发的爱恨纠葛(三)
我一直没有急于写并发的原因是我参不透操作系统,如今,我已经把操作系统刷了一遍,这次试着写一些并发,看看能不能写清楚,卑微小编在线求鼓励...... 我打算采取操作系统和并发同时结合讲起来的方式。
44 0
操作系统和并发的爱恨纠葛(三)
|
缓存 Java 调度
操作系统和并发的爱恨纠葛(一)
我一直没有急于写并发的原因是我参不透操作系统,如今,我已经把操作系统刷了一遍,这次试着写一些并发,看看能不能写清楚,卑微小编在线求鼓励...... 我打算采取操作系统和并发同时结合讲起来的方式。
66 0
操作系统和并发的爱恨纠葛(一)
Computer:现代计算机操作系统的四大基本特性(并发/共享/虚拟/异步)
Computer:现代计算机操作系统的四大基本特性(并发/共享/虚拟/异步)
Computer:现代计算机操作系统的四大基本特性(并发/共享/虚拟/异步)
|
存储 安全 Java
操作系统和并发的爱恨纠葛(三)
我一直没有急于写并发的原因是我参不透操作系统,如今,我已经把操作系统刷了一遍,这次试着写一些并发,看看能不能写清楚,卑微小编在线求鼓励...... 我打算采取操作系统和并发同时结合讲起来的方式。
操作系统和并发的爱恨纠葛(三)
|
1月前
|
监控 Unix Linux
Linux操作系统调优相关工具(四)查看Network运行状态 和系统整体运行状态
Linux操作系统调优相关工具(四)查看Network运行状态 和系统整体运行状态
34 0
|
1月前
|
Linux 编译器 开发者
Linux设备树解析:桥接硬件与操作系统的关键架构
在探索Linux的庞大和复杂世界时🌌,我们经常会遇到许多关键概念和工具🛠️,它们使得Linux成为了一个强大和灵活的操作系统💪。其中,"设备树"(Device Tree)是一个不可或缺的部分🌲,尤其是在嵌入式系统🖥️和多平台硬件支持方面🔌。让我们深入了解Linux设备树是什么,它的起源,以及为什么Linux需要它🌳。
Linux设备树解析:桥接硬件与操作系统的关键架构
|
2月前
|
Linux 数据安全/隐私保护 虚拟化
Linux技术基础(1)——操作系统的安装
本文是龙蜥操作系统(Anolis OS) 8.4 的安装指南,用户可以从[龙蜥社区下载页面](https://openanolis.cn/download)获取ISO镜像。安装方法包括物理机的光驱和USB闪存方式,以及虚拟机中的VMware Workstation Pro设置。安装过程涉及选择语言、配置安装目标、选择软件集合和内核,设置Root密码及创建新用户。安装完成后,可通过文本模式或图形化界面验证系统版本,如Anolis OS 8.4,标志着安装成功。
|
1月前
|
Linux
Linux操作系统调优相关工具(三)查看IO运行状态相关工具 查看哪个磁盘或分区最繁忙?
Linux操作系统调优相关工具(三)查看IO运行状态相关工具 查看哪个磁盘或分区最繁忙?
28 0
|
4天前
|
存储 Linux C语言
Linux:冯·诺依曼结构 & OS管理机制
Linux:冯·诺依曼结构 & OS管理机制
9 0
|
1月前
|
存储 Linux
linux查看系统版本、内核信息、操作系统类型版本
linux查看系统版本、内核信息、操作系统类型版本
62 9