Java CAS

引言

在介绍CAS之前,我们有必要先理解线程安全的三大特性

  • 原子性: 对于涉及共享变量访问的操作,该操作从其执行线程以外的任意线程来看是不可分割的,从而可以让各个线程依次串行访问,但是原子性并不保证可见性
  • 可见性: 修改共享变量时,立即将工作内存中的值同步到主存中,并使该修改对其他线程可见
  • 有序性: 禁止读取共享变量后的代码、修改共享变量前的代码重排序

CAScompare and swap的缩写,中文翻译成比较并交换。是一种用于在多线程环境下实现同步功能的机制。调用Java CAS需要三个操作数

  1. 内存中值的内存位置
  2. 预期值
  3. 新值

具体实现是通过值的内存位置取到内存中的值并与预期值比较,若相等,则将内存位置处的值替换为新值,若不相等,则不做任何操作返回false。如果大家有了解过悲观锁和乐观锁,可以发现CAS其实是一种乐观锁的实现。

使用CAS的目的

对于实现线程安全,我们用的比较多的应该是synchronized关键字,synchronized其实是一种悲观锁,锁被占用的情况会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。CAS是一种乐观锁,每次取数据都不会加锁,更新的时候会进行数据比对,有冲突的话则会自旋重试。可以看到在读操作频繁,更新频率低,冲突概率低的情况下,用CAS的话会更加合理。当然JDK1.6之后,Java对synchronized关键字来了一大波优化(自旋锁,锁消除,锁粗化,偏向锁,轻量级锁),一般情况下使用synchronized是非常稳定的。

CAS的底层实现

现在的CPU都是多核心的,多个核心通过总线来操作内存。那么这里就存在一个问题,就是如果多个核心同时操作一块内存区域,会发生什么问题呢?是的,这里数据就会出现混乱。不过这里我们可以从intel的使用手册中找到答案,对指令加lock前缀可以保证操作的原子性,可见性以及有序性。好了,底层的就不多说了,我们直接去看一下java.util.concurrent.atomic包下的原子类 AtomicInteger的源码实现

AtomicInteger源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
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();
// value值的内存位置
private static final long valueOffset;

static {
try {
// 获取value的内存位置
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}

// value值 volatile 修饰 保证可见性
private volatile int value;

public AtomicInteger(int initialValue) {
value = initialValue;
}

public AtomicInteger() {
}

/**
* 获取value在内存中当前的值
*
* @return the current value
*/
public final int get() {
return value;
}

/**
* 比较并替换 实现在unsafe.compareAndSwapInt中
* @param expect 期望值
* @param update 新值
*/
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

/**
* 自增 实现在unsafe.getAndAddInt中
*/
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

}
  • Unsafe.class
1
2
3
4
5
6
7
8
9
10
11
12
13
public final native boolean compareAndSwapInt(Object o, long valueOffset, int expected, int value);

/**
* 获取当前值 并加1,返回的是加1前的值
*/
public final int getAndAddInt(Object o, long valueOffset, int addValue) {
int currentValue;
do {
currentValue = this.getIntVolatile(o, valueOffset);
// 比较当前内存的值和预期值currentValue是否一致,一致的话则设置新值。但是因为当前内存中的值有可能被其他线程修改,会有和预期值不一致的情况,所以这里会循环直到 compareAndSwapInt 返回成功为止,这里的操作也称为CAS自旋
} while(!this.compareAndSwapInt(o, valueOffset, currentValue, value + addValue));
return value;
}

AtomicIntegerJavaInteger类型原子性操作的实现,可以看到底层都是调用了CAS compareAndSwapIntnative方法。
这里主要看一下compareAndSwapInt(Object o, long valueOffset, int expect, int update)的四个参数

  • o 当前操作的对象
  • valueOffset 操作值所在的内存位置
  • expect 期望值
  • update 新值

具体实现是将内存位置处的数值与预期数值相比较,若相等,则将内存位置处的值替换为新值。若不相等,则不做任何操作。
CAS自旋指的是替换新值失败时会进入循环,重新获取期望值,直到期望值和内存位置处的数值相等。

CAS的问题

ABA问题

提到CAS存在的问题,就不得不提ABA问题,什么是ABA问题呢?
举个例子,A是个共享变量,原值是10,线程1从内存中拿到了A,此时值为10,当线程1要对变量A进行CAS操作前,因为其他线程的操作,A从10变为了11,又从11变回了10。此时线程1对变量A执行CAS操作照道理应该是要失败的,但实际却是成功的。这是因为经过了上面的流程,在线程1看来,变量A没有发生任何变化,所以它执行CAS操作是会成功的。

要解决ABA问题,通常的解决方案给对象加上版本号,每经过一次CAS操作就更新一次版本号

总结

本文的目的主要是让自己对java并发包的基础CAS有个简单的了解,以便进行后续的源码分析

Powered by Hexo and Hexo-theme-hiker

Copyright © 2013 - 2020 王俊男的技术杂谈 All Rights Reserved.

访客数 : | 访问量 :