JUC框架之——CAS及Atomic类
Posted 无扬的博客
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JUC框架之——CAS及Atomic类相关的知识,希望对你有一定的参考价值。
目录
0、前言
1、案例演练
2、Atomic中CAS的应用
3、CAS缺陷及解决方案
0、前言
多线程的同步问题,可以通过加锁来解决,尽管内置锁(synchronized)和显示锁(Lock)能解决问题,但是有些场景下可以使用更加轻量级的“锁”方式来解决问题,CAS(Compare And Swap)即是这样一种实现。
CAS是一种无锁算法,核心思想是 如果V的值等于期望值A,那么将V的值更新为B,否则不操作
。CAS是CPU指令,属于硬件支持,保证原子性。
1、案例演练
假如需要一个全局的计数器,每执行一次某操作,该计数器就加一,下面分别使用 volatile
变量和 AtomicInteger
来实现该计数器
public class CasTest {
private static AtomicInteger atomicCounter = new AtomicInteger(1);
private static volatile int volatileCounter = 1;
@Test
public void test() throws Exception{
CountDownLatch threadReady = new CountDownLatch(1);
CountDownLatch mainResult = new CountDownLatch(10);
for (int i=1; i<=10; i++) {
new Thread(() -> {
try {
threadReady.await();
} catch (InterruptedException e) {
}
for (int j=1; j<=100; j++) {
atomicCounter.incrementAndGet();
volatileCounter++;
}
// 通知主线程查看计数器的值
mainResult.countDown();
}).start();
}
threadReady.countDown();
mainResult.await();
System.out.println("atomic计数器运行结果: "+atomicCounter.get());
System.out.println("volatile计数器运行结果: "+volatileCounter);
}
}
运行结果(不一定必现,多运行几次):
atomic计数器运行结果: 1001
volatile计数器运行结果: 999
上面代码段可以得出的结论:
1、 volatile
虽然可以保证可见性,但是无法保证原子性(复合操作)
2、 AtomicInteger
的 incrementAndGet()
方法可以保证原子性
2、Atomic中CAS的应用
2.1 AtomicInteger中CAS的应用
看了上面案例,那么 AtomicInteger
的 incrementAndGet()
是如何保证原子性的呢?首先 AtomicInteger
维护了3个属性:
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
1、 unsafe
对象:
该对象是CAS的核心,java代码是通过 Unsafe
来实现CAS指令操作, Unsafe
中的方法几乎全都是 native
修饰,底层调用的是C/C++代码.
2、 valueOffset
3、 value
该 volatile
变量用来存储当前的值,对该值直接进行 get()
和 set(intvalue)
操作时,无需使用 unsafe
的CAS方法,因为单指令赋值操作本身就具有原子性。
接下来看 AtomicInteger.incrementAndGet()
的实现代码:
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
然后再看 unsafe.getAndAddInt
方法的实现代码:
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
当CAS操作失败时(有其它线程已经修改了原值V),会通过
var5=this.getIntVolatile(var1,var2);
来获取最新的值。
CAS的操作方法: this.compareAndSwapInt(var1,var2,var5,var5+var4)
,会调用Unsafe的
compareAndSwapInt(Objectvar1,longvar2,intvar4,intvar5)
方法,该方法的4个参数含义分别是:
1、 Objectvar1
: AtomicInteger
对象本身
3、 intvar4
:期望值,可以理解为当前CAS操作时认为的旧值
4、 intvar5
:设定值,CPU指令操作时如果实际值和期望值相同,则把旧值替换(swap)为设定值
如果每次CAS都失败,则在while中会一直循环重试,直到成功,这个循环重试的过程也叫 自旋
。有时候容易把CAS操作和自旋划上等号,底层的CAS返回就是一个boolean类型,代码层面根据返回的结果来进行循环重试,才构成了自旋。 AtomicInteger.compareAndSet(intexpect,intupdate)
方法就不涉及自旋,一次CAS操作之后就返回,成功就返回true,失败就返回false。
2.2 其余Atomic类
在juc的atomic包下面还有很多Atomic开头的类,使用方法的实现和 AtomicInteger
有很多相似的地方,可以对比起来研究,其核心都是一个对比、修改的过程。
另外java的CAS操作都是通过 Unsafe
类来进行二次封装的,不建议直接获取 Unsafe
类,同时 Unsafe
类的 publicstaticUnsafegetUnsafe()
方法只对jdk的类开放,自定义的类中调用该方法是会抛异常的,如果实在要获取该类的实例,那只能通过反射。
3、CAS缺陷及解决方案
3.1 ABA问题
什么是ABA问题不在此赘述,不是说ABA问题一出现就一定需要解决,只不过有些业务场景下ABA问题是不能容忍的,这些场景下ABA问题才需要解决。
atomic包下面为解决ABA问题提供了一个 AtomicStampedReference<V>
类,其维护了一个Pair属性,Pair中保存了 reference
值和该值对应的版本号:
private static class Pair<T> {
final T reference;
final int stamp;
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
static <T> Pair<T> of(T reference, int stamp) {
return new Pair<T>(reference, stamp);
}
}
private volatile Pair<V> pair;
AtomicStampedReference<V>
类将真正需要的值( <V>
) reference
进行保存的同时还保存一个版本号 stamp
,在进行 compareAndSet
的时候,不仅带上期望值 reference
,还带上该期望值对应的版本号 stamp
,使用实例如下:
AtomicStampedReference<String> atomicStampedReference =
new AtomicStampedReference<>("stampedReference", 1);
String oldValue = atomicStampedReference.getReference();
int oldStamp = atomicStampedReference.getStamp();
atomicStampedReference.compareAndSet(oldValue, "new stampedReference", oldStamp, oldStamp+1);
如果需要自旋的设置值,比如就是要把值设置成 "new stampedReference"
AtomicStampedReference<String> atomicStampedReference =
new AtomicStampedReference<>("stampedReference", 1);
String oldValue = atomicStampedReference.getReference();
int oldStamp = atomicStampedReference.getStamp();
while (!atomicStampedReference.compareAndSet(oldValue, "new stampedReference", oldStamp, oldStamp+1)) {
oldStamp = atomicStampedReference.getStamp();
}
另外大多数Atomic类都有一个 weakCompareAndSet
方法,该方法从doc上看多了一行注释:
May fail spuriously and does not provide ordering guarantees, so is only rarely an appropriate alternative to {@code compareAndSet}.
查阅了一些资料,其实这里的大意应该是说 weakCompareAndSet
操作仅保留了volatile自身变量的特性,而去除了happens-before规则带来的内存语义。也就是说,weakCompareAndSet无法保证处理操作目标的volatile变量外的其他变量的执行顺序( 编译器和处理器为了优化程序性能而对指令序列进行重新排序 ),同时也无法保证这些变量的可见性(此处见参考链接3)。不过在jdk8中两者实现一模一样,没有任何差别,所以应该是 weakCompareAndSet
先占了坑位,但是还没有具体实现。
3.2 自旋操作时循环时间太长
在有些场景下面,可能会有自旋的循环时间次数很多,占用大量CPU资源,可以在掉用cas操作的地方加上自旋最大次数。例如在 SynchronousQueue
类的 ObjectawaitFulfill(QNodes,E e,booleantimed,longnanos)
方法中,对自旋的次数就做了限制。
3.3 仅能保证一个共享变量原子操作
对于单个共享变量,我们可以使用CAS循环的方式来保证原子操作,但是对多个共享变量操作时,CAS循环就无法保证操作的原子性,这个时候一般需要加锁操作,或者是把多个变量整合成一个变量。
参考链接
1、https://www.cnblogs.com/Mainz/p/3546347.html
2、http://cmsblogs.com/?p=2235
3、https://www.jianshu.com/p/55a66113bc54
4、《Java特种兵》
以上是关于JUC框架之——CAS及Atomic类的主要内容,如果未能解决你的问题,请参考以下文章