尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

CAS和原子操作类

CAS和原子操作类 一.CAS1.什么是CASCompare-And-Swap即对比交换。是一条CPU的原子指令作用是让CPU先进行比较两个值是否相等然后原子地更新某个位置的值。CAS的实现方式是基于硬件平台的汇编指令也就是说CAS是靠硬件实现的JVM只是封装了汇编调用。简单解释:CAS操作需要输入两个数值一个旧值(期望操作前的值)和一个新值在操作期间先比较旧值有没有发生变化如果没有发生变化才交换为新值发生了变化则不交换2.CAS使用示例之前并发线程累加操作同一个变量500次但最终结果一般到不了500是因为在并发状态下线程是不安全的如果我们把操作变量改为AtomicInteger就可以解决public class Main { //main方法里都一样 public static void main(String[] args) throws InterruptedException { final int threadSize 500; ThreadUnsafeExample example new ThreadUnsafeExample(); final CountDownLatch countDownLatch new CountDownLatch(threadSize); ExecutorService executorService Executors.newCachedThreadPool(); for (int i 0; i threadSize; i) { executorService.execute(() - { example.add(); countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); System.out.println(example.get()); } } class ThreadUnsafeExample { //把操作的共享变量int改为AtomicInteger private AtomicInteger count new AtomicInteger(0); public void add() { count; } public int get() { return count; } }3.CAS问题CAS方式为乐观锁synchronized为悲观锁因此使用CAS解决并发问题通常情况下性能更优乐观锁和悲观锁是用来解决并发场景下数据竞争的两种思想乐观锁:乐观锁在操作数据时非常乐观认为别人不会同时修改数据因此在执行更新的时候只会判断一下在此期间别人是否修改了数据如果别人修改了数据则操作失败。悲观锁:悲观锁在操作数据时比较悲观认为别人会同时修改数据因此操作数据时直接把数据锁住直到操作完成后才释放锁上锁期间其他人不能修改数据但CAS仍然存在三大问题:ABA问题循环开销大只能保证一个共享变量的原子操作1.ABA问题什么是ABA问题:因为CAS需要在操作值的时候检查值是否发生了变化如果没有变化则更新但是如果一个值原来是A变成了B又变成了A那么在使用CAS进行检查的时候会发现它的值没有发生变化但是实际上变化了。ABA问题的解决思路使用版本号在变量前面追加上版本号每次变量更新的时候把版本号加1那么A-B-A就会变为1A-2B-3A。从Java1.5开始JDK的Atomic包里提供了一个类的方法compareAndSet来解决这个问题这个方法的作用是首先检查当前引用是否等于预期引用并且检查当前标志(相当于版本号)是否等于预期标志如果全部相等则以原子方式将该引用和标志的值设置为给定的更新值2.循环时间长开销大因为CAS是无锁的所以如果线程之间存在竞争的时候就只能通过自旋的方式等待。如果自旋CAS长时间不成功会给CPU带来非常大的执行开销在乐观锁中CAS失败意味着“冲突发生”但并不意味着“操作应该放弃”。相反线程通过重试来应对临时冲突直到操作成功。这类似于“如果第一次没成功再试一次”。3.只能保证一个共享变量的原子操作当对一个共享变量执行操作时我们可以使用循环CAS的方式来保证原子操作但是对多个共享变量操作时循环CAS就无法保证循环的原子性这个时候就可以使用锁循环 CAS 无法保证多个共享变量原子性的根本原因在于硬件层面的 CAS 指令只能针对单一内存地址即单个变量保证原子操作。当需要更新多个变量时无法用一条 CAS 指令同时更新它们只能拆分为多次独立的 CAS 执行从而破坏了原子性。二.原子操作类原子操作是计算机科学中的一个概念指的是一系列操作中的一个或多个操作在执行过程中不被其它操作中断或干扰要么全部完成要么全部不完成不会出现中间状态1.原子更新基本类型类使用原子的方式更新基本类型Atomic包提供了下述三个类AtomicBooleanAtomicIntegerAtomicLong以AtomicInteger为例讲解其常用方法public final int get(): 获取当前的值public final int getAndSet(int newValue): 获取当前的值并设置新的值public final int getAndIncrement():获取当前的值并自增1public final int getAndDecrement():获取当前的值并自减1public final int getAndAdd(int delta):获取当前的值并加上预期的值void lazySet(int newValue):最终会设置成newValue,使用lazySet设置值后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。AtomicInteger源码分析public class AtomicInteger extends Number implements java.io.Serializable { private static final long serialVersionUID 6214790243416807050L; //CAS操作需要内存地址即JVM需要知道变量在对象中的具体内存位置 //这内存地址是偏移量是相对于对象头的偏移字节数 //objectFieldOffset获取AtomicInteger类中名为value字段在内存中的偏移量 //返回值是一个long类型的偏移地址 private static final Unsafe U Unsafe.getUnsafe(); private static final long VALUE U.objectFieldOffset(AtomicInteger.class, value); private volatile int value;//这才是实际存储int值的地方 public final int getAndIncrement() { return U.getAndAddInt(this, VALUE, 1); } //o:AtomicInteger对象实例(this) //offset:value字段的内存偏移量(value) public final int getAndAddInt(Object o, long offset, int delta) { int v; do { v getIntVolatile(o, offset); } while (!weakCompareAndSetInt(o, offset, v, v delta)); return v; } public final boolean weakCompareAndSetInt(Object o, long offset, int expected, int x) { return compareAndSetInt(o, offset, expected, x); }2.原子更新数组通过原子的方式更新数组里的某个元素Atomic包提供了以下的3个类:AtomicIntegerArray: 原子更新整型数组里的元素。AtomicLongArray: 原子更新长整型数组里的元素AtomicReferenceArray:原子更新引用类型数组里的元素,这三个类的最常用的方法是如下两个方法:get(int index):获取索引为index的元素值,compareAndSet(int i,E expect,E update):如果当前值等于预期值则以原子方式将数组位置i的元素设置为update值。public class atomic { public static void main(String[] args) { AtomicIntegerArray arrnew AtomicIntegerArray(new int[]{1,2,3,4}); System.out.println(arr); System.out.println(arr.get(1)); System.out.println(arr.compareAndSet(1,2,5)); System.out.println(arr); } }3.原子更新引用类型Atomic包提供了以下三个类AtomicReference:原子更新引用类型AtomicReferenceFieldUpdater: 原子更新引用类型里的字段AtomicMarkableReferce: 原子更新带有标记位的引用类型这三个类提供的方法都差不多首先构造一个引用对象然后把引用对象set进Atomic类然后调用compareAndSet等一些方法去进行原子操作但AtomicReferenceFieldUpdater 略有不同更新的字段必须用volatile修饰。AtomicReference使用举例:public class AtomicReferenceTest { public static AtomicReferencePerson aref new AtomicReferencePerson(); public static void main(String[] args) { Person p1 new Person(原来的, 10); aref.set(p1); Person p2 new Person(改掉啦, 20); //比较p1本身变了吗没变则设置为p2 boolean b aref.compareAndSet(p1, p2); System.out.println(b); System.out.println(aref.get().getName()); System.out.println(aref.get().getOld()); } static class Person{ private String name; private int old; public Person(String name, int old) { this.name name; this.old old; } public String getName() { return name; } public void setName(String name) { this.name name; } public int getOld() { return old; } public void setOld(int old) { this.old old; } } }compareAndSet:调用去比较两个对象即比较两个对象的地址是否相等即内容被改变了但是地址没被改变是不知道的4.原子更新字段类如果需原子地更新某个类里的某个字段时就需要使用原子更新字段类Atomic包提供了以下3个类进行原子字段更新。AtomicIntegerFieldUpdater:原子更新整型的字段的更新器AtomicLongFieldUpdater:原子更新长整型字段的更新器AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起 来可用于原子的更新数据和数据的版本号可以解决使用CAS进行原子更新时可能出现的 ABA问题。(在AtomicReference基础上多加了一个版本号)要想原子的更新字段类需要两步因为原子更新字段类都是抽象类每次使用的时候必须使用静态方法newUpdater()创建按一个更新器并且需要设置想要更新的类和属性更新类的字段(属性)必须使用public volatile修饰符public class AtomicIntegerFieldUpdaterDemo { public static AtomicIntegerFieldUpdaterPerson aifuAtomicIntegerFieldUpdater.newUpdater(Person.class,old); public static void main(String[] args) { Person p1 new Person(原来的, 10); System.out.println(aifu.getAndIncrement(p1));//返回的是旧值 System.out.println(p1.getOld()); } static class Person{ private String name; public volatile int old; public Person(String name, int old) { this.name name; this.old old; } public String getName() { return name; } public void setName(String name) { this.name name; } public int getOld() { return old; } public void setOld(int old) { this.old old; } } }
返回列表