前言
阅读本篇文章,你需要对下方知识有所了解:
- synchronized 关键词的作用
- 线程池的作用(这里)
不靠谱和慢动作
在多线程环境下:
操作 | 靠谱程度 | 执行速度 |
---|---|---|
i++ 自增运算 | 没戏 | 不赖 |
synchronized | 贼棒 | 太废 |
不靠谱的自增
操作类
假如我们现在有一个变量:num
我们这个变量设置两个方法:
方法 | 返回值 | 作用 |
---|---|---|
plus() | void | 将 num 自增(+1) |
getNum() | Integer | 返回 num 的值 |
代码如下:
class Num {
Integer num = 0;
public void plus() {
num++;
}
public Integer getNum() {
return num;
}
}
主类
然后在另一个类主方法中新建一个缓存线程池:
ExecutorService executorService = Executors.newCachedThreadPool();
当我们执行 executorService.execute(new Runnable() {})
时,缓存线程池会将指定的对象以 非阻塞
的方式提交到队列中。
随后再写一个循环,调用 100 次 plus()
方法,此时 num
值应为 100
。
Num num = new Num();
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 100; i++) {
/*
下方语句可套用Lambda表达式,替换为:
executorService.execute(() -> num.plus());
*/
executorService.execute(new Runnable() {
@Override
public void run() {
num.plus();
}
});
}
好。那么复制下方的完整代码,并运行得到结果:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main {
public static void main(String[] args) {
Num num = new Num();
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 100; i++) {
/*
下方语句可套用Lambda表达式,替换为:
executorService.execute(() -> num.plus());
*/
executorService.execute(new Runnable() {
@Override
public void run() {
num.plus();
}
});
}
//当线程池内线程全部执行完毕后,关闭线程池
executorService.shutdown();
//返回num的值
System.out.println(num.getNum());
}
}
class Num {
Integer num = 0;
public void plus() {
num++;
}
public Integer getNum() {
return num;
}
}
你会发现运行结果本应该是 100
的,但是结果却只能看命:大概率在 90-100 之间徘徊的结果。
为什么呢?
自增运算
让我们看看自增运算会进行哪些操作:
也就是说,如果有两个线程正巧同时读取了变量 num,那么运算后返回的结果很有可能出错,除非只使用单线程进行操作。
所以说,线程一点也不安全。
有点慢的同步
synchronized
为方法添加了一个锁,如果有线程在占用,其它线程就会被阻塞,所以可以保证最终数值的正确。
public synchronized void plus() {
num++;
}
我将循环改为了 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
中提供的一些数据类型的实现类。让我们修改一下自己的实例。
class Num {
private AtomicInteger num = new AtomicInteger(0);
public void plus() {
num.incrementAndGet();
}
public Integer getNum() {
return num.get();
}
}
套用上方的统计表,我们对 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
的缺点。
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于