当前位置:   article > 正文

Java 并发编程:Java 中的乐观锁与 CAS

Java 并发编程:Java 中的乐观锁与 CAS

大家好,我是栗筝i,这篇文章是我的 “栗筝i 的 Java 技术栈” 专栏的第 025 篇文章,在 “栗筝i 的 Java 技术栈” 这个专栏中我会持续为大家更新 Java 技术相关全套技术栈内容。专栏的主要目标是已经有一定 Java 开发经验,并希望进一步完善自己对整个 Java 技术体系来充实自己的技术栈的同学。与此同时,本专栏的所有文章,也都会准备充足的代码示例和完善的知识点梳理,因此也十分适合零基础的小白和要准备工作面试的同学学习。当然,我也会在必要的时候进行相关技术深度的技术解读,相信即使是拥有多年 Java 开发经验的从业者和大佬们也会有所收获并找到乐趣。

在现代软件开发中,并发编程已成为必不可少的技术。随着多核处理器的普及,如何高效地管理多线程环境下的资源竞争,成为开发者需要面对的重要课题。传统的锁机制(如synchronized关键字和Lock接口)虽然能够解决并发问题,但也带来了性能瓶颈和死锁风险。

为了克服这些缺点,乐观锁和 CAS(Compare And Swap,比较并交换)作为一种无锁并发解决方案应运而生。乐观锁的核心思想是“假设并发冲突很少发生”,因此在进行操作时不立即加锁,而是通过检测冲突来确保数据的一致性。CAS 操作基于 CPU 的原子指令,能够在不使用锁的情况下实现变量的安全更新,从而提高系统的并发性能。

在本文中,我们将深入探讨 Java 并发编程中的乐观锁与 CAS。通过分析AtomicInteger的源码,我们将揭示CAS 操作的工作原理,并探讨其在多线程环境中的实际应用。此外,我们还将介绍 ABA 问题及其解决方案,以及 CAS 自旋操作中的一些优化策略。



1、悲观锁与乐观锁

悲观锁与乐观锁并不是特指某个锁(Java 中没有哪个 Lock 实现类就叫 PessimisticLock 或 OptimisticLock),而是在并发情况下的两种不同策略。

悲观锁与乐观锁是锁的一种宏观分类方式,代表了在并发情况下的两种不同策略。

1.1、乐观锁

乐观锁(Optimistic Locking)假定系统中的并发冲突是少数情况,因此在对数据进行操作时,不会立即加锁,而是在提交操作之前检查是否有其他线程修改了数据:

  • 如果没有冲突,则提交成功;
  • 如果发生冲突,则重试操作。

这种锁机制的关键在于 “乐观”,即假设大部分情况下不会发生冲突。

实现方式:

  • 乐观锁通常使用版本号(Version Number)或时间戳(Timestamp)来实现;
  • 在 Java 中,乐观锁的实现可以借助 java.util.concurrent.atomic 包中的原子变量,如 AtomicIntegerAtomicLong 等。

适用场景:并发冲突较少的场景、读多写少的场景。

示例代码:

import java.util.concurrent.atomic.AtomicInteger;

public class OptimisticLockExample {
    private final AtomicInteger version = new AtomicInteger(0);
    private int value;

    public void performTask() {
        int currentVersion;
        int newValue;

        do {
            currentVersion = version.get();
            // 执行需要同步的代码
            newValue = value + 1;
        } while (!version.compareAndSet(currentVersion, currentVersion + 1));
        value = newValue;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
1.2、悲观锁

悲观锁(Pessimistic Locking)假定系统中的并发冲突是常态,因此在对数据进行操作时,会采取加锁的方式,以防止其他线程修改数据。

这种锁机制的关键在于"悲观",即假设每次操作都会发生冲突。因此,悲观锁的策略是:在一个线程开始操作数据之前,先获取对该数据的排他锁(exclusive lock),这样其他线程就无法同时操作该数据,从而保证数据的完整性。

实现方式:在 Java 中,悲观锁可以通过 synchronized 关键字或 ReentrantLock 类来实现。

适用场景:并发冲突频繁的场景、操作的数据量大且操作时间较长的场景。

示例代码:

public class PessimisticLockExample {
    private final ReentrantLock lock = new ReentrantLock();

    public void performTask() {
        lock.lock();
        try {
            // 执行需要同步的代码
        } finally {
            lock.unlock();
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

总的来说,悲观锁和乐观锁代表了两种不同的并发控制策略。悲观锁适用于并发冲突频繁的场景,通过加锁机制保证数据的一致性;乐观锁适用于并发冲突较少的场景,通过版本控制机制减少锁开销,提高系统性能。在实际应用中,需要根据具体的并发场景选择合适的锁策略,以平衡数据一致性和系统性能。


2、CAS 比较并交换

2.1、CAS 介绍

CAS,即 “比较并交换”(Compare-And-Swap),是一种用于解决多线程并行情况下性能损耗问题的机制。CAS 操作是一种乐观锁实现,广泛应用于 java.util.concurrent 包中的并发类。

CAS 的优点:

  • 高效:CAS 是无锁操作,避免了传统锁机制带来的线程切换和上下文切换的开销;
  • 无死锁:由于没有使用锁,因此不会出现死锁问题。

CAS 的缺点:忙等待:在高并发情况下,CAS 操作可能会不断重试,导致 CPU 资源浪费。

2.2、CAS 的基本原理

CAS 操作包含三个操作数:

  1. 内存位置(V):需要被更新的变量的内存地址;
  2. 预期原值(A):预期该内存位置的值;
  3. 新值(B):希望设置的新值。

CAS 操作的执行步骤如下:

  • 比较内存位置 V 的值是否等于预期原值 A;
  • 如果相等,则将该内存位置的值更新为新值 B;
  • 如果不相等,则不执行更新操作,并返回该内存位置当前的值。

CAS 有效地说明了:“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可”。

2.3、CAS 在 Java 中的应用

在 Java 中,sun.misc.Unsafe 类提供了硬件级别的原子操作来实现 CAS。java.util.concurrent 包下的大量类都使用了 Unsafe 类的 CAS 操作,如 AtomicIntegerAtomicLongAtomicReference 等。

示例代码:

import java.util.concurrent.atomic.AtomicInteger;

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

    public void increment() {
        int oldValue;
        int newValue;
        do {
          	// 获取当前值
            oldValue = value.get(); 
          	// 计算新值
            newValue = oldValue + 1; 
        } while (!value.compareAndSet(oldValue, newValue)); 	// 比较并交换
    }

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

    public static void main(String[] args) {
        CASExample example = new CASExample();
        example.increment();
        System.out.println("Value: " + example.getValue());
    }
}
  • 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

在上述示例中,compareAndSet 方法用于执行 CAS 操作,如果当前值等于预期值,则更新为新值,否则重试。

2.4、CAS 的 ABA 问题

ABA 问题是指在使用 CAS(比较并交换)操作时,一个值从 A 变为 B,又变回 A,此时 CAS 操作会误认为值没有发生变化,从而导致错误的更新。虽然值看起来没有变化,但实际上已经发生了两次变化。

为了解决 ABA 问题,可以引入版本号(或时间戳)。通过在变量前面追加版本号,每次变量更新时将版本号加一,即使值从 A 变为 B 再变回 A,版本号也会不同,从而避免 ABA 问题。例如:

  • 原值:1A(版本号1,值A)
  • 变化:1A -> 2B -> 3A

在 Java 中,使用 AtomicStampedReference 来解决 ABA 问题。AtomicStampedReference 不仅维护了对象的引用,还维护了一个整数"标记"(通常是版本号),每次修改时更新标记,从而确保每次修改都是唯一的。

示例代码:

import java.util.concurrent.atomic.AtomicStampedReference;

public class ABAProblemSolution {
    private static AtomicStampedReference<Integer> atomicStampedRef =
        new AtomicStampedReference<>(100, 1);

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            int stamp = atomicStampedRef.getStamp();
            System.out.println("Thread1 - Initial Stamp: " + stamp);
            atomicStampedRef.compareAndSet(100, 101, stamp, stamp + 1);
            System.out.println("Thread1 - New Stamp: " + atomicStampedRef.getStamp());
            atomicStampedRef.compareAndSet(101, 100, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);
            System.out.println("Thread1 - Final Stamp: " + atomicStampedRef.getStamp());
        });

        Thread t2 = new Thread(() -> {
            int stamp = atomicStampedRef.getStamp();
            System.out.println("Thread2 - Initial Stamp: " + stamp);
            try {
              	// 确保Thread1完成ABA操作
                Thread.sleep(1000); 
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            boolean success = atomicStampedRef.compareAndSet(100, 110, stamp, stamp + 1);
            System.out.println("Thread2 - CAS success: " + success);
            System.out.println("Thread2 - New Stamp: " + atomicStampedRef.getStamp());
        });

        t1.start();
        t2.start();
    }
}
  • 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

在这个示例中,Thread1 执行了 ABA 操作(100 -> 101 -> 100),但是由于每次操作都会更新版本号,Thread2 在尝试更新时,检测到版本号不匹配,从而避免了 ABA 问题。

2.5、CAS 的自旋问题

CAS 的另一个问题是自旋操作,即在不断尝试 CAS 操作时,如果长时间不成功,会导致CPU资源浪费,影响系统性能。

解决自旋问题的方案:

  1. 限制自旋次数:在一定次数的自旋后,采用其他方式处理,如锁机制;
  2. 带有退避的自旋:在每次自旋失败后,等待一段时间再尝试,以减少 CPU 资源的消耗。

3、对 Java 中 CAS 的实现解读

我们这里以 AtomicInteger 类为例,对 Java 中 CAS 的实现进行解读。

AtomicInteger 是一个支持原子操作的 Integer 类,它保证对 AtomicInteger 类型变量的增加和减少操作是原子性的,不会出现多个线程下的数据不一致问题。如果不使用 AtomicInteger,要实现一个按顺序获取的 ID,就必须在每次获取时进行加锁操作,以避免出现并发时获取到同样的 ID 的现象。

通过源码来看 JDK 8 中 AtomicIntegerincrementAndGet() 方法的实现:

public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

  • 1
  • 2
  • 3
  • 4

incrementAndGet() 方法中,使用了 Unsafe 类。下面是 Unsafe 类中提供的 getAndAddInt 方法:

public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        v = getIntVolatile(o, offset);
    } while (!compareAndSwapInt(o, offset, v, v + delta));
    return v;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

通过一个 do-while 循环实现,核心在于 compareAndSwapInt() 方法:

public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);
  • 1

此方法是 native 方法,compareAndSwapInt 基于的是 CPU 的 CAS 指令来实现的。因此,基于 CAS 的操作可认为是无阻塞的,一个线程的失败或挂起不会引起其它线程也失败或挂起。由于 CAS 操作是 CPU 原语,所以性能比较好。

回到 incrementAndGet() 方法中:

  • 第一个值是当前对象;
  • 第二个值是当前值的偏移量;
  • 第三个值是要增加的量。

getAndAddInt 方法首先通过 getIntVolatile(o, offset) 获取当前值,然后通过 compareAndSwapInt(o, offset, v, v + delta) 进行比较和交换。如果 compareAndSwapInt 返回 false,表示在这个过程中,值已经被其他线程修改过了,循环会重新获取当前值并尝试更新,直到成功为止。


1、AtomicInteger 对 CAS 的实现

AtomicInteger 是一个支持原子操作的 Integer 类,就是保证对 AtomicInteger 类型变量的增加和减少操作是原子性的,不会出现多个线程下的数据不一致问题。如果不使用 AtomicInteger,要实现一个按顺序获取的 ID,就必须在每次获取时进行加锁操作,以避免出现并发时获取到同样的 ID 的现象。

接下来通过源代码来看 Jdk8 中 AtomicInteger 中 incrementAndGet() 方法的实现,下面是具体的代码。

// JDK 8
public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
  • 1
  • 2
  • 3
  • 4

在 incrementAndGet 里,我们可以看到使用了 Unsafe 类,下面是 Unsafe 里提供的 getAndAddInt 方法:

public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
      	v = getIntVolatile(o, offset);
       	} while (!compareAndSwapInt(o, offset, v, v + delta));
    return v;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

通过一个 do while 语句来做一个主体实现的在 while 语句里核心调了 compareAndSwapInt() 方法:

public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);
  • 1

此方法为 native 方法,compareAndSwapInt 基于的是 CPU 的 CAS 指令来实现的。所以基于 CAS 的操作可认为是无阻塞的,一个线程的失败或挂起不会引起其它线程也失败或挂起。并且由于 CAS 操作是 CPU 原语,所以性能比较好。

回到 incrementAndGet 中:我们传过来的第一个值是当前的对象,第二个值是我们当前的值(比如如果我们要实现2+1)那么 offset 就是 2 delta 就是1,这里的 v,它是我们调用底层的方法v v = this.getIntVolatile(o, offset); 获取底层当前的值。如果没有其他线程来处理 o 这个变量的时候,它的正常返回值应该是 2,因此传到 compareAndSwapInt 的参数就是(o,2,2,2+1),这个方法想达到的目标就是对于 o 这个对象,如果当前的这个值和底层的这个值相等的情况下,就把它更新成后面那个值 v + delta。

当我们一个方法进来的时候,我们 offset 的值是2,我们第一次取出来 v 的值也等于 2,但是当我们在执行更新成 3 的时候 也就是这句代码 while (!compareAndSwapInt(o, offset, v, v + delta));可能会被其它线程更改,所以我们要判断 offset 是否与 v 是相同的,只有是相同的,才允许它更新为 3。通过这样不停的循环来判断。就能保证期望的值和底层的值相同。

CAS比较与交换的伪代码可以表示为:
do{
		备份旧数据;
		基于旧数据构造新数据;
}while(!CAS( 内存地址,备份的旧数据,新数据 ))
  • 1
  • 2
  • 3
  • 4
  • 5

Java中的乐观锁大部分都是基于CAS(Compare And Swap,比较和交换)操作实现的,CAS设一种原子操作,在对数据操作之前,首先会比较当前值跟传入值是否一样,如果一样咋更新,否则不执行更新操作直接返回失败状态。compareAndSwapInt 也是 CAS 的核心。

2、Unsafe 类简介

Unsafe 类和 C++ 有点类似,在 Java 中是没有办法直接操作内存的,但是 Unsafe 类却可以间接的让程序员操作内存区域。

Unsafe 是位于 sun.misc 包下的一个类。Unsafe 提供的 API 大致可分为内存操作、CAS、Class 相关、对象操作、线程调度、系统信息获取、内存屏障、数组操作等几类。由于并发相关的源码很多用到了 CAS,比如 java.util.concurrent.atomic 相关类、AQS、CurrentHashMap 等相关类。

CAS 主要相关源码:

    /**
     * 参数说明
     * @param o             包含要修改field的对象
     * @param offset        对象中某个参数field的偏移量,该偏移量不会改变
     * @param expected      期望该偏移量对应的field值
     * @param x             更新值
     * @return              true|false
     */
    public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object x);

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

    public final native boolean compareAndSwapLong(Object o, long offset, long expected, long x);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/木道寻08/article/detail/942153
推荐阅读
相关标签
  

闽ICP备14008679号