CAS概述
CAS是乐观锁的典型实现,也是并发编程中核心的基层依赖,许多并发的机制依靠CAS。
CAS是硬件层面提供的功能,而非软件层面的编码,即cpu拿来就有这个功能。后面会解析为什么需要在硬件层面实现。
什么是乐观锁
锁,即防止共同数据在多线程访问场景下导致的问题。而产生的一种保护机制。
乐观锁是相对于悲观锁而言的。
我们java中使用的synchronized就是悲观锁,所谓悲观,就是这种加锁机制认为,每次获取公共资源的时候都有竞争的情况,都会产生数据的同步问题,那么我就直接把资源占了,不让其他线程进来处理,是一种预先性的排他处理。
这里的synchronized底层是字节码处理中jvm在首尾加上了monitor机制,在进入的时候获取锁,退出代码块的时候释放锁,具体实现细节之后再讲。
要更好地讲解为什么会出现乐观和悲观两种锁,先理解一下读锁和写锁。
读锁和写锁是独立于悲观和乐观的另一个层面的特性。本质是一个数据要读也要写,那么读和写的操作也要保持一个同步,不能在读的时候,写操作已经把它给改了,那么就有歧义了。
读锁:同一时间允许多个线程获取,获取之后,只能读不能写。一个数据上了读锁以后,其他事务依然可以继续加读锁,但是不能加写锁
写锁:同一时间,只能有一个事务获取写锁,在这个锁被释放前,不能获取该数据的读锁或写锁
这两个锁是一对,共同为一个数据服务。准确地说,这两个锁是同一个锁对象的不同模式。
为什么会设计这样一对锁呢?因为以前我们一股脑地使用排他锁(悲观锁)。那么在读的时候也加锁,写的时候也加锁,那读一次数据还得排队,那不等得恼火吗。所以在逻辑上修改了一下,放宽只读的并发访问。在只做读操作的时候,就尽情让大量线程访问就好了,提高了访问性能。
public class ReadWriteLockExample {
public static void main(String[] args) throws InterruptedException {
BankAccount account = new BankAccount("123456789", 1000.0);
// 创建线程池
ExecutorService executor = Executors.newFixedThreadPool(10);
System.out.println("初始余额: " + account.getBalance());
// 启动多个查询线程(读操作)
for (int i = 1; i <= 5; i++) {
final int threadId = i;
executor.submit(() -> {
// 模拟多个用户同时查询余额
double balance = account.getBalance();
System.out.println("查询线程" + threadId + " - 余额: " + balance);
});
}
// 启动存款线程(写操作)
executor.submit(() -> {
System.out.println("存款线程 - 准备存入200元");
account.deposit(200.0);
System.out.println("存款线程 - 存款完成,新余额: " + account.getBalance());
});
// 启动更多查询线程(读操作)
for (int i = 6; i <= 10; i++) {
final int threadId = i;
executor.submit(() -> {
double balance = account.getBalance();
System.out.println("查询线程" + threadId + " - 余额: " + balance);
});
}
// 启动取款线程(写操作)
executor.submit(() -> {
System.out.println("取款线程 - 准备取出150元");
account.withdraw(150.0);
System.out.println("取款线程 - 取款完成,新余额: " + account.getBalance());
});
// 关闭线程池
executor.shutdown();
executor.awaitTermination(5, TimeUnit.SECONDS);
System.out.println("\n最终余额: " + account.getBalance());
}
}
/**
* 银行账户类,使用读写锁保护共享数据
*/
class BankAccount {
private final String accountNumber;
private double balance;
// 创建读写锁
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private final ReadLock readLock = rwLock.readLock(); // 读锁
private final WriteLock writeLock = rwLock.writeLock(); // 写锁
public BankAccount(String accountNumber, double initialBalance) {
this.accountNumber = accountNumber;
this.balance = initialBalance;
}
/**
* 查询余额 - 使用读锁
* 多个线程可以同时查询
*/
public double getBalance() {
readLock.lock(); // 获取读锁
try {
// 模拟查询操作的耗时
try {
Thread.sleep(50); // 50ms 模拟查询时间
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return balance;
} finally {
readLock.unlock(); // 释放读锁
}
}
/**
* 存款 - 使用写锁
* 同一时间只能有一个线程修改余额
*/
public void deposit(double amount) {
writeLock.lock(); // 获取写锁
try {
// 模拟业务处理的耗时
System.out.println("存款线程 - 正在处理存款业务...");
try {
Thread.sleep(100); // 100ms 模拟处理时间
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
balance += amount;
System.out.println("存款线程 - 存款" + amount + "元成功");
} finally {
writeLock.unlock(); // 释放写锁
}
}
/**
* 取款 - 使用写锁
* 同一时间只能有一个线程修改余额
*/
public boolean withdraw(double amount) {
writeLock.lock(); // 获取写锁
try {
// 模拟业务处理的耗时
System.out.println("取款线程 - 正在处理取款业务...");
try {
Thread.sleep(100); // 100ms 模拟处理时间
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
if (balance >= amount) {
balance -= amount;
System.out.println("取款线程 - 取款" + amount + "元成功");
return true;
} else {
System.out.println("取款线程 - 余额不足,取款失败");
return false;
}
} finally {
writeLock.unlock(); // 释放写锁
}
}
public String getAccountNumber() {
return accountNumber;
}
}
在写频繁的环境下,很容易出现数据冲突,那么使用悲观锁进行读写(即前面的读写锁)。如果读频繁于写操作的情况下,那么会出现这么一个问题,修改数据冲突的概率较低,但是悲观锁带来的性能损耗依然在(频繁的上下文切换、内存开销、缓存影响)。
那么这时候,计算机专家想出了一种,不加锁,只在修改数据的时候校验数据是否已经被修正了。这个过程中不加锁,也能保证数据的一致性。是的,乐观锁本质是没有加锁。这样就不会产生损耗了。
CAS机制:
假设共同数据不会出现竞争,获取原数据的内存地址,期望值,新值。
内存地址保证我们能在检查的时候获取原来那个数值,预期值指的是当前地址上应该存在的数值(也是最开始你从这个位置读取的值),如果从内存地址获取的值,不等于预期值,那么就说明这个数值被别的线程修改啦!新值,即你需要更新后的值,如果比对成功了,就把对应地址的数值修改为新值。
但是,这个操作看起来也需要一个原子操作呀
CAS的实现伪代码:
function CAS(pointer, expected, new_value) -> boolean:
old_value = *pointer
if old_value == expected:
*pointer = new_value
return true
else:
return false
这样看起来也很奇怪呀,从地址中获取当前值,再拿来与期望值比对,这个过程中对应地址的值也可能存在并发问题呀!
所以,这里也必须要保证一个原子操作,从读取数值到更新数值,整个过程其实是一个原子操作,但是我们知道又没有在代码中加锁,那么CAS是怎么实现的呢?
原来,这个机制是直接在CPU指令中实现了,直接在硬件中编码。
这不还是用到了锁吗,那么和悲观锁的区别是什么?重点就是在硬件中编码的CAS原子过程,性能高于代码的实现,为纳秒级。且由于是cpu硬件级别的实现,更加底层地符合多核内存的可见性,且CPU对这个处理有专门的优化,也只有CAS指令有这个特殊待遇。
在底层硬件实现了CAS之后,才为一些锁的实现提供的基础,详情以后解析,锁的实现。