




















阅读本篇文章,你需要对下方知识有所了解:
在多线程环境下:
| 操作 | 靠谱程度 | 执行速度 |
|---|---|---|
| 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的线程同步操作相当于强行将多线程“捋”成了单线程。
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的缺点。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。