一、什么是cas
cas:compare and swap,即比较再交换。
cas 的思想很简单:三个参数,一个当前内存值 v、旧的预期值 a、即将更新的值 b,当且仅当预期值 a 和内存值 v 相同时,将内存值修改为 b 并返回 true,否则什么都不做,并返回 false。
上面那段话来源于官方解析
atomic原子类底层核心就是cas,无锁化,乐观锁,每次尝试修改的时候,就对比一下,有没有人修改过这个值,没有人修改,自己就修改,如果有人修改过,就重新查出来最新的值,再次重复那个过程
二、cas 底层原理
我们拿 atomicinteger 类来分析,先来看看 atomicinteger 静态代码块片段:
public class atomicinteger extends number implements java.io.serializable {
private static final long serialversionuid = 6214790243416807050l;
// setup to use unsafe.compareandswapint for updates
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;
// 省略部分代码
}
这里用到了 sun.misc.unsafe 类它可以提供硬件级别的原子操作,它可以获取某个属性在内存中的位置,也可以修改对象的字段值,只不过该类对一般开发而言,很少会用到,其底层是用 c/c 实现的,所以它的方式都是被 native 关键字修饰过的。
而且的话人家限制好了,不允许你去实例化他以及使用他里面的方法的,首先人家的构造函数是私有化,不能自己手动去实例化他,
其次,如果用unsafe.getunsafe()方法来获取一个实例是不行的,在那个源码里,他会判断一下,如果当前是属于我们的用户的应用系统,识别到有我们的那个类加载器以后,就会报错,不让我们来获取实例jdk源码里面,jdk自己内部来使用,不是对外的unsafe,封装了一些不安全的操作,指针相关的一些操作,就是比较底层了,主要就是atomic原子类底层大量的运用了unsafe
如果想使用unsafe可以通过反射获取:
//自己定义一个类获取unsafe
public class unsafeinstance {
public static unsafe reflectgetunsafe() {
try {
field field = unsafe.class.getdeclaredfield("theunsafe");
field.setaccessible(true);
return (unsafe) field.get(null);
} catch (nosuchfieldexception e) {
e.printstacktrace();
} catch (illegalaccessexception e) {
e.printstacktrace();
}
return null;
}
}
//调用使用方式
private static final unsafe unsafe = unsafeinstance.reflectgetunsafe();
private static long stateoffset;
static {
try {
stateoffset = unsafe.objectfieldoffset(aqslock.class.getdeclaredfield("state"));
} catch (nosuchfieldexception e) {
e.printstacktrace();
}
}
我们看到atomicinteger初始化的时候来进行执行的,valueoffset,value这个字段在atomicinteger这个类中的偏移量,在底层,这个类是有自己对应的结构的,无论是在磁盘的.class文件里,还是在jvm内存中
大概可以理解为:value这个字段具体是在atomicinteger这个类的哪个位置,offset,偏移量,这个是很底层的操作,是通过unsafe来实现的。刚刚在类初始化的时候,就会完成这个操作的,final的,一旦初始化完毕,就不会再变更了
#############
接下来看一下atomicinteger 中的getandincrement 方法
public final int getandincrement() {
return unsafe.getandaddint(this, valueoffset, 1);
}
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;
}
var5 通过 this.getintvolatile(var1, var2)方法获取,认为当前的value值。去跟底层当前目前atomicinteger对象实例中的value值比较,如果是一样的话这就是compare的过程,就是set的过程,也就是将value的值设置为i 1(递增的值),如果var5(获取到的值)跟atomicinteger valueoffset获取到的当前的值,不一样的话,此时compareandswapint方法就会返回false,进入下一轮循环,重新执行getintvolatile(var1, var2)再次获取 value 值,因为变量 value 被 volatile 修饰,所以其它线程对它的修改,线程 a 总是能够看到,线程a继续执行compareandswapint进行比较替换,直到成功。
compareandswapint 方法是一个本地方法:
public final native boolean compareandswapint(object paramobject, long paramlong, int paramint1, int paramint2);
java 并没有直接实现 cas,cas 相关的实现是通过 c 内联汇编的形式实现的。java 代码需通过 jni 才能调用,位于 unsafe.cpp,查看源码:
unsafe_entry(jboolean, unsafe_compareandswapint(jnienv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
unsafewrapper("unsafe_compareandswapint");
oop p = jnihandles::resolve(obj);
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
return (jint)(atomic::cmpxchg(x, addr, e)) == e;
unsafe_end
看不懂具体是啥意思,网上资料总结:
1、先想办法拿到变量 value 在内存中的地址。
2、通过atomic::cmpxchg实现比较替换,其中参数 x 是即将更新的值,参数 e 是原内存的值。
以上1、2解析步骤来源于:https://www.jianshu.com/p/fb6e91b013cc
三、cas的缺点
此处参考 https://objcoding.com/2018/11/29/cas/#similar_posts
1、aba问题
- 线程1 查询a的值为a,与旧值a比较,
- 线程2 查询a的值为a,与旧值a比较,相等,更新为b值
- 线程2 查询a的值为b,与旧值b比较,相等,更新为a值
- 线程1 相等,更新b的值为c,尽管线程 1 也更改成功了,但是不代表这个过程就是没有问题的。
- 仔细思考,这样可能带来的问题是,如果需要关注a值变化过程,是会漏掉一段时间窗口的监控
举例分析:
现有一个用单向链表实现的栈,栈顶元素为 a,a.next 为 b,期望用 cas 将栈顶替换成 b。
有线程 1 获取了元素 a,此时线程 1 被挂起,线程 2 也获取了元素 a,并将 a、b 出栈,再 push d、c、a,这时线程 1 恢复执行 cas,因为此时栈顶元素依然为 a,线程 1 执行成功,栈顶元素变成了 b,但 b.next 为 null,这就会导致 c、d 被丢掉了。
这个例子充分说明了 cas 的 aba 问退带来的隐患,通常,我们的乐观锁实现中都会带一个 version 字段来记录更改的版本,避免并发操作带来的问题。在 java 中,atomicstampedreference 也实现了这个处理方式。
atomicstampedreference 的内部类 pair:
private static class pair {
final t reference;
final int stamp;
private pair(t reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
static pair of(t reference, int stamp) {
return new pair(reference, stamp);
}
}
如上,每个 pair 维护一个值,其中 reference 维护对象的引用,stamp 维护修改的版本号。
compareandset 方法:
/**
* atomically sets the value of both the reference and stamp
* to the given update values if the
* current reference is {@code ==} to the expected reference
* and the current stamp is equal to the expected stamp.
*
* @param expectedreference the expected value of the reference
* @param newreference the new value for the reference
* @param expectedstamp the expected value of the stamp
* @param newstamp the new value for the stamp
* @return {@code true} if successful
*/
public boolean compareandset(v expectedreference,
v newreference,
int expectedstamp,
int newstamp) {
pair current = pair;
return
expectedreference == current.reference &&
expectedstamp == current.stamp &&
((newreference == current.reference &&
newstamp == current.stamp) ||
caspair(current, pair.of(newreference, newstamp)));
}
从 compareandset 方法得知,如果要更改内存中的值,不但要值相同,还要版本号相同。
举例分析:
public class atomicstampedreferencetest {
// 初始值为1,版本号为0
private static atomicstampedreference a = new atomicstampedreference<>(1, 0);
// 计数器
private static countdownlatch countdownlatch = new countdownlatch(1);
public static void main(string[] args) {
new thread(() -> {
system.out.println("线程名字:" thread.currentthread() ", 当前 value = " a.getreference());
// 获取当前版本号
int stamp = a.getstamp();
// 计数器阻塞,直到计数器为0,才执行
try {
countdownlatch.await();
} catch (interruptedexception e) {
e.printstacktrace();
}
system.out.println("线程名字:" thread.currentthread() ",cas操作结果: " a.compareandset(1, 2, stamp, stamp 1));
}, "线程1").start();
// 线程2
new thread(() -> {
// 将 value 值改成 2
a.compareandset(1, 2, a.getstamp(), a.getstamp() 1);
system.out.println("线程名字" thread.currentthread() "value = " a.getreference());
// 将 value 值又改成 1
a.compareandset(2, 1, a.getstamp(), a.getstamp() 1);
system.out.println("线程名字" thread.currentthread() "value = " a.getreference());
// 线程计数器
countdownlatch.countdown();
}, "线程2").start();
}
}
2、无限循环问题:大家看源码就知道atomic类设置值的时候会进入一个无限循环,只要不成功,就不停循环再次尝试,这个在高并发修改一个值的时候其实挺常见的,比如你用atomicinteger在内存里搞一个原子变量,然后高并发下,多线程频繁修改,其实可能会导致这个compareandset()里要循环n次才设置成功,所以还是要考虑到的。
jdk 1.8引入的longadder来解决,是一个重点,分段cas思路
3、多变量原子问题:一般的atomicinteger,只能保证一个变量的原子性,但是如果多个变量呢?你可以用atomicreference,这个是封装自定义对象的,多个变量可以放一个自定义对象里,然后他会检查这个对象的引用是不是一个。