这篇文章,说说 CAS
,还有Java java.util.concurrent.atomic
包下的 Atomic 原子类。
1、什么是CAS
CAS
:Compare and Swap,即比较再交换。cpu的特殊指令
,指令级别保证这是一个原子操作。CAS
需要有3个操作数:内存值V,旧的预期值A,即将要更新的目标值B。 CAS指令执行时,当且仅当内存值V的值与预期值A相等时,将内存值V的值修改为B,否则就什么都不做。整个比较并替换的操作是一个原子操作。
通常在自旋(死循环)中不断地进行 CAS
操作。
2、模仿 CAS
/**
* Description: 模仿 CAS
*
* @author Xander
* datetime: 2020-11-13 7:02
*/
public class MockCas {
/**
* 内存值
*/
private volatile int value;
public MockCas(int value) {
this.value = value;
}
/**
* 模仿 CAS
*
* @param expectedValue 预期值
* @param newValue 要更新的新值
* @return
*/
public synchronized int compareAndSwap(int expectedValue, int newValue) {
int oldValue = value;
if (oldValue == expectedValue) {
value = newValue;
System.out.println(Thread.currentThread().getName() + " cas 操作成功");
} else {
System.out.println(Thread.currentThread().getName() + " cas 操作失败");
}
return oldValue;
}
public static void main(String[] args) throws InterruptedException {
// 内存值是 100
int value = 100;
int expectedValue = 100;
MockCas cas = new MockCas(value);
Thread thread0 = new Thread(() -> {
int oldValue = cas.compareAndSwap(expectedValue, 101);
System.out.println(Thread.currentThread().getName() + " cas 内存值:" + oldValue);
});
Thread thread1 = new Thread(() -> {
int oldValue = cas.compareAndSwap(expectedValue, 101);
System.out.println(Thread.currentThread().getName() + " cas 内存值:" + oldValue);
});
thread0.start();
thread1.start();
thread0.join();
thread1.join();
}
}
运行结果:
Thread-0 cas 操作成功
Thread-0 cas 内存值:100
Thread-1 cas 操作失败
Thread-1 cas 内存值:101
3、Java中原子类
原子类的作用和锁类似,是为了保证并发情况下线程安全。
不过原子类相比于锁,有一定的优势:
1)粒度更细:原子变量可以把竞争范围缩小到变量级别,这是我们可以获得的最细粒度的情况了,通常锁的粒度都要大于原子变量的粒度
2)效率更高:通常,使用原子类的效率会比使用锁的效率更高。
Jdk java.util.concurrent.atomic
包中提供了 6 类原子类:
- Atomic* 基本类型原子类:
AtomicBoolean,AtomicInteger,AtomicLong
- Atomic*Array 数组类型原子类:
AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
- Atomic*Reference 引用类型原子类:
AtomicReference,AtomicMarkableReference,AtomicStampedReference
- Atomic*FieldUpdater 升级原子类:
AtomicReferenceFieldUpdater,AtomicIntegerFieldUpdater,AtomicLongFieldUpdater
- Adder累加器:
LongAdder、DoubleAdder
- Accumulator累加器:
LongAccumulator、DoubleAccumulator
4、Java 是如何利用 CAS 实现原子操作的
原子操作,是不可分割,不可中断的,即便是多线程的情况下也可以保证。
Unsafe类
Unsafe
是CAS的核心类。Java无法直接访问低层操作系统,而是通过本地方法(native
)来进行访问。不过尽管如此,JVM还是开了一个后门,JDK中有一个类Unsafe
,它提供了硬件级别的原子操作。
valueOffset表示的是变量值在
内存中的偏移地址
,因为Unsafe就是根据内存偏移地址获取数据的原值的,就这样我们就能通过Unsafe
来实现CAS了。
以 AtomicInteger
为例,分析在Java中是如何利用 CAS
实现原子操作的?AtomicInteger
加载 Unsafe
工具,用来直接操作内存数据,用 Unsafe
来实现低层 CAS
操作, 用 volatile 修饰 value 字段,保证可见性。
AtomicInteger getAndAdd()方法 源码
// AtomicInteger 部分源码
public class AtomicInteger extends Number implements java.io.Serializable {
...
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
// value 的内存偏移地址
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
// value值
private volatile int value;
/**
* Atomically adds the given value to the current value.
*
* @param delta the value to add
* @return the previous value
*/
public final int getAndAdd(int delta) {
// unsafe 中 do-while 循环,自旋,直至 cas 操作成功
return unsafe.getAndAddInt(this, valueOffset, delta);
}
...
}
// Unsafe部分源码 getAndAddInt
public final class Unsafe {
...
public final int getAndAddInt(Object var1, long var2, int var4) {
// do-while 循环,自旋,不断地调用 compareAndSwapInt(cas操作) 直到成功
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
// 低层的 cas 操作
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
...
}
5、基础类型原子类,演示AtomicInteger的基本用法
对比非原子类的线程安全问题,使用了原子类之后,不需要加锁,也可以保证线程安全。
AtomicInteger
常用API:
- public final int get():获取当前的值
- public final int getAndSet(int newValue):获取当前的值,并设置新的值
- public final int getAndIncrement():获取当前的值,并自增1
- public final int getAndDecrement():获取当前值,并自减1
- public final int getAndAdd(int delta):获取当前的值,并加上预期的值
- public final boolean compareAndSet(int expect, int update):如果输入的等于预期值,则以原子方式将该值设置为输入值,设置成功返回true,否则 expect 不等于 内存值,返回false
/**
* Description: 演示AtomicInteger的基本用法,对比非原子类的线程安全问题,使用了原子类之后,不需要加锁,也可以保证线程安全。
* @author Xander
* datetime: 2020-11-13 12:53
*/
public class AtomicIntegerDemo implements Runnable {
/**
* 普通变量,对变量的操作非线程安全
*/
private static volatile int var = 0;
/**
* 原子类变量,线程安全操作
*/
private static final AtomicInteger atomicInteger = new AtomicInteger();
/**
* 对普通变量进行+1
*/
public void incrementBasic() {
var++;
}
/**
* 对原子变量进行+1
*/
public void incrementAtomic() {
atomicInteger.getAndIncrement();
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
incrementAtomic();
incrementBasic();
}
}
public static void main(String[] args) throws InterruptedException {
AtomicIntegerDemo r = new AtomicIntegerDemo();
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("原子类的结果:" + atomicInteger.get());
System.out.println("普通变量的结果:" + var);
}
}
运行结果:
原子类的结果:20000
普通变量的结果:19264
2个线程,分别进行10000次 +1 操作,可以看出原子变量的结果是符合预期的,而普通变量的结果则是不正确的。
6、数组类型原子类,演示 AtomicIntegerArray 的基本用法
- AtomicIntegerArray(int length):根据给定的 length 初始化数组,新建长度为 length 的成员变量
array
,array = new int[length]
; - AtomicIntegerArray(int[] array):根据给定的数组 clone() 初始化成员变量的
array
,this.array = array.clone();
- int addAndGet(int i, int delta):对数组中下标为
i
的元素添加delta
并返回更新后的值 - boolean compareAndSet(int i, int expect, int update):对数组中下标为
i
的元素传入expect
值和update
值,进行 cas 操作,如果expect
值等于内存值,则修改成功,返回true,否则返回false - int decrementAndGet(int i):对数组中下标为
i
的元素-1
,然后返回修改后的值 - int get(int i):返回数组中下标为
i
的元素的值 - int getAndAdd(int i, int delta):对数组中下标为
i
的元素添加delta
并返回更新前的值 - int getAndDecrement(int i):对数组中下标为
i
的元素-1
,然后返回修改前的值 - int getAndIncrement(int i):对数组中下标为
i
的元素+1
,然后返回修改前的值 - int getAndSet(int i, int newValue):对数组中下标为
i
的元素自旋设值newValue
,然后返回修改前的值 - int incrementAndGet(int i):对数组中下标为
i
的元素+1
,然后返回修改后的值 - int length():返回数组的长度
两个线程并发对长度为 5 的普通数组变量和原子类 AtomicIntegerArray
的每个元素分别进行10000次+1,原子变量的结果是符合预期的,而普通数组变量的结果则是不正确的。
/**
* 演示原子数组的使用方法
*/
public class AtomicArrayDemo implements Runnable {
static int length = 5;
/**
* 普通数组变量,对变量的操作非线程安全
*/
private static volatile int[] array = new int[length];
/**
* 原子类变量,线程安全操作
*/
private static final AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(length);
/**
* 对普通变量进行+1
*/
public void incrementBasic(int index) {
array[index]++;
}
/**
* 对原子变量进行+1
*/
public void incrementAtomic(int index) {
atomicIntegerArray.getAndIncrement(index);
}
@Override
public void run() {
for (int index = 0; index < length; index++) {
for (int count = 0; count < 10000; count++) {
incrementAtomic(index);
incrementBasic(index);
}
}
}
public static void main(String[] args) throws InterruptedException {
AtomicArrayDemo r = new AtomicArrayDemo();
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("原子类的结果:" + atomicIntegerArray);
System.out.println("普通变量的结果:");
for (int item : array) {
System.out.print(item + " ");
}
}
}
运行结果:
原子类的结果:[20000, 20000, 20000, 20000, 20000]
普通变量的结果:
19345 18226 17153 17765 19513
7、引用类型原子类,演示 AtomicReference 的基本用法
- V get():获取 AtomicReference 的 value ,是一个引用类型。
- set(V newValue):设置 AtomicReference 的 新值
- V getAndSet(V newValue):设置 AtomicReference 的 新值,并返回旧的值
/**
* 演示引用类型的原子操作类
*/
public class AtomicReferenceDemo {
static AtomicReference<UserInfo> userRef = new AtomicReference<UserInfo>();
public static void main(String[] args) {
UserInfo user = new UserInfo("Mark", 15);//要修改的实体的实例
userRef.set(user);
UserInfo updateUser = new UserInfo("Bill", 17);//要变化的新实例
userRef.compareAndSet(user, updateUser);
System.out.println(userRef.get().getName());
System.out.println(userRef.get().getAge());
System.out.println(user.getName());
System.out.println(user.getAge());
}
//定义一个实体类
static class UserInfo {
private 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;
}
}
}
运行结果:
Bill
17
Mark
15
8、带版本戳引用类型原子类,演示 AtomicStampedReference 的基本用法
/**
* 演示带版本戳的原子操作类
*/
public class AtomicStampedReferenceDemo {
static AtomicStampedReference<String> asr =
new AtomicStampedReference<>("Xander", 0);
public static void main(String[] args) throws InterruptedException {
final int oldStamp = asr.getStamp();//拿初始的版本号
final String oldReferenc = asr.getReference();//拿初始的引用
System.out.println(oldReferenc + "===========" + oldStamp);
Thread rightStampThread = new Thread(new Runnable() {
@Override
public void run() {
String reference = asr.getReference();
System.out.println(Thread.currentThread().getName()
+ " 修改前变量值:" + reference + " 修改前版本戳:" + asr.getStamp() + " 修改成功:"
+ asr.compareAndSet(oldReferenc, oldReferenc + "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() + " 修改成功:"
+ asr.compareAndSet(reference, reference + "C", oldStamp, oldStamp + 1));
}
});
rightStampThread.start();
rightStampThread.join();
errorStampThread.start();
errorStampThread.join();
System.out.println(asr.getReference() + "===========" + asr.getStamp());
}
}
运行结果:
Xander===========0
Thread-0 修改前变量值:Xander 修改前版本戳:0 修改成功:true
Thread-1 修改前变量值:XanderJava 修改前版本戳:1 修改成功:false
XanderJava===========1
9、CAS的问题
ABA 问题
CAS
的理念是,在我修改值期间,没有其他人进行修改,我最后再修改值。
什么是 ABA 问题?
①线程1和线程2,同时需要对内存值V进行操作,同时读出当前内存值V=A
,
②线程1 准备修改为 A1
,于是它准备执行 compareAndSwap(int expectedValue, int newValue)
,其中 expectedValue=A,newValue=A1;
③在线程1准备进行修改之前,线程2也对内存值V 进行修改,并且进行了两次修改,先从 A 修改为 B (compareAndSwap(A,B)
),再由 B 修改为 A (compareAndSwap(B,A)
);
④线程1最后 compareAndSwap(A, A1)
,这时候,线程1也能够执行成功,但是会存在很多潜在的问题,因为在线程1修改期间,内存值V已经被多次修改。
怎么避免 ABA 问题?可以通过版本号解决,例如原子类AtomicStampedReference
。
开销问题
通常原子操作是在自旋(死循环)中不断地进行 CAS
操作直至成功,例如 Unsafe
类中的 getAndAddInt()
。
如果我们设计类似 Unsafe
类中的自旋算法时,需要注意如果 CAS
操作长期不成功,cpu不断的循环,会存在开销问题。
// Unsafe部分源码 getAndAddInt
public final class Unsafe {
...
public final int getAndAddInt(Object var1, long var2, int var4) {
// do-while 循环,自旋,不断地调用 compareAndSwapInt(cas操作) 直到成功
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
// 低层的 cas 操作
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
...
}
这篇文章给大家浅谈了什么是 CAS
,给大家介绍了 JDK 中常用类型的 Atomic* 原子类的使用,我这里也是简单的介绍,相信大家读完这篇文章,理解 CAS
算法后,再去读其他原子类的源码,也是很简单的,最后说明了 CAS
算法和自旋,可能会存在的 ABA
问题和开销问题。
共同学习,写下你的评论
评论加载中...
作者其他优质文章