[图片] 前言 阅读本篇文章,你需要对下方知识有所了解: synchronized 关键词的作用 线程池的作用(这里) 不靠谱和慢动作 在多线程环境下: 操作 靠谱程度 执行速度 i++ 自增运算 没戏 不赖 synchronized 贼棒 太废 不靠谱的自增 操作类 假如我们现在有一个变量:num 我们这个变量设置两 ..

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

前言

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

不靠谱和慢动作

在多线程环境下:

操作 靠谱程度 执行速度
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还是有出错的几率的。下一章我们将讲述Atomic可能导致的ABA问题Atomic的底层实现Unsafe类以及Atomic的缺点。

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

  • 大白话
    17 引用 • 18 回帖
  • 原理
    10 引用 • 30 回帖
  • Java

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

    2267 引用 • 7672 回帖 • 926 关注
  • 线程
    79 引用 • 93 回帖 • 2 关注
4 回帖   
请输入回帖内容...
  • gitors  

    我有两个疑问:

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

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

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

  • AdlerED  

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

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

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

    1 回复
  • AdlerED  

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


  • gitors        

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