大白话之必会 Java Atomic | 线程一点也不安全(一):比自增和 synchronized 更快速、靠谱的原子操作(调用 C 语言)

本贴最后更新于 1934 天前,其中的信息可能已经东海扬尘

前言

阅读本篇文章,你需要对下方知识有所了解:

  • 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 之间徘徊的结果。

为什么呢?

自增运算

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

3.png

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

4.png

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

应用

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

5.png

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

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

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

  • 大白话
    17 引用 • 27 回帖
  • 原理
    16 引用 • 44 回帖
  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3190 引用 • 8214 回帖
  • 线程
    122 引用 • 111 回帖 • 3 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
  • 我有两个疑问:

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

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

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

  • someone
    作者

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

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

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

    1 回复
  • someone
    作者

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


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