难度
初级
学习时间
30分钟
适合人群
零基础
开发语言
Java
开发环境
- JDK v11
- IntelliJIDEA v2018.3
友情提示
- 本教学属于系列教学,内容具有连贯性,本章使用到的内容之前教学中都有详细讲解。
- 本章内容针对零基础或基础较差的同学比较友好,可能对于有基础的同学来说很简单,希望大家可以根据自己的实际情况选择继续看完或等待看下一篇文章。谢谢大家的谅解!
1.温故知新
前面在《“全栈2019”53篇Java多线程学习资料及总结》一章中介绍了Java多线程基础知识总结及学习资料。
在《“全栈2019”Java原子操作第一章:内存可见性volatile关键字解析》一章中介绍了内存可见性volatile关键字。
现在介绍什么是原子性。
2.什么是原子性?
在上一章《“全栈2019”Java原子操作第一章:内存可见性volatile关键字解析》中介绍了内存可见性,这一章我们来给大家介绍什么是原子性。
这里举一个经典的不能再经典的例子,让你秒懂什么是原子性。
银行转账,账户A给账户B转1000元。
这里面必然有两个操作:
- 账户A减去1000元。
- 账户B加上1000元。
这两个操作要么都成功,要么都失败,这就叫原子性。
那么学术一点的原子性定义就出来了:
一个或多个操作是不可中断的,要么全部执行成功,要么全部执行失败。
在上述例子中,其中任意一个操作都不能中断或失败。
比如,账户A把1000元打给了账户B,此时银行突然停电,账户B没有收到这1000元,但是账户A里面被减掉1000元,这明显是不对的。
3.程序中的原子性
“i++”是原子操作吗?
大家先在心中给出自己的答案,然后我们写一个程序进行验证。
首先,我们先定义出变量i:
![1c05031d2ba55cb4ae619d8aa628a5eb.png](https://img-blog.csdnimg.cn/img_convert/1c05031d2ba55cb4ae619d8aa628a5eb.png)
为了验证i++到底是不是原子操作,我们需要使用多个线程进行干扰。
因为这几个线程执行的任务都是相同的,所以这里我们这里需要用到Runnable接口。
接着,我们创建出线程任务:
![5880ecb7a9200f4aac5aa5d8ccfcd8a5.png](https://img-blog.csdnimg.cn/img_convert/5880ecb7a9200f4aac5aa5d8ccfcd8a5.png)
然后,在run()方法里面计算i++结果并输出:
![093fa6f424b0396cecc9a5a71e1ea783.png](https://img-blog.csdnimg.cn/img_convert/093fa6f424b0396cecc9a5a71e1ea783.png)
run()方法书写完毕。
接下来,就是创建出多个线程。
这里采用for循环的方式创建线程:
![dae20ab9e5199ca507e95e92c94ccfaf.png](https://img-blog.csdnimg.cn/img_convert/dae20ab9e5199ca507e95e92c94ccfaf.png)
在循环体里面创建线程并将任务传给线程,以及最后启动线程:
![cb4a1a543a4832fd3995ef63ac6ae110.png](https://img-blog.csdnimg.cn/img_convert/cb4a1a543a4832fd3995ef63ac6ae110.png)
例子书写完毕。
运行程序,执行结果:
![129272d03a76e2be848360b5218a44bd.gif](https://img-blog.csdnimg.cn/img_convert/129272d03a76e2be848360b5218a44bd.gif)
静图:
![33c2aaaa6abbaf144ed18a4c2ebb055b.png](https://img-blog.csdnimg.cn/img_convert/33c2aaaa6abbaf144ed18a4c2ebb055b.png)
从运行结果来看,符合预期。
咦,好像“i++”是原子操作,10个线程同时执行计算任务,i一直从0打印到9,是对的啊。
别高兴的太早,这里我们把每个线程都睡上一段时间再看看。
修改例子,在计算任务Runnable里面使当前线程睡100毫秒:
![79ffdf04c310c77996aa44691dad844b.png](https://img-blog.csdnimg.cn/img_convert/79ffdf04c310c77996aa44691dad844b.png)
例子改写完毕。
运行程序,执行结果:
![d62573f889cd8e6c8ca8d92d8cca14e4.gif](https://img-blog.csdnimg.cn/img_convert/d62573f889cd8e6c8ca8d92d8cca14e4.gif)
静图:
![058189b47d1fb0c93bdeffa568eb4c82.png](https://img-blog.csdnimg.cn/img_convert/058189b47d1fb0c93bdeffa568eb4c82.png)
从运行结果来看,符合预期。i++不是原子操作。
从这些0你就可以看出来:
![8c817c3b4c7c2c4f433ccc93cc2ddc9b.png](https://img-blog.csdnimg.cn/img_convert/8c817c3b4c7c2c4f433ccc93cc2ddc9b.png)
这些0就可以证明i++不是原子操作吗?
可以。
怎么证明?
i++实际上有三步操作:
- 从主内存读取变量i。
- 执行i++。
- 将变量i写入主内存。
我们来看看多个线程读取变量i的过程:
![245f29528306af196018304cc83c6939.gif](https://img-blog.csdnimg.cn/img_convert/245f29528306af196018304cc83c6939.gif)
简单解释上图:
Thread-0 线程要执行i++操作,则需要获取到变量i,于是优先从缓存中获取变量i,因为缓存中并没有变量i,所以再从主内存中获取变量i,此时获取到的变量i=0;
同理,Thread-1 线程也要执行i++操作,也需要获取变量i,也是优先从缓存中获取变量i,因为缓存中并没有变量i,所以再从主内存中获取变量i,此时获取到的变量i=0;
读取到变量i以后,开始执行i++操作:
![5447469606f32fe471a64547e02208fc.gif](https://img-blog.csdnimg.cn/img_convert/5447469606f32fe471a64547e02208fc.gif)
简单解释上图:
Thread-0 线程在拿到变量i以后,开始执行i++操作。
然而i++不是一个整体操作,它被细分成了多个操作:i = i + 1。先计算i+1的值,然后将i+1的结果赋给i。
这就导致了在执行i++操作时,操作与操作之间有可能中断(不连续,期间被其他线程干扰),这里大家需要注意:原子性操作中间是不能中断的!!!这一点就能否定i++是原子操作。
这还不着算,在往主内存写入数据时,还发生了值重复的情况:Thread-0 线程写入的值和后来的Thread-1 线程写入的值一样!
之所以发生重复值的情况是因为他们在同一时间读取的变量i,而且在计算i++的时候又在本地做的运算,这里的本地指的是线程工作内存,工作内存可以把缓存算在里面。
问题就出现了:对于i而言,我被执行了两次自增,为何我还是1?
这已经说明了对于i而言,i++根本不是原子操作,i++被细分成多个操作,而这多个操作又没有作为一个整体。
综上所述,i++不是原子操作,并且++i也不是原子操作,道理和i++一样。
4.volatile不能解决原子性问题
通过上一章《“全栈2019”Java原子操作第一章:内存可见性volatile关键字解析》的学习,有小伙伴提出一个问题:volatile关键字能不能解决原子性问题?
答案是不能的。
我们还是通过实际例子加以证明。
还是上一小节例子,将变量i用volatile关键字修饰一下:
![1be9035b56ef29538cffed4a5d54e62f.png](https://img-blog.csdnimg.cn/img_convert/1be9035b56ef29538cffed4a5d54e62f.png)
例子改写完毕。
运行程序,执行结果:
![83cb5c8d7494fa9b581047f08abc220b.gif](https://img-blog.csdnimg.cn/img_convert/83cb5c8d7494fa9b581047f08abc220b.gif)
静图:
![af8961d0618596303b658641fbcc16d6.png](https://img-blog.csdnimg.cn/img_convert/af8961d0618596303b658641fbcc16d6.png)
从运行结果来看,符合预期。
仍然是有重复值的情况:
![6cee7dc9f0f79c7e3032571f8c348d1b.png](https://img-blog.csdnimg.cn/img_convert/6cee7dc9f0f79c7e3032571f8c348d1b.png)
足以证明volatile关键字不能解决原子性问题。
volatile关键字只能解决内存可见性问题。
那这个原子性问题怎么解决?
我们可以通过同步来解决。
哎,上一章内存可见性问题同步也是可以解决,看来同步真的是包治百病。
下面我们就来看看同步如何解决原子性问题。
5.同步可以解决原子性问题
我们就分别把两把同步锁分别演示一遍,这两把同步锁分别是隐式锁synchronized和显式锁Lock。
隐式锁synchronized
还是上一小节例子,将volatile关键字移除掉:
![4607c959964ce2e6a39c8012cb2ebe85.png](https://img-blog.csdnimg.cn/img_convert/4607c959964ce2e6a39c8012cb2ebe85.png)
然后,在计算任务里面加上隐式锁synchronized,即同步代码块/方法:
![c1f88830764e526a18327eb6ea6c2da0.png](https://img-blog.csdnimg.cn/img_convert/c1f88830764e526a18327eb6ea6c2da0.png)
例子改写完毕。
运行程序,执行结果:
![e5996061df3ff8c7085a0dd2ec6abae4.gif](https://img-blog.csdnimg.cn/img_convert/e5996061df3ff8c7085a0dd2ec6abae4.gif)
静图:
![29c7f24be0988de510ccd851ac27a644.png](https://img-blog.csdnimg.cn/img_convert/29c7f24be0988de510ccd851ac27a644.png)
从运行结果来看,符合预期。
隐式锁synchronized可以解决原子性问题。
显式锁Lock
还是刚刚的例子,将隐式锁synchronized移除掉:
![e67ea6a4b6d00d1853cf3a3a1cd0fd3f.png](https://img-blog.csdnimg.cn/img_convert/e67ea6a4b6d00d1853cf3a3a1cd0fd3f.png)
创建出显式锁Lock:
![0b4b84257ca851d8e52e0657242d0dc0.png](https://img-blog.csdnimg.cn/img_convert/0b4b84257ca851d8e52e0657242d0dc0.png)
然后,给计算任务加上显式锁Lock:
![3da517bebbd1bf8f9ddda044be779488.png](https://img-blog.csdnimg.cn/img_convert/3da517bebbd1bf8f9ddda044be779488.png)
例子改写完毕。
运行程序,执行结果:
![a347a4b445a43c2263c50a69197f152d.gif](https://img-blog.csdnimg.cn/img_convert/a347a4b445a43c2263c50a69197f152d.gif)
静图:
![4722f12a056a092ba16eb97bc16391f3.png](https://img-blog.csdnimg.cn/img_convert/4722f12a056a092ba16eb97bc16391f3.png)
从运行结果来看,符合预期。
显式锁Lock也可以解决原子性问题。
当然了,和内存可见性一样,原子性的问题怎么动不动就用同步来解决呢?
Java为此给我们提供了解决办法:原子类。
6.原子类
究竟什么是原子类呢?
简单来说,就是支持原子操作的类。
原子类位于java.util.concurrent.atomic包下。
一共有12个原子类:
- AtomicBoolean
- AtomicInteger
- AtomicIntegerArray
- AtomicIntegerFieldUpdater
- AtomicLong
- AtomicLongArray
- AtomicLongFieldUpdater
- AtomicMarkableReference
- AtomicReference
- AtomicReferenceArray
- AtomicReferenceFieldUpdater
- AtomicStampedReference
本章先简单介绍一下什么是原子类,以后会单独介绍每一个原子类的基本原理和使用方式。
我们还是试一试其中的一个原子类,顺便也把上面遗留的问题给解决掉。
因为我们上一小节操作的变量类型是int,正好也有原子类AtomicInteger所对应,所以我们就试一试AtomicInteger类。
还是上一小节例子,移除掉显式锁Lock:
![7e1d844bdedf879da538d0708576b82c.png](https://img-blog.csdnimg.cn/img_convert/7e1d844bdedf879da538d0708576b82c.png)
然后,将加锁代码移除掉,并保留锁中代码:
![598977eadc30644b6a4e76bc101c6461.png](https://img-blog.csdnimg.cn/img_convert/598977eadc30644b6a4e76bc101c6461.png)
接着,将变量i也移除掉:
![deb585cdb5d0e81c673a405f997771e1.png](https://img-blog.csdnimg.cn/img_convert/deb585cdb5d0e81c673a405f997771e1.png)
接下来,创建出AtomicInteger原子类对象:
![42eb1e6616b837c9f21860cd988066f8.png](https://img-blog.csdnimg.cn/img_convert/42eb1e6616b837c9f21860cd988066f8.png)
然后,将“i++”代码替换为“atomicInteger.getAndIncrement()”代码:
![e54b1a7156d0f4fd4922596029c4f398.png](https://img-blog.csdnimg.cn/img_convert/e54b1a7156d0f4fd4922596029c4f398.png)
atomicInteger.getAndIncrement()的意思是获取未递增的值,然后递增该值。
例子改写完毕。
运行程序,执行结果:
![ffb1108d0ef320e77280e35c1c31e276.gif](https://img-blog.csdnimg.cn/img_convert/ffb1108d0ef320e77280e35c1c31e276.gif)
静图:
![9f97b1a617c8130ec8ffe994ff119d4b.png](https://img-blog.csdnimg.cn/img_convert/9f97b1a617c8130ec8ffe994ff119d4b.png)
从运行结果来看,符合预期。杀鸡焉用牛刀?使用原子类,我们也可以让原子性问题得到完美解决,无需同步。
好了,原子性的问题到此为止,下一章将介绍和原子操作息息相关的技术:比较并交换CAS技术。
Java原子操作的第一章、第二章以及第三章将介绍原子操作预备知识,大家要好好理解方可在后面的学习中事半功倍。
最后,希望大家可以把这个例子照着写一遍,然后再自己默写一遍,方便以后碰到类似的面试题可以轻松应对。
祝大家编码愉快!
GitHub
本章程序GitHub地址:https://github.com/gorhaf/Java2019/tree/master/Thread/atomic/原子性
总结
- 原子性定义:一个或多个操作是不可中断的,要么全部执行成功,要么全部执行失败。
- volatile关键字只能解决内存可见性问题,不能解决原子性问题。
- 隐式锁synchronized可以解决原子性问题。
- 显式锁Lock可以解决原子性问题。
- 原子类:支持原子操作的类。
- 原子类位于java.util.concurrent.atomic包下。
至此,Java中原子性相关内容讲解先告一段落,更多内容请持续关注。
答疑
如果大家有问题或想了解更多前沿技术,请在下方留言或评论,我会为大家解答。
上一章
“全栈2019”Java原子操作第一章:内存可见性volatile关键字解析
下一章
“全栈2019”Java原子操作第三章:比较并交换CAS技术详解
学习小组
加入同步学习小组,共同交流与进步。
- 方式一:关注头条号Gorhaf,私信“Java学习小组”。
- 方式二:关注公众号Gorhaf,回复“Java学习小组”。
全栈工程师学习计划
关注我们,加入“全栈工程师学习计划”。
![4be8435326adbbd0468a588ea6e4c774.png](https://img-blog.csdnimg.cn/img_convert/4be8435326adbbd0468a588ea6e4c774.png)
版权声明
原创不易,未经允许不得转载!