今天小编给大家分享一下Java CAS与Atomic原子操作核心原理是什么的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。
Mysql事务中的原子性就是一个事务中执行的多条sql,要么同时成功,要么同时失败,他们不可拆分。并发中的原子操作也一样,多个线程中,站在线程A的角度看线程B的操作,线程B的操作就是一个原子的;站在线程B的角度看线程A,线程A的操作是原子的。一整个操作要么全部执行完了,要么就没有执行,中间不能拆分。
那么要怎么实现原子性嘞?可以使用synchronized锁来保证一段代码的原子性,但是加锁影响性能,甚至还有死锁方面的问题需要考虑。
所以锁机制是比较重量级的,粒度较大的一种机制,比如对于计数器方面的操作来说,可能加锁的耗时都比整个计算的耗时还要高。Java 就提供了 Atomic 系列的原子操作类,在java.util.concurrent.atomic
包下
这些原子操作类是基于处理器的CAS指令来实现原子性的,Compare and swap。比较并且交换
每个CAS操作过程基本上都包含三个部分:内存地址V、期望值A、新值B
期望值就是旧值,首先会去内存地址中进行比较,我期望当前这个内存地址中的值是我期望的旧值,如果是则把新值赋值到这个内存地址中,如果不是则不做任何事。在一般的使用中我们会不断尝试去进行CAS操作,直到成功为止。
Java 中的 Atomic 系列的原子操作类的实现则是利用了循环 CAS 来实现。
使用CAS实现原子操作的几个问题
ABA问题
ABA问题在大多数场景下,不解决其实也没什么影响。
解决思路:添加版本戳,在变量前面追加上版本号,每次变量更新的时候把版本号加 1,那么 A-->B-->A
就会变成 1A-->2B-->3A
循环时间长,对于cpu来说开销较大
只能保证一个共享变量的原子操作
对于多个共享变量操作时就无法使用CAS来保证原子性了,这个时候还是需要用锁。
还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量 i=2,j=a,合并一下 ij=2a,然后用 CAS 来操作 ij。
从 Java 1.5开始,JDK 提供了AtomicReference
类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行 CAS 操作。
这些类的用户都大同小异,这里就拿几个典型来举例
// 以原子方式将给定值添加到当前值,然后将相加后的结果返回 public final int addAndGet(int delta){} // 指定期望值与修改后的值,如果期望值和当前值相同则进行更新操作 public final boolean compareAndSet(int expect, int update) {} // 先返回当前值,然后再进行原子自增1 public final int getAndIncrement() {} // 先返回当前值,然后进行原子更新操作 public final int getAndSet(int newValue) {}
案例:
public class UseAtomicInt { static AtomicInteger ai = new AtomicInteger(10); public static void main(String[] args) { ai.getAndIncrement(); ai.incrementAndGet(); //ai.compareAndSet(); ai.addAndGet(24); } }
提供原子的方式更新数据中的整形,常用方法如下:
// 以原子方式将给定值添加到索引 i 处的元素。然后返回更新后的值 public final int addAndGet(int i, int delta){} // 先比较,期望值和当前值相同再执行更新操作 public final boolean compareAndSet(int i, int expect, int update) {}
案例:
public class AtomicArray { static int[] value = new int[] { 1, 2 }; static AtomicIntegerArray ai = new AtomicIntegerArray(value); public static void main(String[] args) { ai.getAndSet(0, 3); System.out.println(ai.get(0)); //原数组不会变化 System.out.println(value[0]); } } Process finished with exit code 0
// 输出结果
3
1
需要注意的是,数组 value 通过构造方法传递进去,然后 AtomicIntegerArray会将当前数组复制一份,所以当 AtomicIntegerArray 对内部的数组元素进行修改 时,不会影响传入的数组。
如果要同时更新多个原子变量就需要使用更新引用类型提供的类了。Atomic提供了三个类:
AtomicReference
原子更新引用类型
案例:
public class UseAtomicReference { public static AtomicReference<UserInfo> atomicUserRef; public static void main(String[] args) { //要修改的实体的实例 UserInfo user = new UserInfo("Mark", 15); atomicUserRef = new AtomicReference(user); // 再创建一个对象 UserInfo updateUser = new UserInfo("Bill",17); // 期望值和当前值相同就进行修改 atomicUserRef.compareAndSet(user,updateUser); System.out.println(atomicUserRef.get()); System.out.println(user); /* 输出结果: UserInfo{name='Bill', age=17} UserInfo{name='Mark', age=15} */ } /** * 定义一个实体类 */ static class UserInfo { private volatile String name; private int age; public UserInfo(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public int getAge() { return age; } @Override public String toString() { return "UserInfo{" + "name='" + name + '\'' + ", age=" + age + '}'; } } }
AtomicStampedReference
利用版本戳的形式记录了每次改变以后的版本号,这样的话就不会存在 ABA问题了
AtomicMarkableReference
原子更新带有标记位的引用类型。可以原子更新一个布尔类型的标记位和引 用类型。
构造方法是 AtomicMarkableReference(V initialRef,booleaninitialMark)。
AtomicMarkableReference跟 AtomicStampedReference 差不多,
AtomicStampedReference 是使用 pair 的 int stamp 作为计数器使用
AtomicMarkableReference 的使用pair 的boolean mark。
AtomicStampedReference 可能关心的是动过几次,AtomicMarkableReference 关心的是有没有被人动过。
案例:
// 第二个线程,期望的时间戳和当前时间戳不同,所以更新不成功 public class UseAtomicStampedReference { static AtomicStampedReference<String> asr = new AtomicStampedReference("mark", 0); public static void main(String[] args) throws InterruptedException { //拿到当前的版本号(旧) final int oldStamp = asr.getStamp(); final String oldReference = asr.getReference(); System.out.println(oldReference + "============" + oldStamp); Thread rightStampThread = new Thread(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName() + ":当前变量值:" + oldReference + "-当前版本戳:" + oldStamp + "\n" + asr.compareAndSet(oldReference, oldReference + "+Java", oldStamp, oldStamp + 1)); } }); Thread errorStampThread = new Thread(new Runnable() { @Override public void run() { String reference = asr.getReference(); System.out.println(Thread.currentThread().getName() + ":当前变量值:" + reference + "-当前版本戳:" + asr.getStamp() + "\n" + asr.compareAndSet(reference, reference + "+C", oldStamp, oldStamp + 1)); } }); rightStampThread.start(); rightStampThread.join(); errorStampThread.start(); errorStampThread.join(); System.out.println(asr.getReference() + "============" + asr.getStamp()); } }
输出结果
mark============0
Thread-0:当前变量值:mark-当前版本戳:0
true
Thread-1:当前变量值:mark+Java-当前版本戳:1
false
mark+Java============1
如果需原子地更新某个类里的某个字段时,就需要使用原子更新字段类
Atomic 包提供了以下 3 个类进行原子字段更新。 要想原子地更新字段类需要两步。
因为原子更新字段类都是抽象类, 每次使用的时候必须使用静态方法 newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。
更新类的字段(属性)必须使用 public volatile修饰符。
AtomicIntegerFieldUpdater:原子更新整型的字段的更新器。
AtomicLongFieldUpdater:原子更新长整型字段的更新器。
AtomicReferenceFieldUpdater:原子更新引用类型里的字段。
并发量较少,自旋的冲突也就较少。但如果并发很多的情况下,CAS机制就不如synchronized了,因为很多个线程都集中判断一个变量的值,不断的自旋,对cpu的消耗也较大,同一时刻又只会一个线程更新成功。
在JDK1.8就引入了LongAdder
类,它在处理上面问题的时候是采用的一种热点数据的分散写
LongAdder中有两个成员变量
// 当为非空时,大小为 2 的幂。 // 如果并发很高就使用cell数组做写热点的分散,其中某些线程共同操作某一个数组中的元素 transient volatile Cell[] cells; // 当争抢较少时使用这个变量来进行cas,就类似于AtomicInteger类中的value变量 transient volatile long base;
然后调用sum()
方法将数组cells和base变量的中做一个汇总,返回当前总和。在没有并发更新的情况下调用将返回准确的结果,但在计算总和时发生的并发更新可能不会合并,所以sum()方法并不能保证强一致性,它返回的只是一个近似值
// 可以看到 sum()方法没有任何加锁的逻辑 public long sum() { Cell[] as = cells; Cell a; long sum = base; if (as != null) { for (int i = 0; i < as.length; ++i) { if ((a = as[i]) != null) sum += a.value; } } return sum; }
以上就是“Java CAS与Atomic原子操作核心原理是什么”这篇文章的所有内容,感谢各位的阅读!相信大家阅读完这篇文章都有很大的收获,小编每天都会为大家更新不同的知识,如果还想学习更多的知识,请关注亿速云行业资讯频道。
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。