最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 并发王者课 - 青铜 3: 双刃剑-理解多线程带来的安全问题

    正文概述 掘金(秦二爷)   2021-05-25   472

    在前面的两篇文章中,我们体验了线程的创建,并从OS进程层面认识了线程。现在,我们已经知晓多线程在解决一些场景问题时有特效。

    然而,不知你可曾想过,多线程虽然效率很高,但是它却有着你无法回避并发问题。举个王者中常见的场景,双方10人同时进攻主宰,最后击败主宰的玩家才是真正的赢家,而且只能有一位。所以问题来了,假如这10位玩家代表10个线程,它们在并发访问同一个资源时,如何保证数据的安全性?总不至于,主宰只有一条命,可是却有多位玩家获得主宰,这显然不符合逻辑。

    这个简单例子的背后,是计算机系统中一个普遍且基本的问题,即多线程的安全问题。在设计多线程时,我们追求它的优点,也务必要理解它存在的安全隐患,并为之设计合理的解决方案。否则,多线程这把双刃剑必将给我们以教训。

    本文将从并发并行的概念触发,帮助你入门这些概念并理解竞态相关问题。

    一、理解并发(Concurrency)和并行(Parallelism)

    在并发编程中,并发与并行像一对孪生兄弟,不仅长相相似,又容易让人混淆。但是,它们又有着本质的区别。所以,理解并行与并发,不要尝试去死记硬背概念,在你未能从本质上认识它们之前,你无法欺骗你的大脑去记住它。

    简而言之,并行与并发的区别的核心在于所竞争的 资源 不同。举个通俗的例子:

    • 蓝方5个人一起打主宰,是并发(Concurrency),因为竞争的目标资源只有一个
    • 蓝方2人去打主宰,3人去打暴君,是并行(Parallelism),因为竞争的目标资源是两个

    类似的,从CPU计算的角度看, 并发和并行的概念可以理解为:

    • 如果1个CPU同时执行5个任务,就是并发
    • 如果5个CPU同时执行5个任务,并且是每个CPU执行一个,那么就是并行

    并发王者课 - 青铜 3: 双刃剑-理解多线程带来的安全问题

    以上是对并行和并发的通俗概述,如果你有兴趣,可以通过检索资料详细了解单CPU下是如何模拟并发的。

    二、理解竞态(Race Condition)下的安全问题

    显然易见,无论是并发还是并行,都有助于提高计算效率。然而,效率是一方面,安全则是更重要的一方面。比如上面进攻主宰的案例中,一定要能知道是谁给予了最后一击,也就是数据不能出错。所以,我们就需要理解多线程下的 竞态(Race Condition) 和解决策略。

    所谓竞态,你可以理解为多个线程试图在同一时刻修改共享数据的情况。你看,从字面上理解的话,Race这个词就是比赛的意思。比赛的目标是什么?是看谁先获得共享资源,即进入临界区(Critical Section)

    常见的竞态有下面这两种模式:

    • Read-modify-write
    • Check-then-act

    1. Read-modify-write

    先看下面这段代码,玩家每次进攻,主宰的血量都会减少:

    public class Master {
        //主宰的初始血量
        private int blood = 100;
    
        //每次被击打后血量减5
        public int decreaseBlood() throws Exception {
            if(blood <= 0){
                throw new Exception("主宰已经被击败!");
            }
            blood = blood - 5;
            return blood;
        } 
    }
    

    当线程执行decreaseBlood()方法调用时,事情是这样发展的:

    • 第一步:从内存中读取blood的值到寄存器(Read);
    • 第二步:修改寄存器中的blood值(Modify);
    • 第三步:将寄存器的值写回内存(Write)。

    这就是Read-modify-write模式。整个过程看起来一气呵成,实则祸根已经种下。想想看,如果在第一步时,两个线程同时都读取到了值(比如100),随后两个线程同时做了修改,此时在第三步,无论是哪个线程率先将值写回内存,后面的线程都会覆盖内存中的值。换句话说,主宰承受了两次攻击,血量应该降低到90,可结果却是95,不是它耐操,而是你代码写错了!

    2. Check-then-act

     //每次被击打后血量减5
        public int decreaseBlood() throws Exception {
            if(blood <= 0){
                throw new Exception("主宰已经被击败!");
            }
            blood = blood - 5;
            return blood;
        } 
    

    我们再近距离观察下decreaseBlood()方法,你会发现,它不仅会让主宰出现攻击两次但血量却只减少一次的情况,还会出现血量为负值的情况!这是为什么?

    注意decreaseBlood()中有一行if(blood <= 0),也就是说如果此时主宰已经被击败,那就不要再往下继续运行,直接抛出异常。但是,问题来了。假设此时主宰的血量是 5 ,就差最后一击了!然后,线程A和线程B两个线程同时进来:

    • 第一步:线程A和线程B检查血量是否为 0Check);
    • 第二步:线程A和线程B都通过了检查;
    • 第三步:线程A和线程B执行血量扣减动作,但顺序未知(Act)。

    问题是,如果线程A在执行blood = blood - 5时,blood的值不再是 5 ,而是已经被线程B更改为 0 了呢?那么结果就是主宰最后的血量是 -5 !很显然,这样的结果就扯淡了。

    以上就是两种常见的竞态情况。简单来说, Read-modify-write是在写入时因并发导致值被覆盖,而Check-then-act则是因并发导致条件判断失效

    3. 如何预发竞态

    既然多线程是不安全的,那如何预防竞态的发生?其核心在于锁+原子操作,即对临界区进行加锁,让临界区每次有且只能有一个线程访问,在当前线程未离开临界区时,其他线程不得进入,且线程在临界区的操作必须保证原子性。

    在Java中,最简单的加锁方式是使用synchronized关键字,我们会在下一篇中对它详细讲解。

    以上就是文本的全部内容,恭喜你又上了一颗星!✨

    夫子的试炼

    • 写一段多线程并发代码,体验并发时的数据错误。

    关于作者

    关注公众号【庸人技术笑谈】,获取及时文章更新。记录平凡人的技术故事,分享有品质(尽量)的技术文章,偶尔也聊聊生活和理想。不贩卖焦虑,不兜售课程。


    起源地下载网 » 并发王者课 - 青铜 3: 双刃剑-理解多线程带来的安全问题

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元