5.4 原子引用
为什么需要原子引用类型?— 用于保证引用类型数据的原子性
- AtomicReference
- AtomicMarkableReference
- AtomicStampedReference
有如下方法
interface DecimalAccount { // 获取余额 BigDecimal getBalance(); // 取款 void withdraw(BigDecimal amount); /** * 方法内会启动 1000 个线程,每个线程做 -10 元 的操作 * 如果初始余额为 10000 那么正确的结果应当是 0 */ static void demo(DecimalAccount account) { List <Thread> ts = new ArrayList <>(); for (int i = 0; i < 1000; i++) { ts.add(new Thread(() -> { account.withdraw(BigDecimal.TEN); })); } ts.forEach(Thread::start); ts.forEach(t -> { try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); } }); System.out.println(account.getBalance()); } }
试着提供不同的 DecimalAccount 实现,实现安全的取款操作
5.4.1 不安全实现
public class Test27 { public static void main(String[] args) { DecimalAccount.demo(new DecimalAccountUnsafe(new BigDecimal("10000"))); } } class DecimalAccountUnsafe implements DecimalAccount { BigDecimal balance; public DecimalAccountUnsafe(BigDecimal balance) { this.balance = balance; } @Override public BigDecimal getBalance() { return balance; } @Override public void withdraw(BigDecimal amount) { BigDecimal balance = this.getBalance(); this.balance = balance.subtract(amount); } }
5.4.2 安全实现-使用锁
class DecimalAccountUnsafe implements DecimalAccount { BigDecimal balance; public DecimalAccountUnsafe(BigDecimal balance) { this.balance = balance; } @Override public BigDecimal getBalance() { return balance; } @Override public void withdraw(BigDecimal amount) { synchronized (this){ BigDecimal balance = this.getBalance(); this.balance = balance.subtract(amount); } } }
5.4.3 安全实现-使用CAS
public class Test27 { public static void main(String[] args) { DecimalAccount.demo(new DecimalAccountCas(new BigDecimal("10000"))); } } class DecimalAccountCas implements DecimalAccount{ private AtomicReference<BigDecimal> balance; public DecimalAccountCas(BigDecimal balance) { this.balance = new AtomicReference <>(balance); } @Override public BigDecimal getBalance() { return balance.get(); } @Override public void withdraw(BigDecimal amount) { while (true){ BigDecimal prev = balance.get(); BigDecimal next = prev.subtract(amount); if(balance.compareAndSet(prev,next)){ break; } } } }
5.4.4 ABA 问题及解决
1. ABA 问题
**问题描述:**一个线程t1在修改共享变量w前,另外一个线程t2也对w进行了修改,但是w修改前后值相等。最终会导致t1也会修改成功! 我们期望的是,只有其他线程修改w了,t1本轮就修改失败!
/** * @author lxy * @version 1.0 * @Description * @date 2022/7/22 15:15 */ @Slf4j(topic = "c.Test28") public class Test28 { static AtomicReference<String> ref = new AtomicReference <>("A"); public static void main(String[] args) { log.debug("main start..."); //获取值 //这个共享变量被其他线程修改过且修改前后值未变能否可以检测出来呢? ---目前的写发不可以 String prev = ref.get(); //如果中间有其他线程干扰,发生了ABA现象,则依旧可以修改成功 other(); Sleeper.sleep(1); //尝试改为C log.debug("change A->C {}",ref.compareAndSet(prev,"C")); } private static void other(){ new Thread(()->{ log.debug("change A->B {}",ref.compareAndSet(ref.get(),"B")); },"t1").start(); Sleeper.sleep(0.5); new Thread(()->{ log.debug("change B->A {}",ref.compareAndSet(ref.get(),"A")); },"t2").start(); } }
输出为:
16:03:10.444 c.Test28 [main] - main start... 16:03:10.481 c.Test28 [t1] - change A->B true 16:03:10.995 c.Test28 [t2] - change B->A true 16:03:12.009 c.Test28 [main] - change A->C true
**结论:**主线程仅能判断出共享变量的值与最初值 A 是否相同,不能感知到这种从 A 改为 B 又 改回 A 的情况,如果主线程希望:
只要有其它线程【动过了】共享变量,那么自己的 cas 就算失败,这时,仅比较值是不够的,需要再加一个版本号 — 可以通过我们接下来的AtomicStampedReference 实现
2. AtomicStampedReference
/** * @author lxy * @version 1.0 * @Description * @date 2022/7/22 15:15 */ @Slf4j(topic = "c.Test29") public class Test29 { static AtomicStampedReference <String> ref = new AtomicStampedReference <>("A",0); public static void main(String[] args) { log.debug("main start..."); //获取值 //这个共享变量被其他线程修改过且修改前后值未变能否可以检测出来呢? ---目前的写发可以 String prev = ref.getReference(); //获取版本号 int stamp = ref.getStamp(); log.debug("版本 {}",stamp); //如果中间有其他线程干扰,发生了ABA现象,由于含有stamp,则无法修改成功 other(); Sleeper.sleep(1); //尝试改为C log.debug("change A->C {}",ref.compareAndSet(prev,"C",stamp,stamp+1)); } //注意:必须先获取版本号,再获取值,才能解决ABA。 //如果先获取值在获取版本号之前就ABA了再获取版本号,就是ABA之后的。 private static void other(){ new Thread(()->{ log.debug("change A->B {}",ref.compareAndSet(ref.getReference(),"B",ref.getStamp(),ref.getStamp()+1)); log.debug("版本 {}",ref.getStamp()); },"t1").start(); Sleeper.sleep(0.5); new Thread(()->{ log.debug("change B->A {}",ref.compareAndSet(ref.getReference(),"A",ref.getStamp(),ref.getStamp()+1)); log.debug("版本 {}",ref.getStamp()); },"t2").start(); } }
输出为:
16:08:56.826 c.Test29 [main] - main start... 16:08:56.828 c.Test29 [main] - 版本 0 16:08:56.864 c.Test29 [t1] - change A->B true 16:08:56.864 c.Test29 [t1] - 版本 1 16:08:57.373 c.Test29 [t2] - change B->A true 16:08:57.373 c.Test29 [t2] - 版本 2 16:08:58.386 c.Test29 [main] - change A->C false
结论:AtomicStampedReference 可以给原子引用加上版本号,追踪原子引用整个的变化过程,如: A -> B -> A ->C ,通过AtomicStampedReference,我们可以知道,引用变量中途被更改了几次。
**改进:**但是有时候,并不关心引用变量更改了几次,只是单纯的关心是否更改过,所以就有了AtomicMarkableReference
3. AtomicMarkableReference
AtomicMarkableReference 可以不关心引用变量的修改次数,只关心是否被修改过 ~ ~~
如下图:主人只关心 垃圾袋是否为空,并不关心垃圾袋被保洁倒了几次
代码实现
/** * @author lxy * @version 1.0 * @Description * @date 2022/7/22 15:39 */ @Slf4j(topic = "c.Test30") public class Test30 { public static void main(String[] args) { GarbageBag bag = new GarbageBag("装满了垃圾"); // 参数2 mark可以看做一个标记,表示垃圾装满了 AtomicMarkableReference <GarbageBag> ref = new AtomicMarkableReference <>(bag, true); log.debug("start..."); GarbageBag prev = ref.getReference(); log.debug(prev.toString()); new Thread(()->{ log.debug("start..."); bag.setDesc("空垃圾袋"); ref.compareAndSet(bag,bag,true,false); log.debug(bag.toString()); },"保洁阿姨").start(); Sleeper.sleep(1); log.debug("想换一只新垃圾袋?"); boolean success = ref.compareAndSet(prev, new GarbageBag("空垃圾袋"), true, false); log.debug("换了么?"+success); log.debug(ref.getReference().toString()); } } class GarbageBag { String desc; public GarbageBag(String desc) { this.desc = desc; } public void setDesc(String desc) { this.desc = desc; } @Override public String toString() { return super.toString() + " " + desc; } }
输出为:
16:17:12.629 c.Test30 [main] - start... 16:17:12.631 c.Test30 [main] - com.rg.thread.GarbageBag@33e5ccce 装满了垃圾 16:17:12.667 c.Test30 [保洁阿姨] - start... 16:17:12.667 c.Test30 [保洁阿姨] - com.rg.thread.GarbageBag@33e5ccce 空垃圾袋 16:17:13.671 c.Test30 [main] - 想换一只新垃圾袋? 16:17:13.671 c.Test30 [main] - 换了么?false 16:17:13.671 c.Test30 [main] - com.rg.thread.GarbageBag@33e5ccce 空垃圾袋
5.5 原子数组
通过原子的方式,更新数组里的某个元素(修改元素的内容,而非元素的引用地址)
- AtomicIntegerArray
- AtomicLongArray
- AtomicReferenceArray
注意:compareAndSet()
的比较是通过引用地址比较的,之前是对String举例,String的不可变性导致了我们每次对String的更改也导致了引用的更改
代码演示:
public class Test31 { public static void main(String[] args) { // 普通数组 demo( ()->new int[10], array->array.length, (array,index)->array[index]++, array->System.out.println(Arrays.toString(array)) ); //原子数组 demo( ()->new AtomicIntegerArray(10), array->array.length(), (array,index)->array.getAndIncrement(index), array-> System.out.println(array) ); } /** 参数1,提供数组、可以是线程不安全数组或线程安全数组 参数2,获取数组长度的方法 参数3,自增方法,回传 array, index 参数4,打印数组的方法 */ // supplier 提供者 无中生有 ()->结果 // function 函数 一个参数一个结果 (参数)->结果 , BiFunction (参数1,参数2)->结果 // consumer 消费者 一个参数没结果 (参数)->void, BiConsumer (参数1,参数2)-> private static <T> void demo( Supplier <T> arraySupplier, Function <T, Integer> lengthFun, BiConsumer <T, Integer> putConsumer, Consumer <T> printConsumer ) { List <Thread> ts = new ArrayList <>(); T array = arraySupplier.get(); int length = lengthFun.apply(array); for (int i = 0; i < length; i++) {// 10个线程 10000 自增操作,最终每个线程的值应该都是10000 // 每个线程对数组作 10000 次操作 ts.add(new Thread(() -> { for (int j = 0; j < 10000; j++) { putConsumer.accept(array, j%length); } })); } ts.forEach(t -> t.start()); // 启动所有线程 ts.forEach(t -> { try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); } }); // 等所有线程结束 printConsumer.accept(array); } }
输出为:
[9984, 9983, 9979, 9985, 9985, 9985, 9985, 9982, 9982, 9986] //普通数组 [10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000]//原子数组
5.6 字段更新器
用于保证多个线程访问对象时,保证成员变量(属性)的安全性
AtomicReferenceFieldUpdater // 域 字段是引用类型
AtomicIntegerFieldUpdater //字段时Integer类型
AtomicLongFieldUpdater //字段时Long类型
利用字段更新器,可以针对对象的某个域(Field)进行原子操作,只能配合 volatile 修饰的字段使用,否则会出现异常
/** * @author lxy * @version 1.0 * @Description * @date 2022/7/22 17:21 */ @Slf4j(topic = "c.Test32") public class Test32 { public static void main(String[] args) { Student student = new Student(); AtomicReferenceFieldUpdater <Student, String> updater = AtomicReferenceFieldUpdater.newUpdater(Student.class, String.class, "name"); System.out.println(updater.compareAndSet(student,null,"张三")); System.out.println(student); } } class Student { //字段一定要用volatile修饰哦,否则会报异常 volatile String name; @Override public String toString() { return "Student{" + "name='" + name + '\'' + '}'; } }