文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

我们一起聊聊从操作系统层面理解多线程冲突

2024-11-30 00:00

关注

什么是多线程的冲突

同一进程内的线程是共享同一内存空间的,所以在多个线程的进程里,线程是可以同时操作这个进程空间的数据的,线程之间可以共享进程的资源:比如代码段、堆空间、数据段、打开的文件等资源,但每个线程也有自己独立的栈空间。如果多个线程如果竞争共享资源,如果不采取有效的措施,则会造成共享数据的混乱。

举个小栗子:一个房子里(代表一个进程),只有一个厕所(代表共享资源)。屋子里面有两个人A和B(代表两个线程),共用这个厕所。一天A去上厕所了,不过厕所门的锁坏了,就没法锁门。这是B也想去上厕所,直接就开门进去了,然后发现A在里面。

上面这个故事说明,对于共享资源,如果没有上锁,在多线程的环境里,那么就可能会发生翻车现场。

竞争与协作

做个小实验,创建五个线程,它们分别对共享变量 i 自增 1 执行 1000 次

package com.atguigu.juc.atomics;

public class Station{


    int i = 0;

    public void add() {
        for(int m = 0; m < 1000; m++){
            try {
                //使用sleep()模拟业务时间,如果不加,大概率不会出现并发问题
                Thread.sleep(1);
                i += 1;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }

    public static void main(String[] args) throws InterruptedException {
        //实例化站台对象,并为每一个站台取名字
        Station station = new Station();
        new Thread(station::add,"线程1").start();
        new Thread(station::add,"线程2").start();
        new Thread(station::add,"线程3").start();
        new Thread(station::add,"线程4").start();
        new Thread(station::add,"线程5").start();
        Thread.sleep(20000);
        // 让每一个站台对象各自开始工作
        System.out.println(station.i);
        Thread.sleep(5000);
        System.out.println(station.i);
    }
}

运行了几次发现,每次运行得到不同的结果。在计算机里是不能容忍的,虽然是小概率出现的错误,但是小概率事件它一定是会发生的。

为什么会出现这样的问题呢?

为了理解为什么会发生这种情况,我们必须了解编译器为更新计数器 i 变量生成的代码序列,也就是要了解汇编指令的执行顺序。

在这个例子中,我们只是想给 i 加上数字 1,那么它对应的汇编指令执行过程是这样的:

可以发现,只是单纯给 i 加上数字 1,在 CPU 运行的时候,实际上要执行 3 条指令。

设想我们的线程 1 进入这个代码区域,它将 i 的值(假设此时是 50 )从内存加载到它的寄存器中,然后它向寄存器加 1,此时在寄存器中的 i 值是 51。

现在,一件不幸的事情发生了:当前线程被挂起了,线程 2 被调度运行,并进入同一段代码。它也执行了第一条指令,从内存获取 i 值并将其放入到寄存器中,此时内存中 i 的值仍为 50,因此线程 2 寄存器中的 i 值也是 50。假设线程 2 执行接下来的两条指令,将寄存器中的 i 值 + 1,然后将寄存器中的 i 值保存到内存中,于是此时全局变量 i 值是 51。

最后,又发生一次上下文切换,线程 1 恢复执行。还记得它已经执行了两条汇编指令,现在准备执行最后一条指令。回忆一下, 线程 1 寄存器中的 i 值是51,因此,执行最后一条指令后,将值保存到内存,全局变量 i 的值再次被设置为 51。

简单来说,增加 i (值为 50 )的代码被运行两次,按理来说,最后的 i 值应该是 52,但是由于不可控的调度,导致最后 i 值却是 51。

针对上面线程 1 和线程 2 的执行过程,我画了一张流程图,会更明确一些:

互斥的概念

上面展示的情况称为竞争条件(race condition),当多线程相互竞争操作共享变量时,由于运气不好,即在执行过程中发生了上下文切换,我们得到了错误的结果,事实上,每次运行都可能得到不同的结果,因此输出的结果存在不确定性(indeterminate)。

由于多线程执行操作共享变量的这段代码可能会导致竞争状态,因此我们将此段代码称为临界区(critical section),它是访问共享资源的代码片段,一定不能给多线程同时执行。

我们希望这段代码是互斥(mutualexclusion)的,也就说保证一个线程在临界区执行时,其他线程应该被阻止进入临界区,说白了,就是这段代码执行过程中,最多只能出现一个线程。

另外,说一下互斥也并不是只针对多线程。在多进程竞争共享资源的时候,也同样是可以使用互斥的方式来避免资源竞争造成的资源混乱。

同步的概念

所谓同步,就是并发进程/线程在一些关键点上可能需要互相等待与互通消息,这种相互制约的等待与互通信息称为进程/线程同步。

上面那个栗子:一A去上厕所了,发现厕所门的锁坏了,告诉B一声。这是B想去上厕所的话,就会先问一下A是不是还在里面,然后再开门进去了。这也是互通消息的方式,如果锁没有坏,A直接把门锁上,这就是相互等待的方式。

怎么解决多线程冲突?

为了实现进程/线程间正确的协作,操作系统必须提供实现进程协作的措施和方法,主要的方法有两种:

这两个都可以方便地实现进程/线程互斥,而信号量比锁的功能更强一些,它还可以方便地实现进程/线程同步。

使用加锁操作和解锁操作可以解决并发线程/进程的互斥问题。

任何想进入临界区的线程,必须先执行加锁操作。若加锁操作顺利通过,则线程可进入临界区;在完成对临界资源的访问后再执行解锁操作,以释放该临界资源。

信号量

信号量是操作系统提供的一种协调共享资源访问的方法。通常信号量表示资源的数量,对应的变量是一个整型(sem)变量。

另外,还有两个原子操作的系统调用函数来控制信号量的,分别是:

举个类比,2 个资源的信号量,相当于 2 条火车轨道,PV 操作如下图过程:


来源:今日头条内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯