大白话之必会Java Atomic | 线程一点也不安全(一):比自增和synchronized更快速、靠谱的原子操作(调用C语言)
前言
阅读本篇文章,你需要对下方知识有所了解:
- synchronized关键词的作用
- 线程池的作用(这里)
不靠谱和慢动作
在多线程环境下:
操作 | 靠谱程度 | 执行速度 |
---|---|---|
i++ 自增运算 | 没戏 | 不赖 |
synchronized | 贼棒 | 太废 |
不靠谱的自增
操作类
假如我们现在有一个变量:num
我们这个变量设置两个方法:
方法 | 返回值 | 作用 |
---|---|---|
plus() | void | 将num自增(+1) |
getNum() | Integer | 返回num的值 |
代码如下:
1class Num {
2 Integer num = 0;
3
4 public void plus() {
5 num++;
6 }
7
8 public Integer getNum() {
9 return num;
10 }
11}
主类
然后在另一个类主方法中新建一个缓存线程池:
1ExecutorService executorService = Executors.newCachedThreadPool();
当我们执行executorService.execute(new Runnable() {})
时,缓存线程池会将指定的对象以非阻塞
的方式提交到队列中。
随后再写一个循环,调用100次plus()
方法,此时num
值应为100
。
1 Num num = new Num();
2 ExecutorService executorService = Executors.newCachedThreadPool();
3 for (int i = 0; i < 100; i++) {
4 /*
5 下方语句可套用Lambda表达式,替换为:
6 executorService.execute(() -> num.plus());
7 */
8 executorService.execute(new Runnable() {
9 @Override
10 public void run() {
11 num.plus();
12 }
13 });
14 }
好。那么复制下方的完整代码,并运行得到结果:
1import java.util.concurrent.ExecutorService;
2import java.util.concurrent.Executors;
3
4public class Main {
5 public static void main(String[] args) {
6 Num num = new Num();
7 ExecutorService executorService = Executors.newCachedThreadPool();
8 for (int i = 0; i < 100; i++) {
9 /*
10 下方语句可套用Lambda表达式,替换为:
11 executorService.execute(() -> num.plus());
12 */
13 executorService.execute(new Runnable() {
14 @Override
15 public void run() {
16 num.plus();
17 }
18 });
19 }
20 //当线程池内线程全部执行完毕后,关闭线程池
21 executorService.shutdown();
22 //返回num的值
23 System.out.println(num.getNum());
24 }
25}
26
27class Num {
28 Integer num = 0;
29
30 public void plus() {
31 num++;
32 }
33
34 public Integer getNum() {
35 return num;
36 }
37}
你会发现运行结果本应该是100
的,但是结果却只能看命:大概率在90-100之间徘徊的结果。
为什么呢?
自增运算
让我们看看自增运算会进行哪些操作:
也就是说,如果有两个线程正巧同时读取了变量num,那么运算后返回的结果很有可能出错,除非只使用单线程进行操作。
所以说,线程一点也不安全。
有点慢的同步
synchronized
为方法添加了一个锁,如果有线程在占用,其它线程就会被阻塞,所以可以保证最终数值的正确。
1 public synchronized void plus() {
2 num++;
3 }
我将循环改为了10000次,并做了如下统计:
组别 | 是否使用synchronized | 执行结果 | 花费时间 |
---|---|---|---|
0 | 否 | 9316 | 61912170ns |
0 | 是 | 10000 | 42229795ns |
--- | --- | --- | --- |
1 | 否 | 9142 | 42752371ns |
1 | 是 | 10000 | 78747787ns |
--- | --- | --- | --- |
2 | 否 | 9495 | 54361835ns |
2 | 是 | 10000 | 47179626ns |
--- | --- | --- | --- |
3 | 否 | 9326 | 44193545ns |
3 | 是 | 10000 | 128409937ns |
我们可以比较清晰的看到,使用了synchronized
关键字的方法执行速度要慢上一拍,这是因为synchronized
的线程同步操作相当于强行将多线程“捋”成了单线程。
Atomic
Compare And Swap
Compare And Swap(CAS),即“比较并交换”。
CAS中有三个值:
V
将要修改的变量
E
在预期中,该变量修改前的值
N
如果符合预期,将变量修改的值
我们还是同样用一张思维导图,说明CAS的逻辑:
CAS是个倔强且严谨的流程,如果num
的值与它运行时所记录的值不同的话,它会尝试重新获取num
的值,并再次重复操作。
应用
Atomic
便是遵循了CAS
原则的原子类
,它能可靠地对数据进行修改
。
上图是Atomic
中提供的一些数据类型的实现类。让我们修改一下自己的实例。
1class Num {
2 private AtomicInteger num = new AtomicInteger(0);
3
4 public void plus() {
5 num.incrementAndGet();
6 }
7
8 public Integer getNum() {
9 return num.get();
10 }
11}
12
套用上方的统计表,我们对Atomic
的性能进行多次测试:
组别 | 使用的方法 | 执行结果 | 花费时间 |
---|---|---|---|
0 | 自增 | 9316 | 61912170ns |
0 | synchronized | 10000 | 42229795ns |
0 | Atomic | 10000 | 44210059ns |
--- | --- | --- | --- |
1 | 自增 | 9142 | 42752371ns |
1 | synchronized | 10000 | 78747787ns |
1 | Atomic | 10000 | 53520536ns |
--- | --- | --- | --- |
2 | 自增 | 9495 | 54361835ns |
2 | synchronized | 10000 | 47179626ns |
2 | Atomic | 10000 | 89278829ns |
--- | --- | --- | --- |
3 | 自增 | 9326 | 44193545ns |
3 | synchronized | 10000 | 128409937ns |
3 | Atomic | 10000 | 53277442ns |
Atomic
相比较synchronized
关键字执行时间要稍快一些。
后语
至此,就是本章全部的内容了。
请思考:
- 截至本章的学习内容,
Atomic
有哪些缺点? - 在链表中,
CAS模型
中E
所存储的是什么? - 为什么
Atomic
是原子性的? Atomic
的CAS模型
中,会不会出现E
始终不正确,陷入死循环的情况?
至此,其实Atomic
还是有出错的几率的。下一章我们将讲述Atomic
可能导致的ABA问题
、Atomic
的底层实现Unsafe
类以及Atomic
的缺点。
前往下一章:Atomic的ABA问题会导致什么情况?如何解决?
如转载请在文章尾部添加:
原作者来自 adlered 个人技术博客:https://www.stackoverflow.wiki/
额,我那个代码是复制你上面原本都代码都
接上:由于篇幅限制,只能将完整代码截图:
Hello,你好,抱歉这么晚回复你。
new Runnable()是匿名内部类,引用外部数据是不需要加final的,如果要加final的话,你的final好像加错了(加在了实例化的线程池上),由于字数限制问题,我会把我实验的完整代码贴在下一条评论中,绝对可以运行,供参考。
抱歉,我使用的是
System.nanoTime()
方法,结果是纳秒(ns),是我的失误,文章已经修改,非常感谢!我有两个疑问:
当然,我不确定这是不是Java版本导致的,我用的是JDK1.8 。我忘记我使用1.7 的时候是不是这样的了。
作者文中时间最短对就是44210059ms 这个值了,我觉得是否作者搞错了时间?或者如果真的执行一个自增 需要这个久的话,我决定放弃Java。
这个时间我们取个整,4200 秒,一个多小时?