JUC原子类: CAS, Unsafe和原子类详解
CAS
CAS的全称是Compare-And-Swap,中文含义就是对比并交换。CAS并不是java中的方法,他实际上是CPU的原子指令,==其作用是让CPU先比较两个值是否相等,然后根据判断条件原子性的更新某个位置的值。==CAS是基于硬件平台(Intel、Linux)的汇编指令,也就是说CAS是靠硬件来实现的。而在JVM或者JDK源码中,只是封装了汇编调用方法。比如AtomicInteger类便是使用了这些借口来完成多线程的安全性操作。
CAS:输入两个值,一个新值一个旧值,在CAS执行操作的时候,先比较旧值是否发生了变换,如果没有改变,则将新值赋给旧值。如果有变化,则不交换。
在保证多线程安全的时候,很多时候大家更愿意使用Synchorinized关键字,但是这种重量级锁属于排他锁,一个时刻只能有一个线程获取锁,效率较低。
CAS是CPU级别或者硬件级别的原子性操作,所以多线程使用CAS来更新数据的时候,可以不使用锁。
CAS使用实例
不使用CAS,使用Synchonized关键字或者lock。
public class Test {
private int i=0;
public synchronized int add(){
return i++;
}
}
但使用CAS
public class Test {
private AtomicInteger i = new AtomicInteger(0);
public int add(){
return i.addAndGet(1);
}
}
这里不要着急,我们声明整型变量i不再使用int,而是使用AtomicInteger
原子类,这个原子类底层是使用CAS来进行数据更新的。我们在使用的时候,对于程序员来说是不需要显示加锁的,但在多线程的环境下也能保证数据安全性。
CAS存在的问题及如何解决
问题1:ABA
因为CAS在操作的时候,需要先检查旧值是否发生了变换,如果没有变换才进行新值的更新,但是假如一个值从A变成B,又变成A,那么对于CAS来说是检测不到他的变换的,但是CAS却进行了新值的替换。
JDK1.5之后的解决方案
不在单单比较变量值的变化,而是在变量之前添加版本号进行标记,CAS再检查的时候,需要确保版本号和旧值都保持不变,则可以进行新值的交换。
落实到源代码上,JDK中的Atomic包里面的一个类可以解决ABA问题AtomicStampedReference
。我们具体来看一下源代码:java.util.concurrent.atomic.AtomicStampedReference
public class AtomicStampedReference<V> {
private static class Pair<T> {
final T reference;
final int stamp;
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
static <T> Pair<T> of(T reference, int stamp) {
return new Pair<T>(reference, stamp);
}
}
private volatile Pair<V> pair;
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
private boolean casPair(Pair<V> cmp, Pair<V> val) {
return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}
主要涉及的代码如上所示。
解释:
final T reference;
final int stamp;
reference:对象引用
stamp:标志版本
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
static <T> Pair<T> of(T reference, int stamp) {
return new Pair<T>(reference, stamp);
}
一个构造方法,一个实例化方法。
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
// 获取当前的(元素值,版本号)对
Pair<V> current = pair;
return
// 引用没变
expectedReference == current.reference &&
// 版本号没变
expectedStamp == current.stamp &&
// 新引用等于旧引用
((newReference == current.reference &&
// 新版本号等于旧版本号
newStamp == current.stamp) ||
// 构造新的Pair对象并CAS更新
casPair(current, Pair.of(newReference, newStamp)));
}
- 如果当前对象引用和版本标志和旧值相比没有变化,并且新值和当前值一致,则直接返回为true。
- 如果当前对象引用和版本标志和旧值相比没有变换,但是新值和当前值并不一致,这是需要实例化一个新的pair对象,在调用CAS来实现。
- 这块有一个比较有趣的东西,逻辑运算符。
- A&&B 如果A为true,则B执行,否则B不执行
- A||B 如果A为false,则B执行,否则B不执行
所以在使用这个方法的时候,用户只需要传入新值和新变量的引用。是两个变量,并不是直接传入pair。
实战:
package com.wangye.Test;
import java.util.concurrent.atomic.AtomicStampedReference;
public class ABAsolved {
private static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(1,0);
public static void main(String[] args) {
// System.out.println(atomicStampedReference.getReference());
Thread main = new Thread(()->{
System.out.println("操作线程"+Thread.currentThread().getName()+",初始值 a="+atomicStampedReference.getReference());
int stamp = atomicStampedReference.getStamp();
try{
Thread.sleep(1000);
}
catch (Exception e){
e.printStackTrace();
}
boolean isCASSuccess = atomicStampedReference.compareAndSet(1,2,stamp,stamp+1);
System.out.println("操作线程"+Thread.currentThread().getName()+",CAS操作结果"+isCASSuccess);
},"主线程");
Thread other = new Thread(()->{
Thread.yield();
atomicStampedReference.compareAndSet(1,2,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
System.out.println("操作线程"+Thread.currentThread().getName()+"增加"+atomicStampedReference.getReference());
atomicStampedReference.compareAndSet(2,1,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
System.out.println("操作线程"+Thread.currentThread().getName()+"减少"+atomicStampedReference.getReference());
},"其他线程");
main.start();
other.start();
}
}
- 当第二个线程将stamp更改后,主线程将无法使用CAS,因为版本标志不对应了。
问题2:循环时间长
自旋问题,不是很理解。
问题3:只能保证一个共享变量的原子操作
CAS只能保证一个共享变量的原子性操作,进而保证线程安全问题。当多个共享变量的时候,CAS失效,这个时候可以用锁。或者是将所有变量合并成一个总变量,然后CAS操作这个总变量,但是比较繁琐。
从JDK1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里进行CAS操作。
JAVA中的原子类
java中的原子类都在java.util.concurrent.atomic
包下。并且这些原子类基本都是由Unsafe实现的。此外,Unsafe在JUC的CAS操作中也有着广泛的应用,Unsafe中也定义了CAS的底层源码。
所以在总结原子类之前,先总结Unsafe类。
Unsafe类的功能图。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eCRyThYh-1599832058257)(https://www.pdai.tech/_images/thread/java-thread-x-atomicinteger-unsafe.png)]
Unsafe与CAS
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
public final long getAndAddLong(Object var1, long var2, long var4) {
long var6;
do {
var6 = this.getLongVolatile(var1, var2);
} while(!this.compareAndSwapLong(var1, var2, var6, var6 + var4));
return var6;
}
public final int getAndSetInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var4));
return var5;
}
public final long getAndSetLong(Object var1, long var2, long var4) {
long var6;
do {
var6 = this.getLongVolatile(var1, var2);
} while(!this.compareAndSwapLong(var1, var2, var6, var4));
return var6;
}
public final Object getAndSetObject(Object var1, long var2, Object var4) {
Object var5;
do {
var5 = this.getObjectVolatile(var1, var2);
} while(!this.compareAndSwapObject(var1, var2, var5, var4));
return var5;
}
可以看到,Unsafe中的这些方法在更新设置值的时候,都是通过不断的自旋完成的。其实自旋也就是while循环中不断执行CAS操作,知道CAS操作成功,返回True。
我们来看一下Unsafe中的原子方法
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
这些方法都是native修饰,表示为本地方法库中的方法,是C语言编写的函数。这些方法都有一个共同的前缀compareAndSwap
。我们来看看unsafe.cpp中是怎么定义的:
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
UnsafeWrapper("Unsafe_CompareAndSwapInt");
oop p = JNIHandles::resolve(obj);
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END
我们可以看到,当Unsafe实现CAS的时候,调用了Atomic的cmpxchg方法,注意这里的Atomic并不是jdk中的Atomic,而是native的本地方法库中的c代码。cmpxchg的实现与平台有关,但是基本原理不变。
Linux
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
int mp = os::is_MP();
__asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
: "=a" (exchange_value)
: "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
: "cc", "memory");
return exchange_value;
}
windows
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
int mp = os::isMP(); //判断是否是多处理器
_asm {
mov edx, dest
mov ecx, exchange_value
mov eax, compare_value
LOCK_IF_MP(mp)
cmpxchg dword ptr [edx], ecx
}
}
// Adding a lock prefix to an instruction on MP machine
// VC++ doesn't like the lock prefix to be on a single line
// so we can't insert a label after the lock prefix.
// By emitting a lock prefix, we can define a label after it.
#define LOCK_IF_MP(mp) __asm cmp mp, 0 \
__asm je L0 \
__asm _emit 0xF0 \
__asm L0:
我们可以看到,无论哪个平台,首先都是要判断是否是多处理器模型,因为只有多处理器模式的情况下,才是用内存屏障完成加锁,单处理器不需要。如果是多处理器,则将加锁。也就是为cmpxchg指令添加lock前缀。这个lock前缀有人说是总线锁,也有人说是缓存锁。这里就不区分了,总之加了锁之后,只有一个线程将被执行,其他线程阻塞,从而保证线程安全性。
Unsafe还有一些其他工能,比如修改变量的内存地址等,这些功能过于底层,不做介绍。
使用原子方式更新基本类型
- AtomicInteger
- AtomicLong
- AtomicBoolean
AtomicInteger
常用的API:
public final int get():获取当前的值
public final int getAndSet(int newValue):获取当前的值,并设置新的值
public final int getAndIncrement():获取当前的值,并自增
public final int getAndDecrement():获取当前的值,并自减
public final int getAndAdd(int delta):获取当前的值,并加上预期的值
void lazySet(int newValue): 最终会设置成newValue,使用lazySet设置值后,可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
其声明的变量可自动实现线程安全,而不需要加锁。
AtomicInteger的底层实现:
public class AtomicInteger extends Number implements java.io.Serializable {
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); }
}
private volatile int value;
//返回当前值
public final int get() {
return value;
}
//递增加detla
public final int getAndAdd(int delta) {
//三个参数,1、当前的实例 2、value实例变量的偏移量 3、当前value要加上的数(value+delta)。
return unsafe.getAndAddInt(this, valueOffset, delta);
}
//递增加1
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
...
}
好,我们可以看到,value字段在此处被valotile修饰,表示一旦这个变量发生改变,则立即对所有的线程可见。
除此之外,可以看到,AtomicInteger中的一些方法,内部都是调用了Unsafe中相应的方法,也就是使用CAS的方式更新value。从而保证操作的原子性。
所以java.util.concurrent.atomic
中的原子类,都是使用
- valotile关键字保证变量的可见性
- CAS保证操作的原子性
来实现的。
还有其他的原子类,这边不做介绍了,基本原理都差不多。
从面试的角度出发
-
线程安全的实现方法?
- 互斥同步:
- synchronized
- ReentrantLock
- 非阻塞同步
- CAS
- Atomic*原子类 (实际上还是CAS)
- 无同步方案
- 栈封闭
- 方法体内的局部变量是线程私有的,不会出现多线程安全问题
- ThreadLocal
- 栈封闭
- 互斥同步:
-
什么是CAS?
- CAS全称可以理解为Compare and Swap,中文可以理解为比较并交换。CAS并不是java中的方法,他的实现底层是c语言。所以CAS描述的是CPU原子指令,CAS通过比较两个值是否相等,根据判断条件来完成原子性的数值更新操作。平时我们所说得CAS机制,指的是Unsafe类下的CompareAndSwap*方法,其底层实现跟具体的硬件平台有关,比如windows和Linux的实现略有不同,但是其基本原理都是通过添加锁的方式,保证多线程多处理器的安全性问题。所以说,CAS并不是JVM或者JDK中定义的方法,JVM或者JDK中只是保存了CAS的汇编调用。
-
CAS使用示例,结合AtomicInteger给出示例?
- AtomicInteger的数据更新操作方法内调用的是Unsafe类的方法,而在Unsafe方法中调用了相应的CAS方法。
- 示例的话比较简单,int声明的变量(不包括方法体内的局部变量)不具备多线程安全性,除非加锁;但是AtomicInteger声明的变量由于其内部的CAS机制保持原子性,所以其符合线程安全性,不需要加锁。
-
CAS会有哪些问题?
-
ABA问题:
- 初始变量值为A,当前线程改为B,另外一个线程改为A,CAS认为变量没有改变,所以可以执行CAS机制,但是变量已经发生了改变。
- 如何解决:CAS默认使用的是变量的Value或者对象引用来做判断,在此基础上在添加一个参数,版本标志。JDK1.5之后,可以使用
AtomicStampedReference
.
-
循环时间太长:
-
public final int getAndUpdate(IntUnaryOperator updateFunction) { int prev, next; do { prev = get(); next = updateFunction.applyAsInt(prev); } while (!compareAndSet(prev, next)); return prev; }
-
以上代码是
java.util.concurrent.atomic.AtomicInteger
下面的一个方法,可以看到,CAS机制在while循环中一直执行,直到成功为止,这可能会出现自旋时间太长的问题。
-
-
只能保证一个共享变量的原子操作
- 当多个变量都需要进行CAS保证其原子性的相关操作时,CAS无能为力。
- 在JDK1.5之后,Java提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里面,从而对这个对象使用CAS机制。
-
-
AtomicInteger底层实现?
- CAS机制保证数值相关操作的原子性,CAS操作由Unsafe类提供。
- volatile关键字保证当前数值一旦被修改,则立即对其他线程可见。
-
请阐述你对Unsafe类的理解?
- Unsafe是位于sun.misc包下的类,主要提供一些低级别、不安全的操作方法,比如直接访问系统内存资源、更改变量的内存地址等,这些方法可以提高java运行的效率,增强java语言的底层操作能力。具体可以看上面的脑图。
-
AtomicStampedReference是怎么解决ABA的?
- 在原有的把对象引用的基础上,增加另外一个标志——变量的版本标志,在做CAS操作时,必须两个值同时保持不变,才可以进行操作。
- 构造包含两个变量reference和stamp的Pair对象,reference维护对象引用,stamp维护变量的版本标志信息
-
java中还有哪些类可以解决ABA的问题?
ger底层实现?**
-
CAS机制保证数值相关操作的原子性,CAS操作由Unsafe类提供。
-
volatile关键字保证当前数值一旦被修改,则立即对其他线程可见。
-
请阐述你对Unsafe类的理解?
- Unsafe是位于sun.misc包下的类,主要提供一些低级别、不安全的操作方法,比如直接访问系统内存资源、更改变量的内存地址等,这些方法可以提高java运行的效率,增强java语言的底层操作能力。具体可以看上面的脑图。
-
AtomicStampedReference是怎么解决ABA的?
- 在原有的把对象引用的基础上,增加另外一个标志——变量的版本标志,在做CAS操作时,必须两个值同时保持不变,才可以进行操作。
- 构造包含两个变量reference和stamp的Pair对象,reference维护对象引用,stamp维护变量的版本标志信息
-
java中还有哪些类可以解决ABA的问题?
- AtomicMarkableReference