目录
4 ABA问题以及AtomicStampedReference的使用
1 Atomic类作用
前面文章我们知道volatile只能保证可见性,不能保证原子性,下面我们通过代码证明一下
// 定义一个volatile变量
private volatile int value = 0;
@Test
public void volatileTest() throws InterruptedException {
// 循环10000次,每次自增1
for (int i = 0; i < 10000; i ++) {
new Thread(() -> value ++).start();
}
TimeUnit.SECONDS.sleep(1);
System.out.println(value);
}
通过测试我们可以看到程序输出结果不确定的,大都达不到10000,可以说明transient不能保证原子性。
为了满足jvm内存模型原子性特性,juc并发包提供了Atomic类来保证原子性。
2 AtomicInteger使用
2.1 AtomicInteger用法
同样的功能我们用AtomicInteger来测试
private AtomicInteger value = new AtomicInteger();
@Test
public void atomicIntegerTest() throws InterruptedException {
for (int i = 0; i < 10000; i ++) {
new Thread(() -> value.addAndGet(1)).start();
}
TimeUnit.SECONDS.sleep(1);
System.out.println(value);
}
每次输出的结果都是10000,可以说明AtomicInteger保证原子性
2.2 AtomicInteger原理
AtomicInteger原理也比较简单,就是前面文章讲过的CAS自旋volatile变量。
2.2.1 重要成员变量
// 定义一个volatile变量
private volatile int value;
// value属性的内存偏移量,在static块中初始化
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
2.2.2 addAndGet方法
public final int addAndGet(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
}
/**
* var1:当前对象
* var2:某个属性的内存地址偏移量
* var4: 增量
*/
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;
}
2.3 AtomicInteger优缺点
优点:无锁,效率高
缺点:cpu占用较高,只能保证一个共享变量原子操作,ABA问题
解决方案
1 无解,高性能是建立在高CPU占用的基础上实现的
2 AtomicReference
3 AtomicStampedReference (多了一个stamp变量,可以理解为版本号)
3 AtomicReference使用
3.1 AtomicReference用法
创建一个员工类,有一个id和age属性,多线程对这个员工的id和age同时+1,循环10000次,观察是否会有并发问题
@Data
public class Employee implements Serializable {
private static final long serialVersionUID = 7773021982257206440L;
private int id;
private int age;
测试代码
@Test
public void atomicReferenceTest() throws InterruptedException {
for (int i = 0; i < 10000; i ++) {
new Thread(() -> employeeAtomicReference.updateAndGet(employee -> {
Employee newEmp = new Employee();
newEmp.setId(employee.getId() + 1);
newEmp.setAge(employee.getAge() + 1);
return newEmp;
})).start();
}
TimeUnit.SECONDS.sleep(1);
System.out.println(employeeAtomicReference.get());
}
输出Employee{id=10000, age=10000}
3.2 AtomicReference原理
3.2.1 重要成员变量
// 定义一个泛型volatile变量
private volatile V value;
// value属性的内存偏移量,在static块中初始化
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
3.2.3 updateAndGet方法
public final V updateAndGet(UnaryOperator<V> updateFunction) {
V prev, next;
do {
// 获取当前对象
prev = get();
// 获取执行后的对象
next = updateFunction.apply(prev);
// CAS更新对象为next
} while (!compareAndSet(prev, next));
return next;
}
/**
* expect 希望的当前值
* update 要修改的值
*/
public final boolean compareAndSet(V expect, V update) {
return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
}
3.3.3 AtomicReference使用误区
在使用AtomicReference的时候做对象操作的时候一定要用一个新建的对象,否则不能保证原子性
错误用例:
@Test
public void atomicReferenceTest() throws InterruptedException {
for (int i = 0; i < 10000; i ++) {
new Thread(() -> employeeAtomicReference.updateAndGet(employee -> {
// 这里直接用原来的对象进行修改
employee.setId(employee.getId() + 1);
employee.setAge(employee.getAge() + 1);
return employee;
})).start();
}
TimeUnit.SECONDS.sleep(1);
System.out.println(employeeAtomicReference.get());
}
输出:Employee{id=9995, age=9996}
问题原因:
看CAS参数compareAndSet(prev, next)方法,prev参数和next参数实际上都是AtomicReference内部引用的employee对象
所以该方法始终返回true,达不到并发控制效果
4 ABA问题以及AtomicStampedReference的使用
4.1 ABA问题
整个CAS过程分为3步,查询当前值,比较希望的值(第一步取到的当前值)和当前值,修改。这里假设有3个线程操作数据
上个流程图可知线程一先获取数据,然后挂起。
线程二和线程三修改数据,但是值没有变。
这个时候线程一不能感知数据已经被修改过,进行CAS修改成功。这就是ABA问题
4.2 AtomicStampedReference使用
AtomicStampedReference解决ABA问题的方法也很简单,在原有的CAS方法在expectValue的基础上多了一个expectStamp条件的判断,
不仅expectedValue要和当前值相等,expectedStamp也要和当前值相等。
测试代码
private AtomicStampedReference<Integer> value = new AtomicStampedReference<>(0, 0);
@Test
public void atomicReferenceTest() throws InterruptedException {
System.out.println(value.compareAndSet(0, 1, 1, 1));
System.out.println(value.getReference() + " " + value.getStamp());
System.out.println(value.compareAndSet(0, 1, 0, 1));
System.out.println(value.getReference() + " " + value.getStamp());
}
输出
false
0 0
true
1 1
4.3 AtomicStampedReference原理
4.3.1 重要成员变量
// 定义一个静态内部类,维护reference和stamp两个属性
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);
}
}
// 定义一个volatile的Pair对象
private volatile Pair<V> pair;
private static final long pairOffset = objectFieldOffset(UNSAFE, "pair", AtomicStampedReference.class);
4.3.2 compareAndSet方法
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
// CAS的时候要保证expectedReference和当前reference相等,expectedStamp和当前stamp相等,才能进行CAS处理
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
// CAS修改成员变量pair
private boolean casPair(Pair<V> cmp, Pair<V> val) {
return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}