乐趣区

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

在后面的两篇文章中,咱们体验了线程的创立,并从 OS 过程层面意识了线程。当初,咱们曾经通晓多线程在解决一些场景问题时有特效。

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

这个简略例子的背地,是计算机系统中一个广泛且根本的问题,即 多线程的平安问题。在设计多线程时,咱们谋求它的长处,也务必要了解它存在的安全隐患,并为之设计正当的解决方案。否则,多线程这把双刃剑必将给咱们以教训。

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

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

在并发编程中,并发与并行像一对孪生兄弟,不仅长相类似,又容易让人混同。然而,它们又有着实质的区别。所以,了解并行与并发,不要尝试去死记硬背概念,在你未能从实质上意识它们之前,你无奈坑骗你的大脑去记住它。

简而言之,并行与并发的区别的外围在于所竞争的 资源 不同。举个艰深的例子:

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

相似的,从 CPU 计算的角度看,并发和并行的概念能够了解为:

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

以上是对并行和并发的艰深概述,如果你有趣味,能够通过检索材料具体理解 单 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 关键字,咱们会在下一篇中对它具体解说。

以上就是文本的全部内容,祝贺你又上了一颗星!

夫子的试炼

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

对于作者

扫码关注公众号,获取及时文章更新。记录平凡人的技术故事,分享有品质(尽量)的技术文章,偶然也聊聊生存和现实。不贩卖焦虑,不抛售课程。

退出移动版