Java原子类的实现原理 && CAS的使用以及缺陷

在这里插入图片描述

🔍 开发者资源导航 🔍
🏷️ 博客主页个人主页
📚 专栏订阅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;
}
  1. 读取阶段
    CAS操作会原子性地读取内存中的当前值(称为expectedold_value),同时记录该内存位置的值(这个操作是原子的,不会被中断)。
  2. 比较阶段
    在准备写入新值前,会检查当前内存值&address内部的值,是否仍然等于之前读取的old_value
    • 如果内存值未变(等于old_value),说明内存的值在这个过程中并没有被修改,则进入写入阶段。
    • 如果内存值已变(不等于old_value),说明内存的值在这个过程中被修改过了,则立即返回false(表示失败)。
  3. 写入阶段(仅当比较成功时):
    原子性地将新值(new_value)写入内存,并返回true(表示成功)。
  4. 返回值含义
    • 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;
	 }
}
  1. 通过 CAS 看当前锁是否被某个线程持有.
  2. 如果这个锁已经被别的线程持有, 那么就⾃旋等待.
  3. 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程.

CAS缺陷

ABA问题

ABA问题是面试时的常见问题,我们知道CAS的判定机理是判断之前读取到的值和内存里面真实的值进行比较,而如果此时出现一个特殊的情况,他在将A变成B后,又将B变回了A,这时它的判断就会出现误判,而这种情况我们就称之为ABA问题。

虽然他在一般情况下不会对我们造成危害,但是他在特殊情景下会对我们造成危害。

例如:
你的名字叫做小明,今天来到银行取取钱,但是你今天手抖了在取钱的时候多按了一次,还好银行是使用CAS来进行增减(假设!),它在第二次扣款时发现内存值和oldvalue值是不一致的,所以仅扣除了一次。

你不由得感叹CAS的伟大。

第二天,你又来取钱了,你今天又手抖了一次,取了两次100元,原本CAS是可以避免这个情况的,但是今天你的朋友此时此刻“恰巧”给你转了100元,使你的金额又变回了原样,CAS在第二次判断的时候呢发现你的钱没有变化,又给你扣了一次,这时你发现你只取出来100但是实际上少了200。

你此时想对CAS说mmp了。

如何避免?

我们知道ABA问题是由于数值又改变回来导致的,那么我们可以设置一个只增/减的值来当做判断依据,例如设置一个版本号,每一次进行更新的时候+1。

此时你可能想到时间这个单位,他是只增的,但是我们并不建议使用这个,因为时间会出现"润秒"事件,感兴趣的可以自行了解。


感谢各位的观看Thanks♪(・ω・)ノ,如果觉得满意的话留个关注再走吧。

### JavaCAS 锁的示例代码与实现机制 #### 1. 自旋锁 (Spin Lock) 基于 CAS实现 以下是基于 `java.util.concurrent.atomic.AtomicReference` 的一个简单自旋锁实现: ```java import java.util.concurrent.atomic.AtomicReference; public class SpinLock { private final AtomicReference<Thread> owner = new AtomicReference<>(); public void lock() { Thread current = Thread.currentThread(); // 使用 CAS 尝试获取锁 while (!owner.compareAndSet(null, current)) { // 如果失败,则继续自旋等待 } } public void unlock() { Thread current = Thread.currentThread(); // 确保释放锁的是当前线程持有的锁 if (!owner.compareAndSet(current, null)) { throw new IllegalMonitorStateException("Calling thread has not held the lock"); } } } ``` 上述代码展示了如何利用 CAS实现一个简单的自旋锁。当线程试图获取锁时,会通过 `compareAndSet` 方法检查是否有其他线程已经持有了锁[^1]。 --- #### 2. CASJava 并发包中的应用实例 Java 提供了许多内置工具来简化开发者的工作,这些工具大多依赖于 CAS 操作。例如,`AtomicInteger` 是一种典型的基于 CAS 的无锁数据结构。 以下是一个使用 `AtomicInteger` 进行原子计数器的操作示例: ```java import java.util.concurrent.atomic.AtomicInteger; public class AtomicIntegerExample { private static final AtomicInteger counter = new AtomicInteger(0); public static int incrementAndGet() { return counter.incrementAndGet(); // 利用 CAS 实现线程安全的递增操作 } public static int get() { return counter.get(); // 获取当前值 } } ``` 在这个例子中,`incrementAndGet()` 方法内部实际上是通过循环调用底层的 `Unsafe` 完成 CAS 操作,直到成功为止[^3]。 --- #### 3. 解决 ABA 问题的方法 —— 使用版本号标记 虽然 CAS 能够有效解决竞态条件并提高并发性能,但它存在著名的 **ABA 问题**。即,在某些情况下,共享变量可能从状态 A 变成 B 后再回到 A,这会让 CAS 认为没有任何变化发生,但实际上中间发生了修改。 为了克服这一缺陷,可以引入带有版本号或时间戳的数据结构。例如,`AtomicStampedReference` 或者 `AtomicMarkableReference` 都提供了额外的信息用于区分相同值的不同状态。 下面是一个使用 `AtomicStampedReference` 的示例: ```java import java.util.concurrent.atomic.AtomicStampedReference; public class ABADemo { private static AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(100, 0); // 初始化值为 100,初始版本号为 0 public static void main(String[] args) throws InterruptedException { int[] stampHolder = {ref.getStamp()}; // 线程 T1 修改值两次 new Thread(() -> { try { System.out.println(Thread.currentThread().getName() + ": Trying to change value..."); boolean success = ref.compareAndSet( 100, 101, stampHolder[0], stampHolder[0] + 1); if (success) { stampHolder[0]++; success = ref.compareAndSet( 101, 100, stampHolder[0], stampHolder[0] + 1); if (success) { stampHolder[0]++; } } } catch (Exception e) { e.printStackTrace(); } }).start(); Thread.sleep(1000L); // 主线程稍作延迟 // 线程 T2 尝试验证是否存在 ABA 问题 Integer expectedValue = 100; int expectedStamp = ref.getStamp(); boolean result = ref.compareAndSet(expectedValue, 102, expectedStamp, expectedStamp + 1); System.out.println("Compare and Set Result: " + result); } } ``` 此程序演示了如何借助版本号防止误判的情况。即使目标对象恢复到原始值,只要其对应的版本号发生变化,就可以检测出潜在的风险[^4]。 --- #### 4. 总结 - CAS 是一种高效的非阻塞算法,广泛应用于多线程环境下的同步控制。 - 它能够显著减少因传统互斥锁带来的上下文切换成本,但在极端条件下也可能引发 CPU 占用率过高或者 ABA 问题等问题。 - 开发人员应根据实际需求权衡选用合适的解决方案,并注意规避可能出现的各种异常状况。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值