前言
阅读本篇文章,你需要对下方知识有所了解:
- 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问题会导致什么情况?如何解决?