🔍 开发者资源导航 🔍 |
---|
🏷️ 博客主页: 个人主页 |
📚 专栏订阅: JavaEE全栈专栏 |
CAS
CAS(Compare-And-Swap)是一种原子操作,广泛用于并发编程中实现无锁(lock-free)数据结构,它的核心功能是比较并交换。
boolean CAS(address, old_value, new_value) {
if (&address == old_value) {
&address = new_value;
return true;
}
return false;
}
- 读取阶段:
CAS操作会原子性地读取内存中的当前值(称为expected
或old_value
),同时记录该内存位置的值(这个操作是原子的,不会被中断)。 - 比较阶段:
在准备写入新值前,会检查当前内存值&address内部的值,是否仍然等于之前读取的old_value
。- 如果内存值未变(等于
old_value
),说明内存的值在这个过程中并没有被修改,则进入写入阶段。 - 如果内存值已变(不等于
old_value
),说明内存的值在这个过程中被修改过了,则立即返回false
(表示失败)。
- 如果内存值未变(等于
- 写入阶段(仅当比较成功时):
原子性地将新值(new_value
)写入内存,并返回true
(表示成功)。 - 返回值含义:
true
:内存值未被其他线程修改,且新值写入成功。false
:内存值已被其他线程修改,操作失败(此时寄存器中的old_value
已过时)。
原子类的实现
既然这个操作是原子性的,那么他对于我们有什么用呢?它最大的作用就是来实现原子类。
上述我们提到过,他可以进行比较和交换操作,同时我们可以用它来判定他是否有被修改过,那我们可以使用这个方式来进行多线程下的增加数值操作:
while ( CAS(value, oldValue, oldValue+1) != true) {
oldValue = value;
}
在高并发状态下,如果这个值被多个线程同时修改就会产生线程安全问题,最明显的现象就是这个值和我们理想的结果并不一样。
而如果我们使用CAS来进行这个操作,他会根据你的当前内存真实值和之前识别的旧值oldValue是否一致,来判断你的真实值有没有被修改过,而如果是被修改过后的,那么我们的oldvalue也需要进行相应更新,因为我们要交换的值是根据oldvalue来进行判断的。
这样的操作可以保证每次增加的值都是“有效”的,而不会造成线程安全问题。
原子类的使用
原子类的增加机理就是通过上述方式实现的,使用原子类的目的是避免加锁,在Java中提供了一些原子类的封装。
它们被封装在java.util.concurrent.atomic下(非完整版):
原子类 | 描述 |
---|---|
AtomicBoolean | 一个可自动更新的 boolean 值。 |
AtomicInteger | 一个可自动更新的 int 值。 |
AtomicIntegerArray | 元素可原子更新的 int 数组。 |
AtomicIntegerFieldUpdater<T> | 基于反射的工具类,用于原子更新指定类的 volatile int 字段。 |
AtomicLong | 一个可自动更新的 long 值。 |
AtomicLongArray | 元素可原子更新的 long 数组。 |
AtomicLongFieldUpdater<T> | 基于反射的工具类,用于原子更新指定类的 volatile long 字段。 |
他们的使用方法很简单,例如AtomicInteger增减方法
方法名 | 返回值 | 说明 | 等价操作 |
---|---|---|---|
getAndIncrement() | int | 原子递增 1,返回旧值 | i++ |
getAndDecrement() | int | 原子递减 1,返回旧值 | i-- |
getAndAdd(int delta) | int | 原子增加 delta ,返回旧值 | - |
incrementAndGet() | int | 原子递增 1,返回新值 | ++i |
decrementAndGet() | int | 原子递减 1,返回新值 | --i |
addAndGet(int delta) | int | 原子增加 delta ,返回新值 | - |
基于cas实现自旋锁
基于 CAS 实现更灵活的锁, 获取到更多的控制权.。
伪代码:
public class SpinLock {
private Thread owner = null;
public void lock(){
while(!CAS(this.owner, null, Thread.currentThread())){
}
}
public void unlock (){
this.owner = null;
}
}
- 通过 CAS 看当前锁是否被某个线程持有.
- 如果这个锁已经被别的线程持有, 那么就⾃旋等待.
- 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程.
CAS缺陷
ABA问题
ABA问题是面试时的常见问题,我们知道CAS的判定机理是判断之前读取到的值和内存里面真实的值进行比较,而如果此时出现一个特殊的情况,他在将A变成B后,又将B变回了A,这时它的判断就会出现误判,而这种情况我们就称之为ABA问题。
虽然他在一般情况下不会对我们造成危害,但是他在特殊情景下会对我们造成危害。
例如:
你的名字叫做小明,今天来到银行取取钱,但是你今天手抖了在取钱的时候多按了一次,还好银行是使用CAS来进行增减(假设!),它在第二次扣款时发现内存值和oldvalue值是不一致的,所以仅扣除了一次。
你不由得感叹CAS的伟大。
第二天,你又来取钱了,你今天又手抖了一次,取了两次100元,原本CAS是可以避免这个情况的,但是今天你的朋友此时此刻“恰巧”给你转了100元,使你的金额又变回了原样,CAS在第二次判断的时候呢发现你的钱没有变化,又给你扣了一次,这时你发现你只取出来100但是实际上少了200。
你此时想对CAS说mmp了。
如何避免?
我们知道ABA问题是由于数值又改变回来导致的,那么我们可以设置一个只增/减的值来当做判断依据,例如设置一个版本号,每一次进行更新的时候+1。
此时你可能想到时间这个单位,他是只增的,但是我们并不建议使用这个,因为时间会出现"润秒"事件,感兴趣的可以自行了解。
感谢各位的观看Thanks♪(・ω・)ノ,如果觉得满意的话留个关注再走吧。