经过前面的 7 篇文章,你可能以为并发编程很复杂,既要考虑程序结果是否是正确,又要考虑程序能不能执行,还要考虑服务器能不能扛住,实在不知道从哪入手~编程
别怕,虽然看起来不少,但这实际上是一个打怪升级的过程,里面有 3 道关卡:安全性问题、活跃性问题、性能问题,每一关都能有收获,若是三关全过了,也就掌握了这门高阶技能了。segmentfault
今天,咱们先过第一个关:安全性问题。缓存
所谓安全性问题,是指程序有没有按照咱们的期待执行,就是正确性。相信你在工做的时候,必定会被人问到:这方法有没有实现线程安全?这个类是否是线程安全的?安全
那什么是线程安全呢?线程安全的本质也是正确性,程序按照咱们的指望执行。具体来讲,不管在单线程环境下,仍是在多线程环境下,最终的运行结果都是同样的,不会随着环境而变化。服务器
一个程序要想实现线程安全,就必须避免这么三个问题,分别是:可见性问题、有序性问题、原子性问题。关于这几个问题,能够看下之前的文章:Java并发编程-并发根源,里面详细讲了这三个问题的来龙去脉。多线程
在弄清问题的前因后果后,咱们得动手解决问题了。然而,一说到并发编程,各路大神就嗨起来了,各类专业术语,各类解读源码,就是不提怎么写代码。并发
但其实,实现线程安全,写好一个多线程应用没那么难。你能够看看这篇文章,Java并发编程-解决并发,里面就是为了破除你的畏难情绪,从实际工做出发,一个个的解决问题。编程语言
问题的缘由都搞清楚了,解决方案也有了。那是否是要仔细检查全部代码,百分百保证线程安全呢?性能
固然不是,实现线程安全的成本很高,须要耗费你大量的时间精力。并且,并发编程毕竟是高阶技能,这就意味着,这项技能虽然很是重要,但使用场景却不多。this
只有在共享数据会变化的状况下,才须要实现线程安全。具体来讲,就是只有在多个线程同时读写一个数据时,你才须要去考虑程序的可见性、有序性、原子性。
换句话说,增删改查之类的业务,差很少就得了,你得把时间精力放在重要业务上。好比说,银行的转帐、提现业务,电商的下单减库存业务等等。
看到这儿,你可能会这么想:我已经知道了并发问题的根源,也有了解决方案,也知道要特别注意某些业务,可我仍是很懵,彻底不知道该从哪里开始。
不要紧,我再给你两个抓手。
数据竞争就是,多个线程同时访问、并修改一个共享数据,就会致使并发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;
就被拆成:
这原本是一个完整的过程,可计算机有一个线程切换的机制,一旦发生了线程切换,那结果也就无法保证了。
关于可见性、原子性问题,能够看下之前的文章:Java并发编程-并发根源,里面有更详细的分析。
总结一下,当多个线程同时访问、并修改一个共享数据,会致使数据竞争,从而出现并发BUG。而数据竞争要知足两个条件:多线程、修改一个共享数据,只要筹齐这两个条件,你就得采起防御措施了。
至于要采起什么防御措施,能够参考这篇文章:Java并发编程-解决并发。
所谓竞态条件,是指程序的执行结果,会随着线程的执行顺序而变化。这听起来有点拗口,咱们仍是来看一个实际的例子吧。
在提现操做中,有一个条件判断:提现金额不能大于帐户余额。但若是同时出现好几笔提现,又没作任何预防措施,就会出现超额提现的问题。
class Account { // 余额 private Integer balance = 150; // 提现 void withdraw(Integer amt) { if (balance >= amt) { this.balance -= amt; } } }
比方说,帐户A 只有 150 块,但线程1、线程二都要提现 100 块。那正常来讲,只有一笔转帐能成功。
可若是线程1、线程二同时执行到第 7 行 if (balance >= amt)
,它们都发现提现金额是 100 块,小于帐户余额 150 块,因而两笔提现都继续执行,你白白亏了 50 块吗?
看到这儿,相信你大概能明白什么是竞态条件。简单来讲,你得特别留意这样的代码:
if (状态变量 知足 执行条件) { 状态变量 = new 状态变量 }
并且,竞态条件很是特殊,无法作简单的归类。它既不是原子性问题,也不是可见性问题,更不是有序性问题,纯粹是由于程序不支持并发访问,必须一个个排队处理。
既然这样,咱们只能采用互斥这种方案了,具体来讲,就是:锁。你能够回顾一下这两篇文章:Java并发编程-解决并发、Java并发编程-用锁的正确姿式
并发编程是一个打怪升级的过程,里面有 3 个关卡:安全性问题、活跃性问题、性能问题。
第一关就是安全性问题,是指程序有没有按照咱们的期待执行。
第一关其实不难,咱们只要注意:程序会不会出现数据竞争、竞态条件,而后作好防御措施就行,你能够回顾一下这些文章:Java并发编程-解决并发、Java并发编程-用锁的正确姿式