如何实现乐观锁

  • 实现乐观锁一般有两种方式:
    • 版本号机制
    • CAS(Compare-And-Swap)算法
  • Java实现CAS主要依赖于Unsafe类提供的底层原子操作方法

1. 版本号机制

  • 一般是在数据表中加上一个数据版本号version字段表示数据被修改的次数。当数据被修改时,version会加1。线程在读取数据时会同时读取version,在提交更新时只有读取到的version和当前数据库中的version一致时才允许更新,否则重试更新操作,直到更新成功

2. CAS(Compare-And-Swap)算法

  • CAS全称是Compare And Swap(比较与交换)。CAS的思想就是用一个预期值和要更新的变量值进行比较,两值相等才会进行更新
  • CAS是一个原子操作,底层依赖于一条CPU的原子指令。
  • CAS涉及到三个操作数:
    • V:要更新的变量值(Var)
    • E:预期值(Expected)
    • N:拟写入的新值(New)
  • 当且仅当V的值与E相等时,才会将V的值更新为N
  • 示例:假设线程A要修改变量i的值为6,i原来的值为1(即V=1, E=1, N=6,假设不存在ABA问题)
    1. i与1进行比较,如果相等,则说明没被其他线程修改,可以修改为6
    2. i与1进行比较,如果不相等,则说明被其他线程修改,当前线程放弃更改,CAS操作失败,线程A可以选择重试

3. Java中CAS是如何实现的

  • 在Java中,实现CAS操作的一个关键类是Unsafe
  • Unsafe类位于sun.misc包中,是一个提供低级别不安全操作的类。
  • Unsafe类提供了compareAndSwapIntcompareAndSwapLongcompareAndSwapObject等方法来实现对intlongObject类型的CAS操作:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    /**
    * 以原子方式更新对象字段的值。
    *
    * @param o 要操作的对象
    * @param offset 对象字段的内存偏移量
    * @param expected 期望的旧值
    * @param x 要设置的新值
    * @return 如果值被成功更新,则返回 true;否则返回 false
    */
    boolean compareAndSwapObject(Object o, long offset, Object expected, Object x);

    /**
    * 以原子方式更新 int 类型的对象字段的值。
    */
    boolean compareAndSwapInt(Object o, long offset, int expected, int x);

    /**
    * 以原子方式更新 long 类型的对象字段的值。
    */
    boolean compareAndSwapLong(Object o, long offset, long expected, long x);
  • Unsafe类中的CAS方法是native方法。native关键字表明这些方法是使用本地代码(通常是C或C++)实现的,这些方法直接调用底层的硬件指令来实现原子操作。也就是说,Java语言并没有直接用Java实现CAS,而是通过C++内联汇编的形式实现的(通过JNI调用)。
  • java.util.concurrent.atomic包提供了一些用于原子操作的类,这些类利用底层的原子指令确保多线程环境下的线程安全。
  • 例如AtomicInteger是Java原子类之一,主要用于对int类型的变量进行原子操作,核心源码如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    // 获取 Unsafe 实例
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
    try {
    // 获取“value”字段在AtomicInteger类中的内存偏移量
    valueOffset = unsafe.objectFieldOffset
    (AtomicInteger.class.getDeclaredField("value"));
    } catch (Exception ex) { throw new Error(ex); }
    }
    // 确保“value”字段的可见性
    private volatile int value;

    // 如果当前值等于预期值,则原子地将值设置为newValue
    // 使用 Unsafe#compareAndSwapInt 方法进行CAS操作
    public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

    // 原子地将当前值加 delta 并返回旧值
    public final int getAndAdd(int delta) {
    return unsafe.getAndAddInt(this, valueOffset, delta);
    }

    // 原子地将当前值加 1 并返回加之前的值(旧值)
    // 使用 Unsafe#getAndAddInt 方法进行CAS操作。
    public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
    }

    // 原子地将当前值减 1 并返回减之前的值(旧值)
    public final int getAndDecrement() {
    return unsafe.getAndAddInt(this, valueOffset, -1);
    }
  • Unsafe#getAndAddInt源码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 原子地获取并增加整数值
    public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
    // 以volatile方式获取对象o在内存偏移量offset处的整数值
    v = getIntVolatile(o, offset);
    // 尝试将当前值加上delta并更新
    } while (!compareAndSwapInt(o, offset, v, v + delta));
    // 返回旧值
    return v;
    }
  • 通过代码可以看出,getAndAddInt方法会通过compareAndSwapInt方法来尝试更新value的值,如果更新失败,它会重新获取当前值并再次尝试更新,直到操作成功
  • 由于CAS操作可能会因为并发冲突而失败,因此通常与while循环搭配使用,这就是自旋锁机制