原子性问题、AtomicInteger类和CAS机制

一、原子性问题

1、问题描述

前几天在用lambda表达式遍历list时,遇到了需要使用AtomicInteger类的情况,于是便记录下学习过程。
这里运行了20个线程,每个线程对count变量进行1000此自增操作,如果上面这段代码能够正常并发的话,最后的结果应该是20000才对,但实际结果却发现每次运行的结果都不相同,都是一个小于20000的数字。这是为什么呢?
	private static final int THREADS_COUNT = 20;
    public static int count = 0;

    public static void increase() {
        count ++;
    }

    public static void main(String[] args) {
        Thread[] threads = new Thread[THREADS_COUNT];
        for (int i = 0; i < THREADS_COUNT; i++) {
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 1000; i++) {
                        increase();
                    }
                    System.out.println(count);
                }
            });
            threads[i].start();
        }

        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println(count);
    }

运行结果:

2、造成上述问题的原因

  • 假如,现在 i = 100,线程A和线程B同时读取了 i,那么线程A和线程B中的 i 都是100。然后,线程A执行了 i++ 操作,此时 i = 101,这里注意,线程B中的 i 不会受到线程A执行完后的影响,还是等于100,线程B执行i++ 操作,此时 i = 101。

  • 为什么线程A中的 i 不会对线程B中的 i 造成影响?

  • 这里就涉及到内存的知识了,最开始定义的全局变量 i 是储存在主内存中的,而每一个线程运行时都有一个线程栈。当线程访问 i 的时候,首先找到堆内存的变量 i,然后把堆内存变量 i 的具体值load到线程本地内存中,建立一个变量副本,之后线程就不再和堆内存变量 i 有任何关系,而是直接修改副本变量的值。在修改完之后的某一个时刻(线程退出之前),自动把线程变量副本的值回写到堆中变量。这样在堆中 i 的值就产生变化了。

3、解决办法

解1:加volatile(结果不变,仍有问题。)

public static volatile int count = 0;

程序中有三个非常重要的概念:原子性、可见性、有序性。volatile修饰的变量保证的只是可见性,并在一定范围内改变有序性。
i++并不是原子性操作,它的操作需要三步完成:

  1. 读取 i 的值
  2. 使 i 的值加1
  3. 把改变后的值重新赋给 i

假如某一时刻 i = 100,线程A执行到了第二步,线程B执行到了第一步,此时线程B中 i 的值依旧是100,然后线程A执行第三步,这时主内存中的 i = 101,但是由于线程B已经完成了读取,它不可能重新再读一次了,所以线程B中会以读取时 i 的值继续运行下去。

你所理解的volatile修饰的变量在值发生变化的时候,会立即将变化后的值回写到主内存,并通知其他线程会将各个线程栈内的变量副本置为无效状态。这样的理解不能说是错的,只能说还有点不完善。volatile的可见是针对主内存的可见,被load到工作内存的volatile无效,通知其他线程栈中变量副本为无效只是针对读取而言,只有当其他线程还没有读取这个变量时才会被通知,对已经读取了的线程,CPU并不会通知。

所以volatile要想提供理想的线程安全,需要满足下面两个条件:

  1. 对变量的写操作不依赖于当前值。
  2. 该变量没有包含在具有其他变量的不变式中。

解2:改为AtomicInteger类型(成功)

public static volatile AtomicInteger count = new AtomicInteger(0);

    public static void increase() {
        count.incrementAndGet();
    }

结果每次都输出20000,程序输出了正确的结果,这都归功于AtomicInteger.incrementAndGet()方法的原子性。

  1. 当某个时刻主内存中count的值为200,然后同时被200个线程read、load到工作内存中。
  2. 线程1打算对count进行加1操作,根据count值所在的地址直接得到一个的值count1=200,然后对于这个count1进行一次cas操作,看count1是否被其他线程改变过而导致内存地址对应的值不为预期的值,这里发现count1没有被修改,于是count1+1,刷新到主内存。
  3. 此刻线程2也打算对count进行加1,操作和步骤2类似,此时得到的count1也为200,就在这时线程1操作完成,目标地址对应的值变为201,然后线程2的count1=200进行cas操作,通过比较发现200!=201,无法进行加1操作,于是线程2再次循环重新通过内存地址获得count1的值为201,在进行cas发现符合条件进行加1操作。
  4. 其他线程同理,每当发现我要操作的count值不为预期值则进行自旋操作。

二、CAS机制

1、描述

1)、CAS机制中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。
2)、更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。
3)、从思想上来说,synchronized属于悲观锁,悲观的认为程序中的并发情况严重,所以严防死守,CAS属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去重试更新。

2、缺点

1)、CPU开销过大,在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很到的压力。
2)、不能保证代码块的原子性,CAS机制所保证的知识一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用synchronized了。
3)、ABA问题。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

w_tt

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值