AtomicReference原子应用类,可以保证你在修改对象引用时的线程安全性,比较时可以按照偏移量进行
这里的cas操作本身是原子的,但是在某些场景下会出现异常场景
线程判断被修改对象是否可以正确写入的条件是对象的当前值和期望是否一致。这个逻辑从一般意义上来说是正确的。但有可能出现一个小小的例外,就是当你获得对象当前数据后,在准备修改为新值前,对象的值被其他线程连续修改了2次,而经过这2次修改后,对象的值又恢复为旧值。这样,当前线程就无法正确判断这个对象究竟是否被修改过。如图所示,显示了这种情况。(图片是转载而来)
一般来说,发生这种情况的概率很小。而且即使发生了,可能也不是什么大问题。比如,我们只是简单得要做一个数值加法,即使在我取得期望值后,这个数字被不断的修改,只要它最终改回了我的期望值,我的加法计算就不会出错。也就是说,当你修改的对象没有过程的状态信息,所有的信息都只保存于对象的数值本身。
但是,在现实中,还可能存在另外一种场景。就是我们是否能修改对象的值,不仅取决于当前值,还和对象的过程变化有关,这时,AtomicReference就无能为力了。
打一个比方,如果有一家蛋糕店,为了挽留客户,绝对为贵宾卡里余额小于20元的客户一次性赠送20元,刺激消费者充值和消费。但条件是,每一位客户只能被赠送一次。
现在,我们就来模拟这个场景,为了演示AtomicReference,我在这里使用AtomicReference实现这个功能。
package algorithmProject.concurrent;
import java.util.concurrent.atomic.AtomicReference;
/**
* Created by wangkai on 2017/4/24.
*/
public class AtomicReferenceDemo {
// 设置账户初始值小于20,显然这是一个需要被充值的账户
static AtomicReference money = new AtomicReference(19);
public static void main(String args[]) {
//模拟多个线程同时更新后台数据库,为用户充值
for (int i = 0; i < 3; i++) {
new Thread() {
public void run() {
while (true) {
while (true) {
Integer m = money.get();
if (m < 20) {
if (money.compareAndSet(m, m + 20)) {
System.out.println("余额小于20元,充值成功,余额:" + money.get() + "元");
break;
}
} else {
//System.out.println("余额大于20元,无需充值");
break;
}
}
}
}
}.start();
}
//有一个线程一直在消费
new Thread() {
public void run() {
for (int i = 0; i < 100; i++) {
while (true) {
Integer m = money.get();
if (m > 10) {
System.out.println("大于10元");
if (money.compareAndSet(m, m - 10)) {
System.out.println("成功消费10元,余额:" + money.get());
break;
}
} else {
System.out.println("没有足够的金额");
break;
}
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
}
}
}.start();
}
}
首先判断用户余额并给予赠予金额。如果已经被其他用户处理,那么当前线程就会失败。因此,可以确保用户只会被充值一次。
此时,如果很不幸的,用户正好正在进行消费,就在赠予金额到账的同时,他进行了一次消费,使得总金额又小于20元,并且正好累计消费了20元。使得消费、赠予后的金额等于消费前、赠予前的金额。这时,后台的赠予进程就会误以为这个账户还没有赠予,所以,存在被多次赠予的可能。万幸的是jdk给我提供了一个类AtomicStampedReference
AtomicReference无法解决上述问题的根本是因为对象在修改过程中,丢失了状态信息。对象值本身与状态被画上了等号。因此,我们只要能够记录对象在修改过程中的状态值,就可以很好的解决对象被反复修改导致线程无法正确判断对象状态的问题。
AtomicStampedReference正是这么做的。它内部不仅维护了对象值,还维护了一个时间戳(我这里把它称为时间戳,实际上它可以使任何一个整数,它使用整数来表示状态值)。当AtomicStampedReference对应的数值被修改时,除了更新数据本身外,还必须要更新时间戳。当AtomicStampedReference设置对象值时,对象值以及时间戳都必须满足期望值,写入才会成功。因此,即使对象值被反复读写,写回原值,只要时间戳发生变化,就能防止不恰当的写入。
package algorithmProject.concurrent;
import java.util.concurrent.atomic.AtomicStampedReference;
/**
* Created by wangkai on 2017/4/24.
*/
public class AtomicStampedReferenceDemo {
static AtomicStampedReference money = new AtomicStampedReference(19, 0);
public static void main(String[] args) {
//模拟多个线程同时更新后台数据库,为用户充值
for (int i = 0; i < 3; i++) {
//获得当前时间戳
final int timestamp = money.getStamp();
new Thread() {
public void run () {
while (true) {
while (true) {
//获得当前对象引用
Integer m = money.getReference();
if (m < 20) {
//比较设置 参数依次为:期望值 写入新值 期望时间戳 新时间戳
if (money.compareAndSet(m, m + 20, timestamp, timestamp + 1)) {
System.out.println("余额小于20元,充值成功,余额:" + money.getReference() + "元");
break;
}
} else {
//System.out.println("余额大于20元,无需充值");
break;
}
}
}
}
}.start();
}
//用户消费线程,模拟消费行为
new Thread() {
public void run() {
for (int i = 0; i < 100; i++) {
while (true) {
int timestamp = money.getStamp();
Integer m = money.getReference();
if (m > 10) {
System.out.println("大于10元");
if (money.compareAndSet(m, m - 10, timestamp, timestamp + 1)) {
System.out.println("成功消费10元,余额:" + money.getReference());
break;
}
} else {
System.out.println("没有足够的金额");
break;
}
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
}
}
}.start();
}
}