大白话之必会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之间徘徊的结果。

为什么呢?

自增运算

让我们看看自增运算会进行哪些操作:

3.png

也就是说,如果有两个线程正巧同时读取了变量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的逻辑:

4.png

CAS是个倔强且严谨的流程,如果num的值与它运行时所记录的值不同的话,它会尝试重新获取num的值,并再次重复操作。

应用

Atomic便是遵循了CAS原则的原子类,它能可靠地对数据进行修改

5.png

上图是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原子性的?
  • AtomicCAS模型中,会不会出现E始终不正确,陷入死循环的情况?

至此,其实Atomic还是有出错的几率的。下一章我们将讲述Atomic可能导致的ABA问题Atomic的底层实现Unsafe类以及Atomic的缺点。

前往下一章:Atomic的ABA问题会导致什么情况?如何解决?

如转载请在文章尾部添加

原作者来自 adlered 个人技术博客:https://www.stackoverflow.wiki/

    评论
    4 评论
    2019-06-14 09:21 回复»

    额,我那个代码是复制你上面原本都代码都

    2019-06-14 00:27 回复»

    接上:由于篇幅限制,只能将完整代码截图:

    2019-06-14 00:23 回复»

    Hello,你好,抱歉这么晚回复你。

    1. new Runnable()是匿名内部类,引用外部数据是不需要加final的,如果要加final的话,你的final好像加错了(加在了实例化的线程池上),由于字数限制问题,我会把我实验的完整代码贴在下一条评论中,绝对可以运行,供参考。

    2. 抱歉,我使用的是System.nanoTime()方法,结果是纳秒(ns),是我的失误,文章已经修改,非常感谢!

    2019-06-12 09:54 回复»

    我有两个疑问:

    1. 线程使用外部对象的时候,为什么不是final修饰的(我不是很确定,只是我从一开始使用的时候,就发现不是final的会编译报错)
      image.png
      当然,我不确定这是不是Java版本导致的,我用的是JDK1.8 。我忘记我使用1.7 的时候是不是这样的了。
    2. 我对执行时间表示怀疑
    组别 使用的方法 执行结果 花费时间
    0 Atomic 10000 44210059ms

    作者文中时间最短对就是44210059ms 这个值了,我觉得是否作者搞错了时间?或者如果真的执行一个自增 需要这个久的话,我决定放弃Java。

    这个时间我们取个整,4200 秒,一个多小时?

avatar

取消