首先来看看Atomic原子类的结构
一、Automic概念
automic在java中为JUC(java.util.concurrent)并发包的一个子包。顾名思义,automic包中包含许多原子类。
1、什么是原子类?
原子是不可分割的最小单位,故原子类可认为是其操作是不可分割的。
2、原子类的作用?
提供一种简单、性能高效、线程安全地更新一个变量的方式。
二、分析Automic底层逻辑(以AtomicInteger自增为例)
通过前面volatile的讲解,大家应该也对为什么要有原子类操作有了一定的了解,那我们都知道,i++自增操作不是原子性的,也就是说在高并发的情况下也许会发生重复覆盖,这显然不是我们想要的,那么Atomic原子类就是来解决这个问题的,下面我们可以看来来AtomicInteger自增源码:
可以看到AtomicInteger的自增操作返回unsafe.getAndAddInt()方法。
这里大家可能有疑问了,什么是unsafe呢?
对于JVM有过了解的小伙伴可能知道JNI,也就是java本地库接口,Unsafe(sun.misc.Unsafe)是属于JNI的类,Unsafe里面的native方法直接操作内存,getUnfate()仅提供高级的Bootstrap类加载器使用,简而言之就是直接操作CPU;
接下来查看unsafe.getAndAddInt()方法源码
可以发现我们在do...while的循环条件调用了compareAndSwapInt(var1, var2, var5, var5 + var4)方法,即AtomicInteger中的CAS算法。
PS:此处对于CAS(CompareAndSwap)以及之前的volatile不熟悉的同学可以看看我的另外一篇文章,那篇文章更适合在看JUC之前了解为什么要学习JUC,思路会更清晰。
三、CAS
CAS是计算机硬件对并发操作共享数据的支持,CAS包含三个操作数:
1、内存值V(var1,var2)
2、预估值A(var 5)
3、更新值B(var5 + var4)
只有当V==A时,才会把B的值赋给V,即V=B,否则不做任何操作。
不用CAS时,线程修改数据,有从主内存取得数据、修改数据、回写数据这三步,在回写数据这一步,可能主内存已经被别的线程回写过了,就会发生回写覆盖。为了解决这个,CAS在回写的时候会将主内存与自己工作内存之前取得的值比较一下,就可以判断是否已经被别人修改过了,如果没有,我就可以修改,不过别人修改过了,我再去一次,在最新的值上面再修改。
为什么用CAS不用Synchronized呢?
锁之后,最耗时的就是线程的上下文交换,也就是线程被挂起和被唤醒的过程,在这个过程中,操作系统会在核态和用户态之间切换,要保存运行环境和恢复运行环境,这些都是很费事的,在JAVA1.6之后优化的自旋锁就是为了避免上下文交换。
PS:关于操作系统核态和用户态是什么以及怎样切换面试也问的挺多的,感兴趣的同学可以看看我操作系统相关的文章和面经。
通过上面的分析可以看到,do,while循环也避免了线程的上下文交换,性能会高很多。
CAS的缺点
1、虽然CAS没有挂起线程、增加并发性,但是当很多个线程进行自旋,就会白白耗费CPU资源,这种并发,就只是让很多线程在那循环等待。
2、只能保证一个共享变量的原子操作。CAS要有一个compare,一个变量就好对比,如果是段代码,一个类呢?synchronized就能锁这样的,当时还有AtomicReference等类有相应的操作。
3、ABA问题
先有两个线程,其中一个运行的比较快,将主内存中的变量从A改成了B,再从B改成了A,这个时候另外一个线程调用compareAndSwap方法时,会发现期望值于实际值是相同的,它并不知道中间已经被修改过了,它就会修改掉这个值。
当我们的实际问题不能容忍这个被修改的过程,这个问题就很严重了,比如会员卡的问题,用户消费20元,紧接着再充值20元,如果积分统计线程是根据余额的减少来统计积分的,那就严重了,积分统计线程并不知道用户已经消费过了,积分没统计上,这是一个大漏洞。
4、如何解决ABA问题?
在加个变量(时间戳)来表示这个变量是否被修改过了,这样下来,再执行 compareAndSet()方法的时候,不仅仅比较这个变量是否相同,还要比较时间戳是否相等。
AtomicStampReference类则实现了这个功能
AtomicStampReference构造函数和compareAndSet()方法
这里我们以AtomicInteger为例讲解了Atomic原子类的作用,通过分析AtomicInteger自增源码我们可以知道Atomic原子类的底层是CAS,这里对于CAS也进行了较为详细的讲解,CAS本身也是面试中经常会遇到的问题,不熟悉的同学需要进行详细的了解。