博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
【实战Java高并发程序设计2】无锁的对象引用:AtomicReference
阅读量:6429 次
发布时间:2019-06-23

本文共 2954 字,大约阅读时间需要 9 分钟。

AtomicReference和AtomicInteger非常类似,不同之处就在于AtomicInteger是对整数的封装,而AtomicReference则对应普通的对象引用。也就是它可以保证你在修改对象引用时的线程安全性。在介绍AtomicReference的同时,我希望同时提出一个有关原子操作的逻辑上的不足。

之前我们说过,线程判断被修改对象是否可以正确写入的条件是对象的当前值和期望是否一致。这个逻辑从一般意义上来说是正确的。但有可能出现一个小小的例外,就是当你获得对象当前数据后,在准备修改为新值前,对象的值被其他线程连续修改了2次,而经过这2次修改后,对象的值又恢复为旧值。这样,当前线程就无法正确判断这个对象究竟是否被修改过。如图4.2所示,显示了这种情况。

Center

图4.2 对象值被反复修改回原数据

一般来说,发生这种情况的概率很小。而且即使发生了,可能也不是什么大问题。比如,我们只是简单得要做一个数值加法,即使在我取得期望值后,这个数字被不断的修改,只要它最终改回了我的期望值,我的加法计算就不会出错。也就是说,当你修改的对象没有过程的状态信息,所有的信息都只保存于对象的数值本身。

但是,在现实中,还可能存在另外一种场景。就是我们是否能修改对象的值,不仅取决于当前值,还和对象的过程变化有关,这时,AtomicReference就无能为力了。

打一个比方,如果有一家蛋糕店,为了挽留客户,绝对为贵宾卡里余额小于20元的客户一次性赠送20元,刺激消费者充值和消费。但条件是,每一位客户只能被赠送一次。

现在,我们就来模拟这个场景,为了演示AtomicReference,我在这里使用AtomicReference实现这个功能。首先,我们模拟用户账户余额。

定义用户账户余额:

static AtomicReference
money=newAtomicReference
(); // 设置账户初始值小于20,显然这是一个需要被充值的账户 money.set(19);

接着,我们需要若干个后台线程,它们不断扫描数据,并为满足条件的客户充值。

01 //模拟多个线程同时更新后台数据库,为用户充值  02 for(int i = 0 ; i < 3 ; i++) {               03     new Thread(){   04         publicvoid run() {   05            while(true){  06                while(true){  07                    Integer m=money.get();  08                    if(m<20){  09                        if(money.compareAndSet(m, m+20)){  10                  System.out.println("余额小于20元,充值成功,余额:"+money.get()+"元");  11                             break;  12                        }  13                    }else{  14                        //System.out.println("余额大于20元,无需充值");  15                         break ;  16                    }  17                 }  18             }  19         }   20     }.start();  21 }

上述代码第8行,判断用户余额并给予赠予金额。如果已经被其他用户处理,那么当前线程就会失败。因此,可以确保用户只会被充值一次。

此时,如果很不幸的,用户正好正在进行消费,就在赠予金额到账的同时,他进行了一次消费,使得总金额又小于20元,并且正好累计消费了20元。使得消费、赠予后的金额等于消费前、赠予前的金额。这时,后台的赠予进程就会误以为这个账户还没有赠予,所以,存在被多次赠予的可能。下面,模拟了这个消费线程:

01 //用户消费线程,模拟消费行为  02 new Thread() {   03     public voidrun() {   04         for(inti=0;i<100;i++){  05            while(true){  06                Integer m=money.get();  07                 if(m>10){  08                    System.out.println("大于10元");  09                    if(money.compareAndSet(m, m-10)){  10                        System.out.println("成功消费10元,余额:"+money.get());  11                        break;  12                    }  13                }else{  14                    System.out.println("没有足够的金额");  15                    break;  16                 }  17             }  18             try{Thread.sleep(100);} catch (InterruptedException e) {}  19         }  20     }   21 }.start();

述代码中,消费者只要贵宾卡里的钱大于10元,就会立即进行一次10元的消费。执行上述程序,得到的输出如下:

余额小于20元,充值成功,余额:39元  大于10元  成功消费10元,余额:29  大于10元  成功消费10元,余额:19  余额小于20元,充值成功,余额:39元  大于10元  成功消费10元,余额:29  大于10元  成功消费10元,余额:39  余额小于20元,充值成功,余额:39元

从这一段输出中,可以看到,这个账户被先后反复多次充值。其原因正是因为账户余额被反复修改,修改后的值等于原有的数值。使得CAS操作无法正确判断当前数据状态。

虽然说这种情况出现的概率不大,但是依然是有可能的出现的。因此,当业务上确实可能出现这种情况时,我们也必须多加防范。体贴的JDK也已经为我们考虑到了这种情况,使用AtomicStampedReference就可以很好的解决这个问题。

摘自《实战Java高并发程序设计》一书

Center

转载地址:http://kciga.baihongyu.com/

你可能感兴趣的文章
amazon s3 的用户验证 access-key, secrete-key
查看>>
Ubuntu-server 下Apache2 配置.htaccess 隐藏thinkPHP项目index.php无效解决办法
查看>>
centos7 mysql数据库安装和配置
查看>>
机器数的定点、浮点表示及阶码、移码
查看>>
Nivoslider插件参数和方法
查看>>
redis hashmap数据结构分析
查看>>
逻辑是PPT的灵魂
查看>>
modelForm所遇问题
查看>>
K - Kia's Calculation (贪心)
查看>>
android笔试题一
查看>>
【JavaEE企业应用实战学习记录】getConnListener
查看>>
了解轮询、长轮询、长连接、websocket
查看>>
bzoj2427[HAOI2010]软件安装
查看>>
bzoj1593[Usaco2008 Feb]Hotel 旅馆*
查看>>
SQL语句中DateAdd 函数说明
查看>>
柔性数组
查看>>
WPF个人助手更新
查看>>
NLPIR技术助力中文智能数据挖掘
查看>>
python操作redis--------------数据库增删改查
查看>>
Android中仿IOS提示框的实现
查看>>