文章目录
- 为什么会出现`CAS`思想?
- `CAS`概念
- `CAS`自旋
- 概念
- `CAS`的简单使用
- `CAS`源码解析
- UnSafe类
- `CAS`底层原理
- `CAS`的硬件保证
- `CAS`自旋锁的实现
- 前置知识----原子引用`AtomicReference`
- 实现自旋锁
- CAS缺点
- `ABA`问题
- 什么是`ABA`问题
- 如何解决`ABA`问题
- 简单案例
- `AtomicStampedReference`的源码分析
为什么会出现CAS
思想?
在没有CAS
思想之前,虽然我们可以使用synchronized
关键字实现锁,但是synchronized
属于悲观锁,降低了程序运行的效率.
synchronized
关键字在Java
中用于实现悲观锁,也称为互斥锁,它确保在同一时刻只有一个线程可以访问特定的代码段或资源。
悲观锁的基本思想:在访问资源之前先尝试获取锁,如果锁被其他线程持有,则当前线程会阻塞等待直到锁释放。
悲观锁的效率并不高,特别是在并发访问频繁并且锁竞争激烈的情况下,因为每次都需要尝试获取锁,这可能会导致频繁的上下文切换和线程阻塞,增加了系统的开销和延迟。此外,如果多个线程同时请求锁,它们可能会顺序地逐个获取,而不是并行处理,这限制了并发性能。
所以在这种情况下,我们希望可以得到一种既可以保证原子性,又不会影响效率的方法.
通俗来说,就是在不加锁的情况下,依旧可以实现原子性.
CAS
概念
CAS
全称:compare and swap
,中文翻译成比较并交换,实现并发算法时常用到的一种技术CAS
包含三个操作数:内存位置,预期原值和更新值- 在执行CAS操作的时候,将内存位置的值与预期值进行比较:
- 如果相匹配,那么处理器会自动将该位置值更新为新值;
- 如果不匹配,处理器不做任何操作,多个线程同时执行
CAS
操作只有一个会成功.
CAS
自旋
概念
我们先来看一下正常情况下的数据进行修改:
我们来分析一下正常情况下具体的流程:
- 线程
1
,线程2
,线程3
均会对 内存中的5
进行修改,所以此时线程1
2
3
均会将内存中的5
拷贝到自己的工作内存中进行计算(进行自增操作) - 此时线程
1
会进行内存位置的值与预期原值的比较,线程1
预期原值为5
,此时内存位置的值也是5
,相匹配 - 线程
1
进行自增后为6
,处理器会自动将内存位置的值更新为6
- 共享数据内存中的数据同步成功
那么在什么情况下会发生CAS
自旋呢?
也就是说,此时在线程1
中进行自增操作之后,线程1
进行了预期原值和内存地址数据的比较,发现不相等,说明在此之前线程2
和3
进行了自增并且同步到了内存中.在这种情况下,我们就需要执行CAS
自旋了
CAS
自旋:线程1
将重新读取内存地址的数据,直到预期原值和内存地址数据相同,内存地址的数据才能够修改.
总结如图:
其中:
E
:内存中当前的值N
:在线程对数据进行操作后的数据V
:要更新的数据值
CAS
的简单使用
这里我们使用原子类中的AutomicInteger
来举例;
在AutomicInteger
中,存在一个方法:
public final boolean compareAndSet(int expectedValue, int newValue) {
return U.compareAndSetInt(this, VALUE, expectedValue, newValue);
}
参数:
expectedValue
:表示预期原值newValue
:表示将要更新的值
public class CasTest {
public static void main(String[] args) {
AtomicInteger integer=new AtomicInteger(5);
integer.compareAndSet(5,10);
System.out.println(integer.get());
}
}
运行结果:
表示修改数据成功
CAS
源码解析
这里我们通过AutomicInteger
中的自增方法进入源码中观察其中的CAS
是如何实现的:
public class CasTest {
public static void main(String[] args) {
AtomicInteger integer=new AtomicInteger(5);
integer.getAndIncrement();
System.out.println(integer);
}
}
这里我们找到了CAS
实现:
@IntrinsicCandidate
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
//取出当前内存地址中的数据
v = getIntVolatile(o, offset);
} while (!weakCompareAndSetInt(o, offset, v, v + delta));
return v;
}
这里使用到了
do{}while()
结构,所以在数据没有自加成功的情况下,会一直进行自旋操作
这里的getIntVolatile
是一个Native
方法,作用是取出当前内存地址中的数据
@IntrinsicCandidate
public native int getIntVolatile(Object o, long offset);
这里的weakCompareAndSetInt
中调用了compareAndSetInt
方法,作用是
@IntrinsicCandidate
public final boolean weakCompareAndSetInt(Object o, long offset,
int expected,
int x) {
return compareAndSetInt(o, offset, expected, x);
}
这里的compareAndSetInt
是Native
方法,作用是使用CAS
自旋的方式,安全实现对数据的修改操作
@IntrinsicCandidate
public final native boolean compareAndSetInt(Object o, long offset,
int expected,
int x);
UnSafe类
是CAS
的核心类,由于Java
方法无法直接访问底层系统,需要通过本地方法(Native
)来访问,Unsafe
相当于一个后门,基于该类可以直接操作特定内存的数据.Unsafe
类在sum.misc
包中,其内部方法操作可以像C
的指针一样直接操作内存,因为Java
中CAS
操作的执行依赖于Unsafe
类的方法
我们来看一看在AtomicInteger.java
源码中的 Unsafe
public class AtomicInteger extends Number implements java.io.Serializable {
//.......
private volatile int value;//这里使用volatile来修饰,保证对所有线程均可见
public AtomicInteger(int initialValue) {
value = initialValue;
}
public final int get() {
return value;
}
private static final Unsafe U = Unsafe.getUnsafe();//AtomicInteger 核心的Unsafe类初始化
private static final long VALUE//初始化Value:是对象在对象实例中的偏移量,获取到主内存中的数据
= U.objectFieldOffset(AtomicInteger.class, "value");
//compareAndSet:主内存中的数据==expectedValue,将主内存中的数据修改为newValue,不相同,则不修改
public final boolean compareAndSet(int expectedValue, int newValue) {
//this:对象实例
//VALUE:主内存中数据的偏移量
//expectedValue:预期原值
//newValue:修改的新值
return U.compareAndSetInt(this, VALUE, expectedValue, newValue);
}
}
Unsafe
public final class Unsafe {
//通过对指针进行偏移,不仅可以直接修改指针指向的数据(即使它们是私有的),
//甚至可以找到JVM已经认定为垃圾、可以进行回收的对象
//objectFieldOffset获取非静态属性Field在对象实例中的偏移量
private native long objectFieldOffset1(Class<?> c, String name);
public long objectFieldOffset(Class<?> c, String name) {
if (c == null || name == null) {
throw new NullPointerException();
}
//返回对象在对象实例中的偏移量
return objectFieldOffset1(c, name);
}
@IntrinsicCandidate
public final native boolean compareAndSetInt(Object o, long offset,
int expected,
int x);
}
CAS
底层原理
CAS
的硬件保证
CAS
是JDK
提供的非阻塞原子性操作,它通过硬件保证了比较-更新的原子性.
CAS
是一条CPU
的原子指令(cmpxchg
指令),不会造成所谓的数据不一致的问题,Unsafe
提供的CAS
方法底层实现即为CPU
指令cmpxchg
.
执行cmpxchg
指令的时候,会判断当前系统是否为多核系统,如果是就给总线加锁,只有一个线程会对总线加锁成功,加锁成功之后就会执行CAS
操作,也就是说CAS
原子性实际上是CPU
实现独占的,比起用synchronized
重量级锁,这里的排他时间要短很多,所以多线程情况下性能会比较好.
CAS
自旋锁的实现
前置知识----原子引用AtomicReference
使用AtomicReference
类,我们可以自己设计一个具备原子性的类,获得各种原子性的方法
实现自旋锁
使用CAS
原理,我们自行实现自旋锁
要求:
- 线程
A
进入后,先拿到锁(Lock
),释放锁 - 线程
B
进入之后,一直等待线程A
释放锁,线程A
释放锁之后,线程B
拿到锁
public class SpinLock {
AtomicReference<Thread> atomicReference=new AtomicReference<>();
//拿到锁
public void Lock() throws InterruptedException {
Thread curThread=Thread.currentThread();
while(!atomicReference.compareAndSet(null,curThread)){//如果当前值不是null,就等待获取锁
System.out.println(curThread.getName()+" : 正在等待锁");
TimeUnit.SECONDS.sleep(1);
}
//得到锁
System.out.println(curThread.getName()+"获取到锁");
TimeUnit.SECONDS.sleep(5);
}
//释放锁
public void unLock(){
Thread curThread = Thread.currentThread();
atomicReference.compareAndSet(curThread,null);
System.out.println(curThread.getName()+"释放掉锁");
}
public static void main(String[] args) throws InterruptedException {
SpinLock spinLock=new SpinLock();
Thread t1=new Thread(()->{
try {
spinLock.Lock();
} catch (InterruptedException e) {
e.printStackTrace();
}
spinLock.unLock();
},"A");
t1.start();
TimeUnit.SECONDS.sleep(2);
Thread t2=new Thread(()->{
try {
spinLock.Lock();
} catch (InterruptedException e) {
e.printStackTrace();
}
spinLock.unLock();
},"B");
t2.start();
}
}
运行结果:
运行结果表示:在A
得到锁之后,B
需要一直等待锁被A
释放,才能得到锁.
这里的
AtomicReference<Thread> atomicReference=new AtomicReference<>();
相当于一个锁,其中原子类Thread
相当于当前哪个线程持有该锁
atomicReference.compareAndSet(null,curThread)
相当于判断当前的锁是否被持有.
CAS缺点
- 循环时间开销很大:
getAndAddInt
方法执行时,有一个do while
,如果CAS
失败,就会一直循环,进行尝试.如果CAS
长时间不成功,就会给CPU
带来很大的开销 ABA
问题:下节详解
ABA
问题
什么是ABA
问题
如何解决ABA
问题
在JUC.Atomic
的包中,存在一个AtomicStampedReference
的类,这个类就是版本号时间戳原子引用类,这个类记录了主内存中每一次数据的改变,即改变一次,版本号自增1
,这样便可以在值相同的情况下,也能判断出值是否发生过更新.
AtomicStampedReference
中存在的一些常用API
:
简单案例
public static void main(String[] args) {
Book book1=new Book(1,"Java");
Book book2=new Book(2,"MySQL");
AtomicStampedReference<Book> atomicStampedReference=new AtomicStampedReference<>(book1,1);
//AtomicStampedReference<Book> atomicStampedReference2=new AtomicStampedReference<>(book2,2);
if(atomicStampedReference.compareAndSet(book1,book2,1,2));
System.out.println(book2);
System.out.println(atomicStampedReference.getReference());
System.out.println(atomicStampedReference.getStamp());
}
运行结果:
我们重点学习其中的public boolean compareAndSet(V expectedReference,V newReference,int expectedStamp,int newStamp)
方法
参数:
V expectedReference
:预期的原子引用V newReference
:替换的新的原子引用int expectedStamp
:预期的版本戳int newStamp
:新的版本戳
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;
//CAS方法:
//expectedReference:期待引用
//newReference:修改的新引用
//expectedStamp:期待的版本号
//newStamp::修改的新版本号
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)));