傻子也能理解的并发编程中的原子性(Java)

1

线程A对变量a和b在进行如下操作:读取A-读取B-修改A-修改B-写入A-写入B

线程B也同样的可以对变量a和b进行上述操作

当线程A进行到写入A的时候,线程B是个急性子,抢占线程A,率先执行了读取B

这个过程就是一种线程安全问题的一种通俗解释。

完成读取A-读取B-修改A-修改B-写入A-写入B这个段功能的程序在单线程下是正确的,在多线程下就因为不是原子性的导致其功能不正常,有时候产生非常异常的结果

2

如果线程A可以在一瞬间完成读取A-读取B-修改A-修改B-写入A-写入B这个过程,那么线程B就不会读取错误的b变量的数据。

这种可以让线程A“一口气”完成不会被别人打断的过程,就是符合原子性的一个过程

这就是原子性

3

但是把所有java代码都弄成原子性那肯定是不可能的,计算机一个时间内能处理的东西永远是有限的。如果没法达到原子性那么我们就必须使用一种策略去让这个过程看上去是符合原子性的。这个策略最简单的就是控制代码的执行顺序,我们只需要控制线程B会在A完成读取A-读取B-修改A-修改B-写入A-写入B之后才会执行读取代码,这样就保证了线程安全问题不会发生了。所以有了加锁机制

4

我刚刚阐述了读取A-读取B-修改A-修改B-写入A-写入B这个过程,必须是原子性的才能保证线程安全。

现在我们在具体一点,假设读取A读取B以及后面的每个小操作都是原子性的,比如AtomicLong这些原子类的修改操作,它们本身的crud操作是原子的。

每个小操作都符合原子性是不是代表了这整个构成是符合原子性了呢?

显然不是

它仍然会产生线程安全问题,比如在修改A完成以后,失去操作原子性,所以线程B也开始执行读取B操作了。总之不要以为使用了线程安全类,你的所有代码就都是线程安全的!这总归都要去审查你代码的整体原子性出发的。就比如下面的例子:

虽然它全部用了原子类来进行操作,但是各个操作之间不是原子性的,也就是说比如线程A在执行else语句里的lastNumber.set(i)完后,也许其他线程执行了if语句里的lastFactorys.get()方法,随后线程A才继续执行lastFactors.set(factors)方法更新factors!

从这个逻辑过程中,线程安全问题就已经发生了。它破坏了读取A-读取B-修改A-修改B-写入A-写入B这个整体过程,在写入A完成以后其他线程去执行了读取B,导致A和B的状态不匹配了!真的是场大灾难!

5

本小节用了实例来讲述了原子性,从术语上直接解释什么是原子性是非常困难的,但是从这些示例里来说就会变的非常清晰

Live2d