摘要:实战高并发程序设计连载中的指针类和非常类似,不同之处就在于是对整数的封装,而则对应普通的对象引用。这样,当前线程就无法正确判断这个对象究竟是否被修改过。摘自实战高并发程序设计一书
【实战Java高并发程序设计】连载1–Java中的指针:Unsafe类
AtomicReference和AtomicInteger非常类似,不同之处就在于AtomicInteger是对整数的封装,而AtomicReference则对应普通的对象引用。也就是它可以保证你在修改对象引用时的线程安全性。在介绍AtomicReference的同时,我希望同时提出一个有关原子操作的逻辑上的不足。
之前我们说过,线程判断被修改对象是否可以正确写入的条件是对象的当前值和期望是否一致。这个逻辑从一般意义上来说是正确的。但有可能出现一个小小的例外,就是当你获得对象当前数据后,在准备修改为新值前,对象的值被其他线程连续修改了2次,而经过这2次修改后,对象的值又恢复为旧值。这样,当前线程就无法正确判断这个对象究竟是否被修改过。如图4.2所示,显示了这种情况。
图4.2 对象值被反复修改回原数据
一般来说,发生这种情况的概率很小。而且即使发生了,可能也不是什么大问题。比如,我们只是简单得要做一个数值加法,即使在我取得期望值后,这个数字被不断的修改,只要它最终改回了我的期望值,我的加法计算就不会出错。也就是说,当你修改的对象没有过程的状态信息,所有的信息都只保存于对象的数值本身。
但是,在现实中,还可能存在另外一种场景。就是我们是否能修改对象的值,不仅取决于当前值,还和对象的过程变化有关,这时,AtomicReference就无能为力了。
打一个比方,如果有一家蛋糕店,为了挽留客户,绝对为贵宾卡里余额小于20元的客户一次性赠送20元,刺激消费者充值和消费。但条件是,每一位客户只能被赠送一次。
现在,我们就来模拟这个场景,为了演示AtomicReference,我在这里使用AtomicReference实现这个功能。首先,我们模拟用户账户余额。
定义用户账户余额:
static AtomicReferencemoney=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高并发程序设计》一书
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/65492.html
摘要:在本例中,讲述的无锁来自于并发包我们将这个无锁的称为。在这里,我们使用二维数组来表示的内部存储,如下变量存放所有的内部元素。为什么使用二维数组去实现一个一维的呢这是为了将来进行动态扩展时可以更加方便。 我们已经比较完整得介绍了有关无锁的概念和使用方法。相对于有锁的方法,使用无锁的方式编程更加考验一个程序员的耐心和智力。但是,无锁带来的好处也是显而易见的,第一,在高并发的情况下,它比有锁...
摘要:有时候,由于初期考虑不周,或者后期的需求变化,一些普通变量可能也会有线程安全的需求。它可以让你在不改动或者极少改动原有代码的基础上,让普通的变量也享受操作带来的线程安全性,这样你可以修改极少的代码,来获得线程安全的保证。 有时候,由于初期考虑不周,或者后期的需求变化,一些普通变量可能也会有线程安全的需求。如果改动不大,我们可以简单地修改程序中每一个使用或者读取这个变量的地方。但显然,这...
摘要:公平锁非公平锁公平锁公平锁是指多个线程按照申请锁的顺序来获取锁。加锁后,任何其他试图再次加锁的线程会被阻塞,直到当前进程解锁。重量级锁会让其他申请的线程进入阻塞,性能降低。 Java 中15种锁的介绍 在读很多并发文章中,会提及各种各样锁如公平锁,乐观锁等等,这篇文章介绍各种锁的分类。介绍的内容如下: 公平锁 / 非公平锁 可重入锁 / 不可重入锁 独享锁 / 共享锁 互斥锁 / 读...
摘要:但是,有些操作会依赖于对象的变化过程,此时的解决思路一般就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么就会变成。四的引入就是上面所说的加了版本号的。 showImg(https://segmentfault.com/img/remote/1460000016012188); 本文首发于一世流云的专栏:https://segmentfault.com/blo...
摘要:并发包将这种无锁方案封装提炼之后,实现了一系列的原子类。无锁方案相对互斥锁方案,最大的好处就是性能。作为一条指令,指令本身是能够保证原子性的。 前面我们多次提到一个累加器的例子,示例代码如下。在这个例子中,add10K() 这个方法不是线程安全的,问题就出在变量 count 的可见性和 count+=1 的原子性上。可见性问题可以用 volatile 来解决,而原子性问题我们前面一直都...
阅读 929·2019-08-30 15:55
阅读 502·2019-08-26 13:56
阅读 2036·2019-08-26 12:23
阅读 3274·2019-08-26 10:29
阅读 579·2019-08-26 10:17
阅读 2813·2019-08-23 16:53
阅读 650·2019-08-23 15:55
阅读 2725·2019-08-23 14:25