Java 入门指南:Java 并发编程 —— CAS 机制实现乐观锁(Optimistic Locking)

乐观锁

乐观锁(Optimistic Locking)是一种并发控制机制,相对于悲观锁(如互斥锁)而言,它更倾向于假设并发冲突不会发生,从而减少锁的使用,提高并发性能。

乐观锁并不需要像悲观锁一样显式地加锁和释放锁,而是通过比较数据版本或执行原子操作来实现并发控制

乐观锁适用于读操作远多于写操作、并发度较高的场景,能够提高并发性能,但需要考虑并发冲突的处理。在设计系统时,需要根据具体的应用场景来权衡选择合适的并发控制策略。如果数据竞争不激烈,可以优先考虑使用乐观锁来提高系统的并发性能。

实现方式

在 Java 中,乐观锁可以通过以下几种方式实现:

  1. 版本号(Versioning):每个数据对象维护一个版本号字段。

    在读取数据时,记录当前的版本号,执行更新操作时,比较当前的版本号与记录的版本号是否一致,如果一致则进行更新操作,并更新版本号;如果不一致则说明有其他线程已经修改了数据,需要进行冲突处理。

  2. 时间戳(Timestamp):每个数据对象维护一个时间戳字段。

    通过比较时间戳来判断是否发生并发冲突。读取数据时记录当前时间戳,执行更新操作时,比较当前时间戳与记录的时间戳,如果一致则进行更新操作,并更新时间戳;如果不一致则说明有其他线程已经修改了数据,需要进行冲突处理。

  3. CAS(Compare and Swap)操作:CAS 是一种原子操作,通过比较内存中的值与预期值是否一致,如果一致则进行更新操作,否则不进行任何操作。

    在 Java 中可以使用 java.util.concurrent.atomic 包中的原子类(如 AtomicIntegerAtomicLongAtomicReference 等)来实现 CAS 操作。

乐观锁的实现需要考虑并发操作的原子性和线程安全性。以上的实现方式都是通过使用特定的字段(如版本号或时间戳)来进行数据版本的控制,再结合适当的冲突处理机制来保证数据的一致性。

CAS 机制

CAS(Compare and Swap)是一种用于管理多线程并发访问共享资源的原子操作,用于解决并发环境下的数据竞争问题。

CAS 机制包含三个操作数:

  • 需要读写的内存位置 V
  • 期望的原值(旧值) E
  • 新值 N

若内存位置 V 的值与预期的原值 E 相等,那么处理器会用新值 N 更新内存位置 V 的值,否则说明已经有其它线程更新了 V 的值,会放弃更新操作,不做任何处理。

这个过程是原子性的。CAS 是一种系统原语,是一条 CPU 的原子指令,从 CPU 层面已经保证它的原子性。

当多个线程同时使用 CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,也允许失败的线程放弃操作。

CAS 操作在读写共享资源时始终保持一种 乐观 的状态,它充分利用了多处理器系统的优势,并且不需要使用互斥锁等机制。因此,CAS操作通常比使用互斥锁等机制的方式具有更高的效率

实现原理

JDK 通过 CPU 的 cmpxchg 指令去比较寄存器中的 A 和 内存中的值 V。如果相等,就把要写入的新值 B 存入内存中。如果不相等,就将内存值 V 赋值给寄存器中的值 A。

然后通过 Java 代码中的 while 循环再次调用 cmpxchg 指令进行重试,直到设置成功为止。

CMPXCHG是“Compare and Exchange”的缩写,它是一种原子指令,用于在多核/多线程环境中安全地修改共享数据。

CMPXCHG在很多现代微处理器体系结构中都有,例如Intel x86/x64体系。对于32位操作数,这个指令通常写作CMPXCHG,而在64位操作数中,它被称为CMPXCHG8B或CMPXCHG16B。

应用场景

需要注意的是,虽然 CAS 能够解决一部分的并发问题,但并不是所有的场景都适合使用 CAS

因为 CAS 的操作是基于某个内存位置上的原值进行的,因此当目标内存位置被多个线程频繁操作时,CAS 机制的性能会有所下降。另外,在 CAS 操作完成之后,不一定能够正确地判断出值是否被更新,因此需要谨慎使用 CAS

ABA 问题

ABA 问题是在并发编程中常见的一个问题,主要发生在使用 CAS(Compare and Swap)操作进行同步时。

ABA 问题的发生是因为在 CAS 操作中,只比较了值是否相等,而没有考虑值在期间是否发生了变化

具体来说,假设初始值为 A,并发 线程1 将其改为 B,然后又改回 A,最后 线程2 执行 CAS操作,发现预期值依然为A,于是执行了更新操作。尽管 线程1 和 线程2 之间的值不一样,但是CAS操作仍然成功,这就导致了数据的不一致性。

为了解决 ABA 问题,可以引入版本号时间戳,并在 CAS 操作时将其一同比较。当更新操作发生时,版本号也会相应增加,这样即使值发生了变化,版本号的变化也能被检测到。这样,只有在值与版本号都满足预期时,CAS操作才会成功。

ABA 问题并不是所有情况都需要解决的问题,它的发生和解决与具体的业务场景和代码逻辑有关。在使用 CAS 操作进行同步时,如果存在 ABA 问题的风险,就需要考虑相应的解决方案。

解决 ABA 问题

Java 中的 AtomicStampedReferenceAtomicMarkableReference 就是为了解决ABA问题而提供的原子引用类,它们在 CAS 比较时同时比较了值和版本号或标记,从而避免了ABA问题的发生。

AtomicStampedReference.compareAndSet 方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果二者都相等,才使用 CAS 设置为新的值和标志。

Java 入门指南:Java 并发编程 —— CAS 机制实现乐观锁(Optimistic Locking)_第1张图片

  1. Pair current = pair; 这行代码获取当前的 pair 对象,其中包含了引用和标记。

  2. 接下来的 return 语句做了几个检查:

    • 首先检查当前的引用和标记是否和预期的引用和标记相同。如果二者中有任何一个不同,返回 false。

    • 如果当前的引用和标记与预期的相同,接下来检查新的引用和标记是否也与当前的相同。如果相同,则不需要更改标记和引用,返回 true。

    • 如果新的引用或者标记与当前的不同,那么就会调用 casPair 方法来尝试更新 pair 对象。

      casPair 方法会尝试用 newReference 和 newStamp 创建的新的 Pair 对象替换当前的 pair 对象。

      • 如果替换成功,casPair 方法会返回 true

      • 如果替换失败(在尝试替换的过程中,pair 对象已经被其他线程改变了),casPair 方法会返回 false。

长时间自旋

在自旋 CAS 过程中,如果比较和更新操作失败,则会进入自旋等待,即通过不断尝试重新比较和更新的方式来避免线程阻塞。自旋等待的时间需要适度控制,过长会浪费 CPU 资源,过短又可能导致频繁的比较和更新操作。

旋 CAS 适用于在共享资源不频繁发生竞争时,通过自旋等待来避免线程阻塞的场景。但在并发度较高或共享资源竞争激烈的情况下,过多的自旋等待可能会造成CPU资源浪费。

解决 CPU 资源浪费问题的思路是让 JVM 支持处理器提供的pause 指令

pause 指令能让自旋失败时 CPU 睡眠一小段时间再继续自旋,从而使得读操作的频率降低很多,为解决内存顺序冲突而导致的 CPU 流水线重排的代价也会小很多。

多个共享变量的原子操作

当对一个共享变量执行操作时,CAS 能够保证该变量的原子性。但是对于多个共享变量,CAS 就无法保证操作的原子性,这时通常有两种做法:

  1. 使用 AtomicReference 类保证对象之间的原子性,把多个变量放到一个对象里面进行 CAS 操作;

  2. 使用锁。锁内的临界区代码可以保证只有当前线程能操作。

原子操作

原子操作(Atomic Operation)是一种不可分割的操作,要么全部执行成功,要么全部不执行,不存在中间状态。原子操作可以保证在多线程或多进程环境下对共享数据的操作是原子的,即无论有多少线程同时进行这个操作,其结果都是符合预期的。

原子操作的特性

原子操作具有以下特性:

  1. 原子性:完成操作的过程是不可中断的,要么完全执行成功,要么完全不执行。

  2. 可见性:原子操作的结果对其他线程或进程是可见的,即其他线程可以立即看到操作后的结果。
    Java 入门指南:Java 并发编程 —— CAS 机制实现乐观锁(Optimistic Locking)_第2张图片

  3. 顺序性:原子操作会按照顺序执行,不会重排或调整顺序。

原子操作可以保证在并发执行的情况下,共享数据的一致性和正确性。常见的原子操作包括原子读取、原子写入、原子比较并交换(Compare-and-Swap)等。这些操作通常由硬件提供支持,确保在执行时不会被中断。

原子操作常用于解决并发编程中的竞态条件和数据不一致性问题。使用原子操作可以避免使用锁机制的开销和复杂性,提高并发性能和编程简洁性。

Atomic 包

在编程中,很多编程语言和库提供了原子操作的支持,例如 C++std::atomicJavajava.util.concurrent.atomic 包、Pythonmultiprocessing 模块等。通过使用原子操作,可以编写出更加并发安全和高效的程序。

在 Java 中, Atomic 系列的类是通过 CAS机制 来实现线程安全的,如AtomicIntegerAtomicLongAtomicBooleanAtomicIntegerArrayAtomicLongArray, AtomicReferenceAtomicStampedReference 等,它们都是在一个基本类型或引用类型上提供了原子性的操作。如:

  • AtomicBoolean:一个提供原子操作的boolean值,这些操作包括设置值、获取当前值以及使用CAS(Compare-And-Swap)机制安全地更新值。

  • AtomicInteger:一个提供原子操作的int值。它提供了递增(incrementAndGet())、递减(decrementAndGet())、添加(addAndGet(int delta))等原子操作。

  • AtomicReference:一个对象引用的原子更新器,可以安全地更新对象引用。它支持原子的获取、设置和比较设置操作。

  • AtomicIntegerArrayAtomicLongArray:这些类分别提供了对 intlong 数组元素进行原子操作的能力。

  • AtomicIntegerFieldUpdaterAtomicLongFieldUpdaterAtomicReferenceFieldUpdater:这些更新器类允许原子地更新指定类的指定volatile字段。这对于需要更新对象内部状态但不希望锁定整个对象的场景非常有用。

在 Java 中,乐观锁可以通过 CAS(Compare and Swap)机制来实现。CAS 是一种无锁编程技术,它在多线程环境下提供了一种原子更新操作。Java 的 java.util.concurrent.atomic 包提供了多个原子类,如 AtomicIntegerAtomicLongAtomicReference 等,这些类内部使用了 CAS 机制来保证线程安全。

使用 AtomicInteger 实现乐观锁

假设我们有一个简单的计数器类,使用 AtomicInteger 来实现乐观锁:

import java.util.concurrent.atomic.AtomicInteger;

public class OptimisticCounter {
    private AtomicInteger value = new AtomicInteger(0);
    private AtomicInteger version = new AtomicInteger(0);

    public int getValue() {
        return value.get();
    }

    public int increment() {
        int currentVersion = version.get();
        int currentValue = value.get();
        while (!value.compareAndSet(currentValue, currentValue + 1)) {
            currentValue = value.get();
        }
        version.set(currentVersion + 1);
        return currentValue + 1;
    }

    public int decrement() {
        int currentVersion = version.get();
        int currentValue = value.get();
        while (!value.compareAndSet(currentValue, currentValue - 1)) {
            currentValue = value.get();
        }
        version.set(currentVersion + 1);
        return currentValue - 1;
    }
}

示例说明

  • AtomicInteger

    • value 字段使用 AtomicInteger 来保证原子性。
    • version 字段同样使用 AtomicInteger 来记录版本号。
  • increment 和 decrement 方法

    • incrementdecrement 方法中,首先读取当前的 valueversion
    • 使用 compareAndSet 方法尝试更新 value。如果更新失败(即有其他线程修改了 value),则重新读取 value 并再次尝试。
    • 更新成功后,增加版本号 version

使用 AtomicReference 实现乐观锁

除了使用 AtomicInteger 外,还可以使用 AtomicReference 来实现乐观锁,特别是当需要乐观锁的对象不仅仅是简单的数值时。以下是一个使用 AtomicReference 的示例:

import java.util.concurrent.atomic.AtomicReference;

public class OptimisticLockingExample {
    private static class Item {
        private int value;
        private int version;

        public Item(int value, int version) {
            this.value = value;
            this.version = version;
        }

        public int getValue() {
            return value;
        }

        public int getVersion() {
            return version;
        }

        public void setValue(int value) {
            this.value = value;
        }

        public void setVersion(int version) {
            this.version = version;
        }

        @Override
        public String toString() {
            return "Item{" +
                    "value=" + value +
                    ", version=" + version +
                    '}';
        }
    }

    private AtomicReference<Item> item = new AtomicReference<>(new Item(0, 0));

    public int increment() {
        Item current = item.get();
        while (true) {
            Item updated = new Item(current.getValue() + 1, current.getVersion() + 1);
            if (item.compareAndSet(current, updated)) {
                return updated.getValue();
            }
            current = item.get();
        }
    }

    public int decrement() {
        Item current = item.get();
        while (true) {
            Item updated = new Item(current.getValue() - 1, current.getVersion() + 1);
            if (item.compareAndSet(current, updated)) {
                return updated.getValue();
            }
            current = item.get();
        }
    }
}

示例说明:

  • AtomicReference

    • item 字段使用 AtomicReference 来保存一个 Item 对象的引用,确保线程安全。
  • increment 和 decrement 方法

    • incrementdecrement 方法中,首先读取当前的 Item 对象。
    • 创建一个新的 Item 对象,更新其 valueversion
    • 使用 compareAndSet 方法尝试更新 item。如果更新失败,则重新读取 item 并再次尝试。
    • 更新成功后,返回新的 value

你可能感兴趣的:(Java,java,开发语言,个人开发,后端,运维,安全)