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

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

前言

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

  • 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执行结果花费时间
0931661912170ns
01000042229795ns
------------
1914242752371ns
11000078747787ns
------------
2949554361835ns
21000047179626ns
------------
3932644193545ns
310000128409937ns

我们可以比较清晰的看到,使用了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自增931661912170ns
0synchronized1000042229795ns
0Atomic1000044210059ns
------------
1自增914242752371ns
1synchronized1000078747787ns
1Atomic1000053520536ns
------------
2自增949554361835ns
2synchronized1000047179626ns
2Atomic1000089278829ns
------------
3自增932644193545ns
3synchronized10000128409937ns
3Atomic1000053277442ns

Atomic相比较synchronized关键字执行时间要稍快一些。

后语

至此,就是本章全部的内容了。

请思考:

  • 截至本章的学习内容,Atomic有哪些缺点?
  • 在链表中,CAS模型E所存储的是什么?
  • 为什么Atomic原子性的?
  • AtomicCAS模型中,会不会出现E始终不正确,陷入死循环的情况?

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

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

如转载请在文章尾部添加

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

  • gitors 回复»

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

  • AdlerED @ gitors 回复»

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

  • AdlerED @ gitors 回复»

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

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

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

  • gitors 回复»

    我有两个疑问:

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

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

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

死钻技术 | 绝不抄袭