【实战Java高并发程序设计 2】无锁的对象引用:AtomicReference

  AtomicReference和AtomicInteger很是相似,不一样之处就在于AtomicInteger是对整数的封装,而AtomicReference则对应普通的对象引用。也就是它能够保证你在修改对象引用时的线程安全性。在介绍AtomicReference的同时,我但愿同时提出一个有关原子操做的逻辑上的不足。html

   以前咱们说过,线程判断被修改对象是否能够正确写入的条件是对象的当前值和指望是否一致。这个逻辑从通常意义上来讲是正确的。但有可能出现一个小小的例外,就是当你得到对象当前数据后,在准备修改成新值前,对象的值被其余线程连续修改了2次,而通过这2次修改后,对象的值又恢复为旧值。这样,当前线程就没法正确判断这个对象到底是否被修改过。如图4.2所示,显示了这种状况。java

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

   通常来讲,发生这种状况的几率很小。并且即便发生了,可能也不是什么大问题。好比,咱们只是简单得要作一个数值加法,即便在我取得指望值后,这个数字被不断的修改,只要它最终改回了个人指望值,个人加法计算就不会出错。也就是说,当你修改的对象没有过程的状态信息,全部的信息都只保存于对象的数值自己。安全

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

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

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

定义用户帐户余额:orm

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


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

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就能够很好的解决这个问题。


推荐本书:

相关文章
相关标签/搜索