文章目录
1. 什么是CAS?
CAS: compare and swap(compare and exchange),比较并交换,读取内存中的数值,计算修改后,将修改前的值与内存值比较,如果相同,则说明没有其他线程修改过,就将修改后的值写入内存;如果不同,则说明被改过,重复上述过程,直到写入成功,听着有点蒙,没关系,看一张图:
CAS究竟干了什么事呢?举个例子,假如有两个线程想对一个值【V】进行+1操作,第一个进程进来之后,先把这个值V【0】取出来,进行+1操作,然后再把+1后的结果【1】写回去之前,我先判断这个值是否依然为0,如果我发现这个值不是0了,变成【2】了,那么我就知道这肯定是有其他线程将这个值改掉了,这时候我不去写这个值,而是把这个值【2】再次读出来,再进行+1操作,写之前我判断这个值V是否为【2】,如果不为2,我就一直循环下去,直到成功为止。这个过程是不需要加锁的,所以说CAS叫无锁,又叫自旋锁。
2. ABA问题
看到这你可能有个疑问,如果我这个线程将值改成【1】后,在我往回写的过程之中,有一个线程将这个值【0】取走了,改成【1】,写回去,又有一个线程将值【1】取走了,改成了【0】,这个值从0 -->1 --> 0,如果是简单的值从0到1又回到0,这种没问题,不影响最终的结果,但是如果值是引用类型,不允许中间有人改过,那这就有问题,这就是CAS的ABA问题。
怎么解决?加版本号,举个例子,比如说你和你的女朋友分手了,分手之后她又经历了别的男人,又回到了你的身边,但是你发现她和以前不太一样了,那么你怎么察觉这一点?很简单,她离开的时候在她脑门上贴个标签1.0,当她再回来的时候,发现脑门上是99.0,那你就知道她到底经历了什么。
如果问你JDK具体的实现,BooleanReference, 用布尔值标注它有没有改变或版本号version标注。
从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference
来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
3. compareAndSwapInt()
3.1 AtomicInteger例子
看到这你可能又会问,你CAS不是比较并交换吗,如果一个线程比较完之后,将要写回的过程,另一个线程将这个值改了,那你不是傻了吗。其实最后这一步也就是写回的操作是原子操作,是不允许打断的,那么这个原子操作是怎么实现的呢?
如果我想实现一个功能,对一个值进行自增操作,创建100个线程,每个线程都对这个值进行10000次自增,那么期待值就是1000000,先用synchronized实现:
private static AtomicInteger m = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[100];
CountDownLatch latch = new CountDownLatch(threads.length); //初始个数
for (int i = 0; i < 100; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 10000; j++) {
m.incrementAndGet();
}
});
latch.countDown(); //当前子线程执行完,就减1,直至为0,表示所有的子线程都执行完了
}
Arrays.stream(threads).forEach(Thread::start);
latch.await(); //主线程阻塞,等所有的子线程执行完
System.out.println(m);
}
AtomicInteger
底层都是CAS实现的,CountDownLatch
叫门栓,latch.await();
的意思就是把门栓打开,把门堵住,想进来必须拿到锁,必须等签名100把锁latch.countDown()
到0了才能执行下面的操作,等同于写100遍thread.join();
说跑题了,这里的核心是m.incrementAndGet();
,它没有加锁是怎么保证数据一致性的?
3.2 AtomicInteger的incrementAndGet()底层实现
简单,进源码看下:
incrementAndGet()
返回的是修改后的值,注意和getAndAdd()
进行区分,后者返回的是修改前的值。
/**
* Atomically increments by one the current value.
*
* @return the updated value 返回的是修改后的值,注意和getAndAdd进行区分
*/
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
注意:不同jdk版本,底层代码实现稍有不同!
上面的代码有点难以理解的地方,可以看下图绿色部分的解释:
这里解释一下valueOffset
变量,首先valueOffset的初始化在static静态代码块里面,代表value成员变量相对起始内存地址的字节相对偏移量:
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
AtomicInteger类在编译时,就确定了成员变量的顺序,包括value成员,算出偏移量,后续,如果知道了AtomicInteger实例的对象的地址后,那么根据偏移量,就能得到value属性的地址,那么操作内存时,传入对象和偏移量,就等价传入对象和该对象的成员变量,那么就能修改该对象的指定成员变量。
在生成一个AtomicInteger对象后,可以看做生成了一段内存,对象中各个字段按一定顺序放在这段内存中,字段可能不是连续放置的,unsafe.objectFieldOffset(Field f)
这个方法准确地告诉我"value"字段相对于AtomicInteger对象的起始内存地址的字节相对偏移量:
private volatile int value;
/**
* Creates a new AtomicInteger with the given initial value.
*
* @param initialValue the initial value
*/
public AtomicInteger(int initialValue) {
value = initialValue;
}
你会发现它调用了unsafe类的getAndAddInt()方法,点进去:
public final int getAndAddInt(Object obj, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(obj, var2);
} while(!this.compareAndSwapInt(obj, var2, var5, var5 + var4));
return var5;
}
compareAndSwapInt
,这个方法怎么看着那么熟悉,What?这不就是CAS吗?点进去:
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
很不幸,只能到这里了,看到了native
,这是调用了C++代码的实现,如果你想继续往下看,那你只能看UnSafe的底层C++代码了
3.2.1 C++代码实现
上网搜unsafe.cpp compareAndSetInt
代码实现:
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSetInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x)) {
oop p = JNIHandles::resolve(obj);
//获取对象的变量的地址
jint* addr = (jint *)index_oop_from_field_offset_long(p, offset);
//调用Atomic操作
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
} UNSAFE_END
发现最终调用了cmpxchg
方法:
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
int mp = os::is_MP(); //判断是否为单核
__asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
: "=a" (exchange_value)
: "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
: "cc", "memory");
return exchange_value;
}
这里已经是底层汇编实现了,看到LOCK_IF_MP,如果mp为true,就Lock,那么我们看下is_MP方法:
static inline bool is_MP() {
// During bootstrap if _processor_count is not yet initialized
// we claim to be MP as that is safest. If any platform has a
// stub generator that might be triggered in this phase and for
// which being declared MP when in fact not, is a problem - then
// the bootstrap routine for the stub generator needs to check
// the processor count directly and leave the bootstrap routine
// in place until called after initialization has ocurred.
return (_processor_count != 1) || AssumeMP;
}
看到_processor_count != 1
进程数不等于1,什么意思呢,就是不是单核的,再看下LOCK_IF_MP
方法:
// Adding a lock prefix to an instruction on MP machine
#define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: "
从注释翻译出来, 添加一个“lock”的前缀,在哪添加呢?在某一指令上,MP的意思是multi processor,多核机器,也就是在多核上添加一个lock的前缀。所以最终汇编调用的指令是lock cmpxchg
,这条汇编的指令的作用就是锁总线。锁总线什么意思?如果多CPU也就是多核,未防止我写入的时候被其他CPU打断,我把CPU通往内存的一组线给锁定,不允许其他CPU去修改。
所以CAS的底层实现就是lock cmpxchg
,多核才会加锁,单核不需要。
4. compareAndSwapObject()
我们AQS源码(AbstractQueuedSynchronizer
)中添加同步队列节点的逻辑:
不断地自旋
加入一个新的节点compareAndSetTail或compareAndSetHead:
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
如果某个对象的tailOffset属性指向的对象地址仍是expect对象的话,那么把tailOffset属性指向的对象地址变更为update对象。