JAVA实现乐观锁-CAS算法

博客模拟商品促销预购场景,对比了Java三种实现方式。正常Java实现会因并发导致数据一致性错误;加锁实现虽能保证数据一致,但效率低且易阻塞;使用CAS实现可避免Java层面加锁,虽有竞争,但能保证结果正确,避免数据不一致问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

场景模拟

假设商品有500件库存,进行促销预购,每有一位客户预购,商品预购数加1。

省略数据库的操作,用i++来模拟数据库操作

正常JAVA实现

public class CASTest {
	public static int numValue;//商品预购数

	public static void main(String[] args) throws InterruptedException {
		CASTest test = new CASTest();
		for (int i = 0; i < 500; i++) {
			new Thread(() -> { // 起500个线程,当成500个客户同时都在预购该商品
				try {
					Thread.sleep(30);// 模拟操作延时
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				test.addNum();
			}).start();
		}
		Thread.sleep(3000);// 等上面线程都跑完后再看 商品预购数 的值
		System.out.println("numValue:" + test.numValue);
	}

	//模拟是预购数加1操作
	public void addNum() {
		numValue++;//模拟数据库操作
		System.out.println(numValue);
	}

}

预期结果:最后输出的 numValue 值应该是500
实际结果:

在这里插入图片描述
每次有客户预购后numValue的输出是无序的,结果可能不是500

原因:numValue++ 时,会从内存中读取数值numValue,然后numValue+1,最后再赋值回numValue。 当并发量大时,多个用户同时读取到相同的numValue值,各自+1,再赋值回numValue,导致numValue原本应该加2或者加3最后却只加了1. 显然这样的写法会导致数据一致性错误

JAVA加锁实现

写法与上面相同,只是addNum() 方法加个锁 变成了
	//模拟是预购数加1操作
	public synchronized void addNum() {
		numValue++;//模拟数据库操作
		System.out.println(numValue);
	}

预期结果:最后输出的 numValue 值应该是500
实际结果:
在这里插入图片描述
每次有客户预购后numValue的输出是有序的,结果是500

显然加锁可以避免并发操作时出现的数据不一致问题,但是每次只能一个客户去进行预购操作,效率低下并且如果中间有客户操作时出现阻塞后面的客户也会受影响,即使用lock锁,实际情况下也不建议使用

JAVA使用CAS实现

CAS算法:CAS是CPU的指令,是属于硬件层次的,底层也用了锁,相当于是硬件层次上的处理并发修改变量的操作算法,有三个操作数,内存地址V ,预期值B(就是你获取的numValue的修改前的值),要替换得到的目标值A(修改后的numValue值)。

CAS指令执行时,比较内存地址V(内存地址的numValue值)与预期值B是否相等,若相等则将A赋给B,否则不赋值,这时候我们可以通过自旋去重新去获取期望值,重新再去进行CAS操作。
我们只需要知道JAVA怎么去调用和传参,具体的更新操作交给CAS算法
具体写法可以参考AtomicInteger.class

import java.lang.reflect.Field;
import sun.misc.Unsafe;

public class CASTest2 {

	private volatile int numValue;//无阻塞修改的参数

	private static Unsafe unsafe;
	private static final long valueOffset;//相当于CAS的获取内存地址

	static {
		try {
			//初始化 unsafe
			Field f;
			f = Unsafe.class.getDeclaredField("theUnsafe");
			f.setAccessible(true);
			unsafe = (Unsafe) f.get(null);
			//初始化 valueOffset
			valueOffset = unsafe.objectFieldOffset(CASTest2.class.getDeclaredField("numValue"));
		} catch (Exception ex) {
			throw new Error(ex);
		}
	}

	public static void main(String[] args) throws Exception {
		CASTest2 test = new CASTest2();
		for (int i = 0; i < 500; i++) {
			new Thread(() -> { // 起500个线程,当成500个用户都在修改同一个数据
				try {
					Thread.sleep(30);// 模拟操作延时
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				test.addNum();
				System.out.println(test.numValue);

			}).start();

		}
		Thread.sleep(3000);// 等上面线程都跑完后再看 numValue 的值
		System.out.println("numValue:" + test.numValue);
	}

	public final int get() {
		return numValue;
	}

	public final int addNum() {
		for (;;) {
			//如果竞争失败,重新获取期望值,重新更新,直到成功。实际情况就是用select 去查库重新获取新的期望值
			int current = get();//期望值
			int next = current + 1;//目标值
			if (compareAndSet(current, next)) {
				//下面可以加数据库更新操作
				return next;
			} else {//当多个客户同时修改 numValue时,只有一个人可以操作成功,失败的人就会进入到下面的分支,重新再去更新操作
				System.out.println("当current=" + current + " 时有竞争并竞争失败,自旋一次重新自增");
			}
		}
	}

	public final boolean compareAndSet(int expect, int update) {
		//unsafe会调用java公共组件,再调C++代码再调底层CAS算法,我们不用管,值的修改也交给下面的方法
		return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
	}
}

预期结果:最后输出的 numValue 值应该是500
实际结果:
在这里插入图片描述
每次有客户预购后numValue的输出是无序的,结果是500,并且也会发现会后竞争失败,重新去更新的操作。虽然也会出现竞争的情况,却可以通过CAS算法,避免了java层面上的加锁

总结:

上述被并发修改的参数是int类型的,实际上使用别的类型的参数也是可以操作的。但是这个使用cas算法实现的并发修改参数只能保证一个变量,如果同时修改多个参数的情况下还需要进行代码优化,并且如果并发量过大,会导致自旋的次数大大增加,压力在服务器这边,要根据实际情况去判断是否要采用这种操作。
### 乐观锁与悲观锁概念 乐观锁和悲观锁代表了处理并发控制的不同哲学。乐观锁认为冲突很少发生,在操作时不锁定资源,仅在提交更新时检查是否有冲突;而悲观锁则假定冲突频繁发生,因此在整个事务期间持续持有锁。 对于乐观锁而言,其主要通过版本号机制或比较并交换(Compare-And-Swap, CAS)来实现[^1]。当多个线程尝试访问同一资源而不希望造成阻塞的情况下,乐观锁能够提供更高的性能表现,尤其适合于读多写少的应用环境[^3]。 相比之下,悲观锁是一种更为保守的方法,它会在整个交易过程中保持对共享资源的独占权,防止任何其他进程在同一时间内对其进行更改。这种方式虽然能有效避免竞态条件的发生,但却可能导致较低的数据吞吐率以及较高的延迟时间,特别是在高并发环境下[^4]。 ### CAS算法及其工作原理 CAS 是一种无锁同步原语,用于构建高效的并发程序组件。该算法允许一个线程基于某个预期值来进行一次性的原子性测试及设置操作。具体来说: 如果当前内存位置的实际值等于期望旧值,则将其替换为目标新值; 如果不相等,则不做改变,并返回实际值给调用者知晓失败原因。 这种方法可以在不依赖传统互斥锁的前提下完成安全的操作序列化,从而减少了上下文切换带来的开销[^2]。 ```java public class AtomicInteger { private volatile int value; public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); } } ``` 上述代码展示了 `AtomicInteger` 类如何利用底层 Unsafe API 来执行 CAS 操作。此方法接受两个参数:一个是期待看到的老数值 (`expect`) 和另一个是要设定的新数值(`update`). 如果老数值得到了确认匹配成功的话就会立即把新的数值赋上去; 否则就维持现状不变.
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值