乐观锁和悲观锁

乐观锁和悲观锁

什么是悲观锁?

共享资源被访问的时候就锁柱了,其他线程不能对该共享资源操作

像synchronized和ReentrantLock就是独占锁的悲观思想的实现

高并发场景下就会产生激烈的锁竞争,影响性能。

举个例子

数据库表结构
CREATE TABLE accounts (
    account_id INT PRIMARY KEY,
    balance DECIMAL
);

账户id和账户余额

转战操作及步骤

  1. 从源账户扣除金额。
  2. 向目标账户添加相同的金额。

要么同时成功要么同时失败

public void transferAmount(Connection conn, int fromAccountId, int toAccountId, BigDecimal amount) throws SQLException {
    try {
        // 开始事务
        conn.setAutoCommit(false);

        // 锁定源账户和目标账户的行,防止其他事务修改这些行
        //FOR UPDATE向目标数据加锁,这里锁住两行数据
        String lockAccountsSql = "SELECT * FROM accounts WHERE account_id IN (?, ?) FOR UPDATE";
        try (PreparedStatement lockStmt = conn.prepareStatement(lockAccountsSql)) {
            lockStmt.setInt(1, fromAccountId);
            lockStmt.setInt(2, toAccountId);
            lockStmt.executeQuery();
        }
        // 执行转账逻辑
        try (PreparedStatement updateStmt = conn.prepareStatement(
                "UPDATE accounts SET balance = balance - ? WHERE account_id = ?")) {
            updateStmt.setBigDecimal(1, amount);
            updateStmt.setInt(2, fromAccountId);
            updateStmt.executeUpdate();
        }

        try (PreparedStatement updateStmt = conn.prepareStatement(
                "UPDATE accounts SET balance = balance + ? WHERE account_id = ?")) {
            updateStmt.setBigDecimal(1, amount);
            updateStmt.setInt(2, toAccountId);
            updateStmt.executeUpdate();
        }

        // 提交事务
        conn.commit();
    } catch (SQLException e) {
        // 出现异常,回滚事务
        conn.rollback();
        throw e;
    } finally {
        // 恢复自动提交
        conn.setAutoCommit(true);
    }
}

悲观锁一般是锁住数据库数据的,但是乐观锁就可以在数据库层面和后端层面实现

什么是乐观锁?

乐观锁就是共享资源被访问的时候不会被锁住,多少线程都可以,只有修改的时候回去查询该资源是否已经被修改

实现方式有两种CAS和版本号

CAS

先来了解CAS,全称,Compare And Swap(比较和交换),底层依赖一个CPU的原子命令

CAS涉及到三个操作数

V:要更新的变量值(Var)

E:预期值(Expected)

N:拟写入的新值(New)

只有V的值等于E的时候,才会通过原子性操作把N值更新给V值

举个例子:

一个变量count=1,一个线程要修改count,进行count++操作,这时候拟写入的新值是2,当count++后,他会从内存中重新得到要更新的变量值,然后对比预期值(预期值就是我希望他没更新,还是i=1,这样我才能更新),如果一样说明没有线程更改过,可以将新值赋给要跟新的变量值

sun.misc包下的Unsafe类提供了compareAndSwapObject,compareAndSwapInt,compareAndSwapLong来实现Object,int,long的CAS操作

/**
  *  CAS
  * @param o         包含要修改field的对象
  * @param offset    对象中某field的偏移量
  * @param expected  期望值
  * @param update    更新值
  * @return          true | false
  */
public final native boolean compareAndSwapObject(Object o, long offset,  Object expected, Object update);

public final native boolean compareAndSwapInt(Object o, long offset, int expected,int update);

public final native boolean compareAndSwapLong(Object o, long offset, long expected, long update);

看他们都有native修饰,直接设计到底层调用(本地方法,由C或C++编写),Java一般不会直接使用这些方法

所以我们一般实现CAS使用java.util.concurrent.atomic包下的一些类AtomicInteger,AtomicIntegerArray,AtomicReference,

AtomicInteger的部分源码

// setup to use Unsafe.compareAndSwapInt for updates(更新操作时提供“比较并替换”的作用)
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;

static {
    try {
        valueOffset = unsafe.objectFieldOffset
            (AtomicInteger.class.getDeclaredField("value"));
    } catch (Exception ex) { throw new Error(ex); }0
}
//volatile关键词,使value可见
private volatile int value;

AtomicInteger常用方法:

public final int get() //获取当前的值
public final int getAndSet(int newValue)//获取当前的值,并设置新的值
public final int getAndIncrement()//获取当前的值,并自增
public final int getAndDecrement() //获取当前的值,并自减
public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)
public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。

可以随便查看一个AtomicInteger的一个方法getAndAdd(),

在这里插入图片描述

它调用了魔法类的getAndAddInt()

在这里插入图片描述

本质上还是用到了compareAndSwapInt (CAS的一种方法),并且是由native关键词修饰

所以说AtomicInteger实现一般是由native,volatile和CAS实现的

举一个使用AtomicInteger的例子:

class Test2 {
        private AtomicInteger count = new AtomicInteger();

        public void increment() {
                  count.incrementAndGet();
        }
      //使用AtomicInteger之后,不需要加锁,也可以实现线程安全。
       public int getCount() {
                return count.get();
        }
}

上面的例子增加失败后会不断重试,这样就对后端实现了乐观锁

**CAS就讲到这的话会出现一个问题,**试想以下如果我有两个线程,都读取到了i=1,然后i++,但是由于某种原因,第二个线程先将i改成了2,然后又改成了1,这是后第一个线程将数据返回内存的时候就会以为没有更改过i的值,就会出现安全问题

怎么解决呢?我们通过加版本号来解决,每个线程每次更改后版本号都会+1,这就可以解决ABA问题

我们在后端使用AtomicStampedReference类或者AtomicMarkableReference来实现这个功能,这里举例AtomicStampedReference

源码中定义了一个stamp来控制版本号

在这里插入图片描述

import java.util.concurrent.atomic.AtomicStampedReference;

public class AtomicStampedReferenceDemo {
    public static void main(String[] args) {
        // 实例化、取当前值和 stamp 值
        final Integer initialRef = 0, initialStamp = 0;
        final AtomicStampedReference<Integer> asr = new AtomicStampedReference<>(initialRef, initialStamp);
        System.out.println("currentValue=" + asr.getReference() + ", currentStamp=" + asr.getStamp());

        // compare and set
        final Integer newReference = 666, newStamp = 999;
        final boolean casResult = asr.compareAndSet(initialRef, newReference, initialStamp, newStamp);
        System.out.println("currentValue=" + asr.getReference()
                + ", currentStamp=" + asr.getStamp()
                + ", casResult=" + casResult);

        // 获取当前的值和当前的 stamp 值
        int[] arr = new int[1];
        final Integer currentValue = asr.get(arr);
        final int currentStamp = arr[0];
        System.out.println("currentValue=" + currentValue + ", currentStamp=" + currentStamp);

        // 单独设置 stamp 值
        final boolean attemptStampResult = asr.attemptStamp(newReference, 88);
        System.out.println("currentValue=" + asr.getReference()
                + ", currentStamp=" + asr.getStamp()
                + ", attemptStampResult=" + attemptStampResult);

        // 重新设置当前值和 stamp 值
        asr.set(initialRef, initialStamp);
        System.out.println("currentValue=" + asr.getReference() + ", currentStamp=" + asr.getStamp());

        // [不推荐使用,除非搞清楚注释的意思了] weak compare and set
        // 困惑!weakCompareAndSet 这个方法最终还是调用 compareAndSet 方法。[版本: jdk-8u191]
        // 但是注释上写着 "May fail spuriously and does not provide ordering guarantees,
        // so is only rarely an appropriate alternative to compareAndSet."
        // todo 感觉有可能是 jvm 通过方法名在 native 方法里面做了转发
        final boolean wCasResult = asr.weakCompareAndSet(initialRef, newReference, initialStamp, newStamp);
        System.out.println("currentValue=" + asr.getReference()
                + ", currentStamp=" + asr.getStamp()
                + ", wCasResult=" + wCasResult);
    }
}

输出结果如下:

currentValue=0, currentStamp=0
currentValue=666, currentStamp=999, casResult=true
currentValue=666, currentStamp=999
currentValue=666, currentStamp=88, attemptStampResult=true
currentValue=0, currentStamp=0
currentValue=666, currentStamp=999, wCasResult=true

AtomicMarkableReference道理大同小异,标记使用boolean的true or false

import java.util.concurrent.atomic.AtomicMarkableReference;

public class AtomicMarkableReferenceDemo {
    public static void main(String[] args) {
        // 实例化、取当前值和 mark 值
        final Boolean initialRef = null, initialMark = false;
        final AtomicMarkableReference<Boolean> amr = new AtomicMarkableReference<>(initialRef, initialMark);
        System.out.println("currentValue=" + amr.getReference() + ", currentMark=" + amr.isMarked());

        // compare and set
        final Boolean newReference1 = true, newMark1 = true;
        final boolean casResult = amr.compareAndSet(initialRef, newReference1, initialMark, newMark1);
        System.out.println("currentValue=" + amr.getReference()
                + ", currentMark=" + amr.isMarked()
                + ", casResult=" + casResult);

        // 获取当前的值和当前的 mark 值
        boolean[] arr = new boolean[1];
        final Boolean currentValue = amr.get(arr);
        final boolean currentMark = arr[0];
        System.out.println("currentValue=" + currentValue + ", currentMark=" + currentMark);

        // 单独设置 mark 值
        final boolean attemptMarkResult = amr.attemptMark(newReference1, false);
        System.out.println("currentValue=" + amr.getReference()
                + ", currentMark=" + amr.isMarked()
                + ", attemptMarkResult=" + attemptMarkResult);

        // 重新设置当前值和 mark 值
        amr.set(initialRef, initialMark);
        System.out.println("currentValue=" + amr.getReference() + ", currentMark=" + amr.isMarked());

        // [不推荐使用,除非搞清楚注释的意思了] weak compare and set
        // 困惑!weakCompareAndSet 这个方法最终还是调用 compareAndSet 方法。[版本: jdk-8u191]
        // 但是注释上写着 "May fail spuriously and does not provide ordering guarantees,
        // so is only rarely an appropriate alternative to compareAndSet."
        // todo 感觉有可能是 jvm 通过方法名在 native 方法里面做了转发
        final boolean wCasResult = amr.weakCompareAndSet(initialRef, newReference1, initialMark, newMark1);
        System.out.println("currentValue=" + amr.getReference()
                + ", currentMark=" + amr.isMarked()
                + ", wCasResult=" + wCasResult);
    }
}

输出结果如下:

currentValue=null, currentMark=false
currentValue=true, currentMark=true, casResult=true
currentValue=true, currentMark=true
currentValue=true, currentMark=false, attemptMarkResult=true
currentValue=null, currentMark=false
currentValue=true, currentMark=true, wCasResult=true

ABA问题通过CAS(原子性)和版本号来解决

解决了后端的ABA问题,我们来看数据库怎么控制乐观锁

-- 假设我们要将ID为1的产品库存减少1

-- 首先,获取当前库存
SELECT stock FROM products WHERE id = 1;

-- 假设查询结果为10
-- 接下来,尝试更新库存,仅当库存仍为10时执行更新
UPDATE products SET stock = stock - 1 WHERE id = 1 AND stock = 10;

-- 检查更新是否成功
SELECT ROW_COUNT();

我们一般查询的时候就会多加一个类似版本号控制的(stock = 10),意思是我们要更新的时候就会在对比看看这个stock是不是被改过了,没有才会更改

那么数据库有ABA问题吗 ?

-- 假设我们要将ID为1的产品库存减少1

-- 首先,获取当前库存
SELECT stock FROM products WHERE id = 1;

-- 假设查询结果为10
-- 接下来,尝试更新库存,仅当库存仍为10时执行更新
UPDATE products SET stock = stock - 1 WHERE id = 1 AND stock = 10;

-- 检查更新是否成功
SELECT ROW_COUNT();

我们一般查询的时候就会多加一个类似版本号控制的(stock = 10),意思是我们要更新的时候就会在对比看看这个stock是不是被改过了,没有才会更改

那么数据库有ABA问题吗 ?
答案是没有,因为mysql数据库的事务特性ACID中的A就说明了原子性

参考:https://javaguide.cn/java/concurrent/atomic-classes.html

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值