目录
课程内容
一、什么是原子操作?如何实现原子操作?
原子性和原子操作
什么是原子性,什么是原子操作,我想大家应该都知道吧。原子操作就是【一系列方法,要么全部执行,要么全部不执行】,这就是原子操作。最典型的就是数据库事务里面的操作。
在并发里,同样存在原子性,他跟上面说到的原子操作是一样的内涵和概念。假定有两个操作A和B都包含多个步骤,如果从执行A的线程来看,当另一个线程执行B时,要么将B全部执行完,要么完全不执行B,执行B的线程看A的操作也是一样的,那么A和B对彼此来说是原子的。
如何实现原子操作
实现原子操作可以使用锁机制,它用来满足基本的需求是没有问题的了。但是有的时候我们的需求并非这么简单,我们需要更有效,更加灵活的机制,synchronized关键字是基于阻塞的锁机制,也就是说当一个线程拥有锁的时候,访问同一资源的其它线程需要等待,直到该线程释放锁。
这里会有些问题:首先,如果被阻塞的线程优先级很高很重要怎么办?其次,如果获得锁的线程一直不释放锁怎么办?同时,还有可能出现一些例如死锁之类的情况,最后,其实锁机制是一种比较粗糙,粒度比较大的机制,相对于像计数器这样简单的需求显然有点儿过于笨重了。为了解决这个问题,Java提供了Atomic系列的原子操作类。
Atomic原子操作类简介
Java提供的Atomic系列的原子操作类,本质上是使用当前的处理器基本都支持CAS的指令来运行的,比如Intel的汇编指令cmpxchg,每个厂家所实现的具体算法并不一样,但是原理基本一样。
每一个CAS操作过程都包含三个运算符:一个内存地址V,一个期望的值A和一个新值B,操作的时候如果这个地址上存放的值等于这个期望的值A,则将地址上的值赋为新值B,否则不做任何操作,但是要返回原值是多少。
自然CAS操作执行完成时,在业务上不一定完成了,这个时候我们就会对CAS操作进行反复重试,于是就有了循环CAS。很明显,循环CAS就是在一个循环里不断的做CAS操作,直到成功为止。Java中的Atomic系列的原子操作类的实现就是利用循环CAS来实现的。如下图AtomicInteger类部分代码示例:
二、CAS 实现原子操作的三大问题
但是CAS实现原子操作会有3大问题。
Q1:ABA问题
什么是ABA问题?这个问题很经典。
- 问题介绍:因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。
- ABA问题的解决思路:就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A→B→A就会变成1A→2B→3A
Q2:循环时间长开销大
很显然,while循环的CAS就是典型的【自旋,占着茅坑不拉屎】,所以,如果长时间不干正事的话,就会极大浪费CPU资源。
Q3:只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。
有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java 1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。示例代码如下:
public class AtomicTest {
public static void main(String[] args) {
AtomicReference<MyAtomic> atomicAtomicReference = new AtomicReference<>(new MyAtomic());
Thread t1 = new Thread(() -> {
MyAtomic myAtomic = atomicAtomicReference.get();
MyAtomic myNewAtomic = new MyAtomic();
myNewAtomic.setA(myAtomic.a + 1);
myNewAtomic.setB(myAtomic.b + 1);
if (atomicAtomicReference.compareAndSet(myAtomic, myNewAtomic)) {
System.out.println("t1---更新成功了, 看看新值:atomicAtomicReference.get()=" + atomicAtomicReference.get().toString());
} else {
System.out.println("t1---更新失败了,为啥?:atomicAtomicReference.get()=" + atomicAtomicReference.get().toString());
}
});
Thread t2 = new Thread(() -> {
MyAtomic myAtomic = atomicAtomicReference.get();
MyAtomic myNewAtomic = new MyAtomic();
myNewAtomic.setA(myAtomic.a + 2);
myNewAtomic.setB(myAtomic.b + 2);
if (atomicAtomicReference.compareAndSet(myAtomic, myNewAtomic)) {
System.out.println("t2---更新成功了, 看看新值:atomicAtomicReference.get()=" + atomicAtomicReference.get().toString());
} else {
System.out.println("t2---更新失败了,为啥?:atomicAtomicReference.get()=" + atomicAtomicReference.get().toString());
}
});
t1.start();
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
}
public static class MyAtomic {
int a = 1;
int b = 2;
public int getA() {
return a;
}
public void setA(int a) {
this.a = a;
}
public int getB() {
return b;
}
public void setB(int b) {
this.b = b;
}
@Override
public String toString() {
return "MyAtomic{" +
"a=" + a +
", b=" + b +
'}';
}
}
}
三、jdk 中相关原子操作类的使用
Atomic分类
Atomic原子类,大概可以分为4种类型:
-
基本类型:使用原子的类型更新基本类型
- AtomicInteger整形原子类
- AtomicLong长整型原子类
- AtomicBoolean布尔原子类
-
数组类型:使用原子的方式更新数组中某个元素
- AtomicIntegerArray:整形数组原子类
- AtomicLongArray:长整形数组原子类
- AtomicReferenceArray:引用类型数组原子类(即对应数组中存放的元素为对象形式)
-
引用类型:使用原子的方式更新某个对象
- AtomicReference:引用类型原子类
- AtomicStampedReference:AtomicReference的扩展版,增加了一个参数stamp标记,这里是为了解决了AtomicInteger和AtomicLong的操作会出现ABA问题
- AtomicMarkableReference :与AtomicStampedReference差不多,只不过Stamped能记录修改次数,而Markable只是记录是否修改过
-
对象的属性修改类型:使用原子的方式更新某个对象中某个字段
- AtomicIntegerFieldUpdater:原子更新整形字段的更新器
- AtomicLongFieldUpdater:原子更新长整形字段的更新器
- AtomicReferenceFieldUpdater:原子更新引用类型字段的更新器
Atomic使用方法
1.基本类型原子类
由于三种类的方法基本一样,下面就以AtomicInteger为例:
public final int set() //设一个值
public final int get() //获取当前的值
public final int getAndSet(int newValue)//获取当前的值,并设置新的值
public final int getAndIncrement()//获取当前的值,并自增
public final int getAndDecrement() //获取当前的值,并自减
public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
boolean compareAndSet(int expect, int update) //如果当前值等于预期值,则以原子方式将该值设置为输入值(update)
public final void lazySet(int newValue) //最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(0);
// 演示set/get方法
System.out.println("演示AtomicInteger的get/set方法:" + atomicInteger.get());
atomicInteger.set(99);
System.out.println("演示AtomicInteger的get/set方法:" + atomicInteger.get());
// 系统输出如下:
// 演示AtomicInteger的get/set方法:0
// 演示AtomicInteger的get/set方法:99
// 演示getAndSet
int andSet = atomicInteger.getAndSet(100);
System.out.println("getAndSet之前:" + andSet + ",getAndSet之后:" + atomicInteger.get());
// 系统输出如下:
// getAndSet之前:99,getAndSet之后:100
// 演示getAndIncrement
int andIncrement = atomicInteger.getAndIncrement();
System.out.println("getAndIncrement之前:" + andIncrement + ",getAndIncrement之后:" + atomicInteger.get());
// 系统输出
// getAndIncrement之前:100,getAndIncrement之后:101
// 演示getAndDecrement
int andDecrement = atomicInteger.getAndDecrement();
System.out.println("getAndDecrement之前:" + andDecrement + ",getAndDecrement之后:" + atomicInteger.get());
// 系统输出
// getAndDecrement之前:101,getAndDecrement之后:100
// 演示getAndAdd
int andAdd = atomicInteger.getAndAdd(100);
System.out.println("getAndAdd之前:" + andAdd + ",getAndAdd之后:" + atomicInteger.get());
// 系统输出
// getAndAdd之前:100,getAndAdd之后:200
// 演示【正确】预期的compareAndSet
boolean b = atomicInteger.compareAndSet(200, 199);
if (b) {
System.out.println("预期值:200,修改为199,结果正确。 result=" + atomicInteger.get());
} else {
System.out.println("预期值:200,修改为199,结果错误。 result=" + atomicInteger.get());
}
// 系统输出
// 预期值:200,修改为199,结果正确。 result=199
// 演示【错误】预期的compareAndSet
boolean c = atomicInteger.compareAndSet(100, 199);
if (c) {
System.out.println("预期值:100,修改为199,结果正确。 result=" + atomicInteger.get());
} else {
System.out.println("预期值:100,修改为199,结果错误。 result=" + atomicInteger.get());
}
// 系统输出
// 预期值:100,修改为199,结果错误。 result=199
}
// 演示lazySet
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(199);
// 演示lazySet
for (int i = 0; i < 10; i++) {
final int finalI = i;
new Thread(()->{
int lazySetPre = atomicInteger.get();
atomicInteger.lazySet(finalI);
System.out.println("lazySet之前:" + lazySetPre + ",lazySet之后:" + atomicInteger.get());
}).start();
}
// 系统输出
// lazySet之前:199,lazySet之后:1
// lazySet之前:3,lazySet之后:4
// lazySet之前:2,lazySet之后:3
// lazySet之前:1,lazySet之后:2
// lazySet之前:0,lazySet之后:1
// lazySet之前:8,lazySet之后:9
// lazySet之前:6,lazySet之后:8
// lazySet之前:5,lazySet之后:6
// lazySet之前:4,lazySet之后:5
// lazySet之前:9,lazySet之后:7
}
2.数组类型原子类
由于三种类的方法基本一样,下面就以AtomicIntegerArray为例:
public final int get(int i) //获取 index=i 位置元素的值
public final int set(int i, int newValue) //为 index=i 位置元素设新值
public final int getAndSet(int i, int newValue) //返回 index=i 位置的当前的值,并将其设置为新值:newValue
public final int getAndIncrement(int i) //获取 index=i 位置元素的值,并让该位置的元素自增
public final int getAndDecrement(int i) //获取 index=i 位置元素的值,并让该位置的元素自减
public final int getAndAdd(int i, int delta) //获取 index=i 位置元素的值,并加上预期的值
boolean compareAndSet(int i, int expect, int update) //如果index=i 位置的值等于预期值,则以原子方式将 index=i 位置的元素值设置为输入值(update)
public final void lazySet(int i, int newValue) //最终 将index=i 位置的元素设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
public static void main(String[] args) {
int[] ints = {1, 2, 3};
AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(ints);
System.out.println("atomicIntegerArray的初始值:" + atomicIntegerArray.toString());
// 系统输出:
// atomicIntegerArray的初始值:[1, 2, 3]
// 演示set/get方法
for (int i = 0; i < ints.length; i++) {
int i1 = atomicIntegerArray.get(i);
atomicIntegerArray.set(i, i1 + 1);
System.out.println("set之前:" + i1 + ",set之后:" + atomicIntegerArray.get(i));
}
// 系统输出:
// set之前:1,set之后:2
// set之前:2,set之后:3
// set之前:3,set之后:4
// 演示getAndSet,把全部位置的值置为1
for (int i = 0; i < ints.length; i++) {
int i1 = atomicIntegerArray.getAndSet(i, 1);
System.out.println("getAndSet之前:" + i1 + ",getAndSet之后:" + atomicIntegerArray.get(i));
}
// 系统输出:
// getAndSet之前:2,getAndSet之后:1
// getAndSet之前:3,getAndSet之后:1
// getAndSet之前:4,getAndSet之后:1
// 演示getAndIncrement
for (int i = 0; i < ints.length; i++) {
int i1 = atomicIntegerArray.getAndIncrement(i);
System.out.println("getAndIncrement之前:" + i1 + ",getAndIncrement之后:" + atomicIntegerArray.get(i));
}
// 系统输出:
// getAndIncrement之前:1,getAndIncrement之后:2
// getAndIncrement之前:1,getAndIncrement之后:2
// getAndIncrement之前:1,getAndIncrement之后:2
// 其余操作不演示了,其实跟AtomicInteger差不多,只不过面向的是数组
}
3.引用原子类AtomicReference
public static class BankAccount {
String userName;
int money;
public BankAccount(String userName, int money) {
this.userName = userName;
this.money = money;
}
public int getMoney() {
return money;
}
public void setMoney(int money) {
this.money = money;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
@Override
public String toString() {
return "BankAccount{" +
"userName='" + userName + '\'' +
", money=" + money +
'}';
}
}
public static void main(String[] args) {
BankAccount bankAccount = new BankAccount("张深", 10);
AtomicReference<BankAccount> bankAccountAtomicReference = new AtomicReference<>();
// 演示set/get方法
System.out.println("演示AtomicReference的set/get方法之前:bankAccountAtomicReference=" + bankAccountAtomicReference.toString());
bankAccountAtomicReference.set(bankAccount);
System.out.println("演示AtomicReference的set/get方法之后:bankAccountAtomicReference=" + bankAccountAtomicReference.toString());
// 系统输出:
// 演示AtomicReference的set/get方法之前:bankAccountAtomicReference=null
// 演示AtomicReference的set/get方法之后:bankAccountAtomicReference=BankAccount{userName='张深', money=10}
// 演示compareAndSet
{
// 经过一系列操作,从数据库查找了一个叫做”周深“的账户
BankAccount failureAccount = new BankAccount("周深", 11);
BankAccount newBankAccount = new BankAccount(bankAccount.getUserName(), 100);
if (bankAccountAtomicReference.compareAndSet(failureAccount, newBankAccount)) {
System.out.println("【compareAndSet成功】预期账户:" + bankAccount + ",查询出来的银行账户:" + failureAccount);
} else {
System.out.println("【compareAndSet失败】预期账户:" + bankAccount + ",查询出来的银行账户:" + failureAccount);
}
}
// 系统输出:
// 【compareAndSet失败】预期账户:BankAccount{userName='张深', money=10},查询出来的银行账户:BankAccount{userName='周深', money=11}
}
// 演示AtomicStampedReference
public static void main(String[] args) throws InterruptedException {
AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(1, 0);
System.out.println("【一开始】getReference=" + atomicStampedReference.getReference() + ",getStamp=" + atomicStampedReference.getStamp());
// 系统输出:
// 【一开始】getReference=1,getStamp=0
atomicStampedReference.set(100, 2);
System.out.println("【set之后】,getReference=" + atomicStampedReference.getReference() + ",getStamp=" + atomicStampedReference.getStamp());
// 系统输出:
// 【set之后】,getReference=100,getStamp=2
Thread t1 = new Thread(() -> {
Integer reference = atomicStampedReference.getReference();
int stamp = atomicStampedReference.getStamp();
System.out.println("t1---在compareAndSet之前:getReference=" + reference + ",getStamp=" + stamp);
atomicStampedReference.compareAndSet(reference, reference + 1, stamp, stamp + 1);
System.out.println("t1---在compareAndSet之后:getReference=" + atomicStampedReference.getReference() + ",getStamp=" + atomicStampedReference.getStamp());
});
Thread t2 = new Thread(() -> {
Integer reference = atomicStampedReference.getReference();
int stamp = atomicStampedReference.getStamp();
System.out.println("t2---在compareAndSet之前:getReference=" + reference + ",getStamp=" + stamp);
atomicStampedReference.compareAndSet(reference, reference + 1, stamp, stamp + 1);
System.out.println("t2---在compareAndSet之后:getReference=" + atomicStampedReference.getReference() + ",getStamp=" + atomicStampedReference.getStamp());
});
t1.start();
t1.join();
t2.start();
t2.join();
System.out.println("【最终】getReference=" + atomicStampedReference.getReference() + ",getStamp=" + atomicStampedReference.getStamp());
// 系统输出:
// t1---在compareAndSet之前:getReference=100,getStamp=2
// t1---在compareAndSet之后:getReference=101,getStamp=3
// t2---在compareAndSet之前:getReference=101,getStamp=3
// t2---在compareAndSet之后:getReference=102,getStamp=4
// 【最终】getReference=102,getStamp=4
}
4.对象属性修改类型
以AtomicIntegerFieldUpdater 为例介绍一下简单使用方法:(注意:使用这种方式更新,更新值必须是volatile修饰)
public static class BankAccount {
String userName;
volatile int money;
public BankAccount(String userName, int money) {
this.userName = userName;
this.money = money;
}
public int getMoney() {
return money;
}
public void setMoney(int money) {
this.money = money;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
@Override
public String toString() {
return "BankAccount{" +
"userName='" + userName + '\'' +
", money=" + money +
'}';
}
}
public static void main(String[] args) {
BankAccount bankAccount = new BankAccount("张深", 10);
AtomicIntegerFieldUpdater<BankAccount> updater = AtomicIntegerFieldUpdater.newUpdater(BankAccount.class, "money");
int i = updater.addAndGet(bankAccount, 10);
System.out.println("AtomicIntegerFieldUpdater之前,updater=" + i + ",addAndGet之后,updater=" + updater.get(bankAccount));
// 系统输出:
// AtomicIntegerFieldUpdater之前,updater=20,addAndGet之后,updater=20
}
四、LongAddr(介绍)
JDK1.8 时,java.util.concurrent.atomic 包中提供了一个新的原子类:LongAdder。根据 Oracle 官方文档的介绍,LongAdder 在高并发的场景下会比它的前辈————AtomicLong 具有更好的性能,代价是消耗更多的内存空间。
AtomicLong是利用了底层的CAS操作来提供并发性的,调用了Unsafe类的getAndAddLong方法,该方法是个native方法,它的逻辑是采用自旋的方式不断更新目标值,直到更新成功。在并发量较低的环境下,线程冲突的概率比较小,自旋的次数不会很多。但是,高并发环境下,N个线程同时进行自旋操作,会出现大量失败并不断自旋的情况,此时AtomicLong的自旋会成为瓶颈。这就是LongAdder引入的初衷——解决高并发环境下AtomicLong的自旋瓶颈问题。
AtomicLong中有个内部变量value保存着实际的long值,所有的操作都是针对该变量进行。也就是说,高并发环境下,value变量其实是一个热点,也就是N个线程竞争一个热点。
LongAdder的基本思路就是分散热点,将value值分散到一个数组中,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个值进行CAS操作,这样热点就被分散了,冲突的概率就小很多。如果要获取真正的long值,只要将各个槽中的变量值累加返回。这种做法和ConcurrentHashMap中的“分段锁”其实就是类似的思路。
LongAdder提供的API和AtomicLong比较接近,两者都能以原子的方式对long型变量进行增减。但是AtomicLong提供的功能其实更丰富,尤其是addAndGet、decrementAndGet、compareAndSet这些方法。addAndGet、decrementAndGet除了单纯的做自增自减外,还可以立即获取增减后的值,而LongAdder则需要做同步控制才能精确获取增减后的值。如果业务需求需要精确的控制计数,做计数比较,AtomicLong也更合适。另外,从空间方面考虑,LongAdder其实是一种“空间换时间”的思想,从这一点来讲AtomicLong更适合。
总之,低并发、一般的业务场景下AtomicLong是足够了。如果并发量很多,存在大量写多读少的情况,那LongAdder可能更合适。适合的才是最好的,如果真出现了需要考虑到底用AtomicLong好还是LongAdder的业务场景,那么这样的讨论是没有意义的,因为这种情况下要么进行性能测试,以准确评估在当前业务场景下两者的性能,要么换个思路寻求其它解决方案。
对于LongAdder来说,内部有一个base变量,一个Cell[]数组。
- base变量:非竞态条件下,直接累加到该变量上。
- Cell[]数组:竞态条件下,累加个各个线程自己的槽Cell[i]中。
所以,最终结果的计算应该是:
在实际运用的时候,只有从未出现过并发冲突的时候,base基数才会使用到,一旦出现了并发冲突,之后所有的操作都只针对Cell[]数组中的单元Cell。
而LongAdder最终结果的求和,并没有使用全局锁,返回值不是绝对准确的,因为调用这个方法时还有其他线程可能正在进行计数累加,所以只能得到某个时刻的近似值,这也就是LongAdder并不能完全替代LongAtomic的原因之一。而且从测试情况来看,线程数越多,并发操作数越大,LongAdder的优势越大,线程数较小时,AtomicLong的性能还超过了LongAdder。
学习总结
- 学习了Java基于CAS实现的Atomic原子类
- 学习了CAS实现原子操作的三个问题
- 学习了Atomic类的基本用法
感谢
感谢大佬【作者:有时候我也会】《Atomic原子类常用方法总结(包含四大类型)》文章
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/180535.html