对CAS/乐观锁、读锁/写锁的理解

0 阅读6分钟

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之后,才为一些锁的实现提供的基础,详情以后解析,锁的实现。