JUC ---- 原子操作 CAS
程序员文章站
2022-05-03 18:36:42
...
1. CAS产生的背景
JDK其实在语言层级上就用Synchronized实现了线程安全,那为什么还要在1.5的版本后引入原子操作呢?
sync是基于阻塞的锁机制,它可能会带来以下问题:
- 被阻塞的线程优先级高。
- 拿到锁的线程一直不释放锁
- 大量的竞争,消耗CPU, 同时带来死锁或者其他安全问题。
2. CAS
2.1 简介
CAS (Compare And Swap), 是通过自旋实现的,它的底层是一个native方法(有C++ 语言编写,来控制JVM)。其最终的实现上是通过一条CPU指令实现的(大概意思上,就是若存在多核处理任务,则锁住总线)。这也从计算机的原语级别上保证了这个操作的原子性。
CAS操作中的三个运算符:
一个内存地址V, 一个期望值 A, 一个新值B
基本思路:如果地址V上的值和期望的值A相等,就给地址V赋上新值B。如果不是,就在循环(死循环,自旋)里不断进行CAS操作。
2.2 CAS遇到的问题
- ABA问题: A—> B —> A
原来的值是A, 但它进过更改后,先从A 变成B, 再从B变成了A。 那么CAS进行检查时发现它的值没有变化,但是实际上却变化了。(某些应用场景下是不允许这种变化的发生的)
ABA问题的解决思路使用版本号,在变量前面追加版本号,每次变量更新的时候版本号加1,那么A-B-A 就会变为 1A-2B-3A。Java中提供了原子类AtomicStampedReference来解决这个问题(它表示动过几次),而AtomicMarkableReference则是使用boolean表示有没有动过。 - 开销问题:CAS操作长期不成功,那么CPU会在这上面不断地自旋消耗大量资源。
- 只能保证一个共享变量的原子操作
2.3 JDK中相关的原子操作类
- 更新基本类型:AtomicBoolean, AtomicInteger, AtomicLong
- 更新数组类型:AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray
- 更新引用类型:AtomicReference, AtomicMarkableReference, AtomicStampedReference(解决ABA问题)
- 原子更新字段类:AtomicReferenceFieldUpdater, AtomicIntegerFieldUpdater, AtomicLongFieldUpdater (使用相对较少)
由于使用方式比较类似,这里以AtomicStampedReference为例子进行演示
3. 原子类的使用
这里以两个线程为例子,线程1使用正确的版本戳修改完引用后,线程2再使用老的版本号进行修改,这时会发现修改失败(因为版本号不对应)
package atomic;
import java.util.concurrent.atomic.AtomicStampedReference;
public class UseAtomicStampedReference {
//新建一个带版本号和引用类型的原子变量
static AtomicStampedReference<String> asr = new AtomicStampedReference<>("gs",0);
public static void main(String[] args) throws Exception {
final int oldStamp = asr.getStamp(); //初始化的版本号
final String oldReference = asr.getReference();
System.out.println(oldReference+"=========="+oldStamp);
//使用正确版本号的更改的线程
Thread rightStamp = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()
+"当前变量值:"+ oldReference + ",当前版本号为:"+oldStamp
+"-"+asr.compareAndSet(oldReference,oldReference+" Love you",
oldStamp,oldStamp+1));
}
});
//使用老版本号修改的线程
Thread errorStamp = new Thread(new Runnable() {
//此时不能再使用oldStamp,因为版本号发生了变化
@Override
public void run() {
System.out.println(Thread.currentThread().getName()
+"当前变量值:"+ asr.getReference() + ",当前版本号为:"+asr.getStamp()
+"-"+asr.compareAndSet(asr.getReference(),asr.getReference()+" Love you",
oldStamp,oldStamp+1));
}
});
rightStamp.start();
//这里表示rightStamp的线程先运行完再运行errorStamp的线程
rightStamp.join();
errorStamp.start();
errorStamp.join();
System.out.println(asr.getReference()+"-----"+asr.getStamp());
}
}
控制台的输出
上一篇: 移动端 的属性 touch
下一篇: 5、CAS原子操作