关于java:Java并发编程阶段性总结安全性问题

5次阅读

共计 2473 个字符,预计需要花费 7 分钟才能阅读完成。

通过后面的 7 篇文章,你可能感觉并发编程很简单,既要思考程序后果是不是正确,又要思考程序能不能执行,还要思考服务器能不能扛住,切实不晓得从哪动手~

别怕,尽管看起来很多,但这其实是一个打怪降级的过程,外面有 3 道关卡:安全性问题、活跃性问题、性能问题,每一关都能有播种,如果三关全过了,也就把握了这门高阶技能了。

明天,咱们先过第一个关:安全性问题。

安全性问题

所谓安全性问题,是指程序有没有依照咱们的期待执行,就是正确性。置信你在工作的时候,肯定会被人问到:这办法有没有实现线程平安?这个类是不是线程平安的?

那什么是线程平安呢?线程平安的实质也是正确性,程序依照咱们的冀望执行。具体来说,无论在单线程环境下,还是在多线程环境下,最终的运行后果都是一样的,不会随着环境而变动。

一个程序要想实现线程平安,就必须防止这么三个问题,别离是:可见性问题、有序性问题、原子性问题。对于这几个问题,能够看下以前的文章:Java 并发编程 - 并发本源,外面具体讲了这三个问题的前因后果。

在弄清问题的前因后果后,咱们得入手解决问题了。然而,一说到并发编程,各路大神就嗨起来了,各种专业术语,各种解读源码,就是不提怎么写代码。

但其实,实现线程平安,写好一个多线程利用没那么难。你能够看看这篇文章,Java 并发编程 - 解决并发,外面就是为了破除你的畏难情绪,从理论工作登程,一个个的解决问题。

问题的起因都搞清楚了,解决方案也有了。那是不是要仔细检查所有代码,百分百保障线程平安呢?

当然不是,实现线程平安的老本很高,须要消耗你大量的工夫精力。而且,并发编程毕竟是高阶技能,这就意味着,这项技能尽管十分重要,但应用场景却很少。

只有在共享数据会变动的状况下,才须要实现线程平安。具体来说,就是只有在多个线程同时读写一个数据时,你才须要去思考程序的可见性、有序性、原子性。

换句话说,增删改查之类的业务,差不多就得了,你得把工夫精力放在重要业务上。比如说,银行的转账、提现业务,电商的下单减库存业务等等。

看到这儿,你可能会这么想:我曾经晓得了并发问题的本源,也有了解决方案,也晓得要特地留神某些业务,可我还是很懵,齐全不晓得该从哪里开始。

没关系,我再给你两个抓手。

在数据竞争的场景下,必须思考并发

数据竞争就是,多个线程同时拜访、并批改一个共享数据,就会导致并发 BUG。这里有两个关键词:多线程、批改一个共享数据,你看上面的代码。

class Account {
    // 余额
    private Integer balance = 1000;

    // 充值
    void charge(Integer amt) {this.balance += amt;}
}

下面是一段充值的代码,如果充值一笔一笔地进行,这齐全没问题。因为两个条件没凑齐,balance- 余额 尽管是共享变量,但一天也没几笔充值进来,更别提有人同时充值了。

可公司总有做大的一天,到时如果同时进来几万笔充值,那两个条件就凑齐了,最初的余额必定一塌糊涂。咱们来具体分析一下,这段代码会同时呈现可见性、原子性问题。

先来说可见性问题,当初是多核 CPU 时代,每颗外围都有本人的 CPU 缓存。如果一笔充值运行在 CPU- 1 上,另一笔充值运行在 CPU- 2 上。这就相当于,它们同时读取了 balance- 余额,又同时批改了 balance,但单方没有任何沟通,齐全不晓得对方做了什么,最初 balance 必定错得一塌糊涂。

再来看原子性问题,Java 是一门高级编程语言,一条语句往往会被拆成多个 CPU 指令。比如说,第八行代码 this.balance -= amt; 就被拆成:

  1. 读取内存,把 balance 加载到 CPU;
  2. CPU 执行 balance – amt 操作;
  3. 把最终的 balance 写入内存;

这原本是一个残缺的过程,可计算机有一个线程切换的机制,一旦产生了线程切换,那后果也就没法保障了。

对于可见性、原子性问题,能够看下以前的文章:Java 并发编程 - 并发本源,外面有更具体的剖析。

总结一下,当多个线程同时拜访、并批改一个共享数据,会导致数据竞争,从而呈现并发 BUG。而数据竞争要满足两个条件:多线程、批改一个共享数据,只有筹齐这两个条件,你就得采取防护措施了。

至于要采取什么防护措施,能够参考这篇文章:Java 并发编程 - 解决并发。

有竞态条件的代码,必须要实现互斥

所谓竞态条件,是指程序的执行后果,会随着线程的执行程序而变动。这听起来有点拗口,咱们还是来看一个理论的例子吧。

在提现操作中,有一个条件判断:提现金额不能大于账户余额。但如果同时呈现好几笔提现,又没做任何预防措施,就会呈现超额提现的问题。

class Account {
    // 余额
    private Integer balance = 150;

    // 提现
    void withdraw(Integer amt) {if (balance >= amt) {this.balance -= amt;}
    }
}

比方说,账户 A 只有 150 块,但线程一、线程二都要提现 100 块。那失常来说,只有一笔转账能胜利。

可如果线程一、线程二同时执行到第 7 行 if (balance >= amt),它们都发现提现金额是 100 块,小于账户余额 150 块,于是两笔提现都继续执行,你白白亏了 50 块吗?

看到这儿,置信你大略能明确什么是竞态条件。简略来说,你得特地注意这样的代码:

if (状态变量 满足 执行条件) {状态变量 = new 状态变量}

而且,竞态条件十分非凡,没法做简略的归类。它既不是原子性问题,也不是可见性问题,更不是有序性问题,纯正是因为程序不反对并发拜访,必须一个个排队解决。

既然这样,咱们只能采纳互斥这种计划了,具体来说,就是:锁。你能够回顾一下这两篇文章:Java 并发编程 - 解决并发、Java 并发编程 - 用锁的正确姿态

写在最初

并发编程是一个打怪降级的过程,外面有 3 个关卡:安全性问题、活跃性问题、性能问题。

第一关就是安全性问题,是指程序有没有依照咱们的期待执行。

第一关其实不难,咱们只有留神:程序会不会呈现数据竞争、竞态条件,而后做好防护措施就行,你能够回顾一下这些文章:Java 并发编程 - 解决并发、Java 并发编程 - 用锁的正确姿态

正文完
 0