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

CSDN 2024-08-08 12:05:02 阅读 95

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

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

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

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


文章目录

1、悲观锁与乐观锁1.1、乐观锁1.2、悲观锁

2、CAS 比较并交换2.1、CAS 介绍2.2、CAS 的基本原理2.3、CAS 在 Java 中的应用2.4、CAS 的 ABA 问题2.5、CAS 的自旋问题3、对 Java 中 CAS 的实现解读1、AtomicInteger 对 CAS 的实现2、Unsafe 类简介


1、悲观锁与乐观锁

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

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

1.1、乐观锁

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

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

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

实现方式:

乐观锁通常使用版本号(Version Number)或时间戳(Timestamp)来实现;在 Java 中,乐观锁的实现可以借助 <code>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、悲观锁

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

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

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

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

示例代码:

public class PessimisticLockExample {

private final ReentrantLock lock = new ReentrantLock();

public void performTask() {

lock.lock();

try {

// 执行需要同步的代码

} finally {

lock.unlock();

}

}

}

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


2、CAS 比较并交换

2.1、CAS 介绍

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

CAS 的优点:

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

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

2.2、CAS 的基本原理

CAS 操作包含三个操作数:

内存位置(V):需要被更新的变量的内存地址;预期原值(A):预期该内存位置的值;新值(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());

}

}

在上述示例中,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();

}

}

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

2.5、CAS 的自旋问题

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

解决自旋问题的方案:

限制自旋次数:在一定次数的自旋后,采用其他方式处理,如锁机制;带有退避的自旋:在每次自旋失败后,等待一段时间再尝试,以减少 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;

}

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;

}

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

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

此方法是 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;

}

在 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;

}

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

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

此方法为 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( 内存地址,备份的旧数据,新数据 ))

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);



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。