赞
踩
大家好,我是栗筝i,这篇文章是我的 “栗筝i 的 Java 技术栈” 专栏的第 025 篇文章,在 “栗筝i 的 Java 技术栈” 这个专栏中我会持续为大家更新 Java 技术相关全套技术栈内容。专栏的主要目标是已经有一定 Java 开发经验,并希望进一步完善自己对整个 Java 技术体系来充实自己的技术栈的同学。与此同时,本专栏的所有文章,也都会准备充足的代码示例和完善的知识点梳理,因此也十分适合零基础的小白和要准备工作面试的同学学习。当然,我也会在必要的时候进行相关技术深度的技术解读,相信即使是拥有多年 Java 开发经验的从业者和大佬们也会有所收获并找到乐趣。
–
在现代软件开发中,并发编程已成为必不可少的技术。随着多核处理器的普及,如何高效地管理多线程环境下的资源竞争,成为开发者需要面对的重要课题。传统的锁机制(如
synchronized
关键字和Lock
接口)虽然能够解决并发问题,但也带来了性能瓶颈和死锁风险。为了克服这些缺点,乐观锁和 CAS(Compare And Swap,比较并交换)作为一种无锁并发解决方案应运而生。乐观锁的核心思想是“假设并发冲突很少发生”,因此在进行操作时不立即加锁,而是通过检测冲突来确保数据的一致性。CAS 操作基于 CPU 的原子指令,能够在不使用锁的情况下实现变量的安全更新,从而提高系统的并发性能。
在本文中,我们将深入探讨 Java 并发编程中的乐观锁与 CAS。通过分析
AtomicInteger
的源码,我们将揭示CAS 操作的工作原理,并探讨其在多线程环境中的实际应用。此外,我们还将介绍 ABA 问题及其解决方案,以及 CAS 自旋操作中的一些优化策略。
悲观锁与乐观锁并不是特指某个锁(Java 中没有哪个 Lock 实现类就叫 PessimisticLock 或 OptimisticLock),而是在并发情况下的两种不同策略。
悲观锁与乐观锁是锁的一种宏观分类方式,代表了在并发情况下的两种不同策略。
乐观锁(Optimistic Locking)假定系统中的并发冲突是少数情况,因此在对数据进行操作时,不会立即加锁,而是在提交操作之前检查是否有其他线程修改了数据:
这种锁机制的关键在于 “乐观”,即假设大部分情况下不会发生冲突。
实现方式:
java.util.concurrent.atomic
包中的原子变量,如 AtomicInteger
、AtomicLong
等。适用场景:并发冲突较少的场景、读多写少的场景。
示例代码:
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; } }
悲观锁(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();
}
}
}
总的来说,悲观锁和乐观锁代表了两种不同的并发控制策略。悲观锁适用于并发冲突频繁的场景,通过加锁机制保证数据的一致性;乐观锁适用于并发冲突较少的场景,通过版本控制机制减少锁开销,提高系统性能。在实际应用中,需要根据具体的并发场景选择合适的锁策略,以平衡数据一致性和系统性能。
CAS,即 “比较并交换”(Compare-And-Swap),是一种用于解决多线程并行情况下性能损耗问题的机制。CAS 操作是一种乐观锁实现,广泛应用于 java.util.concurrent
包中的并发类。
CAS 的优点:
CAS 的缺点:忙等待:在高并发情况下,CAS 操作可能会不断重试,导致 CPU 资源浪费。
CAS 操作包含三个操作数:
CAS 操作的执行步骤如下:
CAS 有效地说明了:“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可”。
在 Java 中,sun.misc.Unsafe
类提供了硬件级别的原子操作来实现 CAS。java.util.concurrent
包下的大量类都使用了 Unsafe
类的 CAS 操作,如 AtomicInteger
、AtomicLong
、AtomicReference
等。
示例代码:
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 操作,如果当前值等于预期值,则更新为新值,否则重试。
ABA 问题是指在使用 CAS(比较并交换)操作时,一个值从 A 变为 B,又变回 A,此时 CAS 操作会误认为值没有发生变化,从而导致错误的更新。虽然值看起来没有变化,但实际上已经发生了两次变化。
为了解决 ABA 问题,可以引入版本号(或时间戳)。通过在变量前面追加版本号,每次变量更新时将版本号加一,即使值从 A 变为 B 再变回 A,版本号也会不同,从而避免 ABA 问题。例如:
在 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 问题。
CAS 的另一个问题是自旋操作,即在不断尝试 CAS 操作时,如果长时间不成功,会导致CPU资源浪费,影响系统性能。
解决自旋问题的方案:
我们这里以 AtomicInteger
类为例,对 Java 中 CAS 的实现进行解读。
AtomicInteger
是一个支持原子操作的 Integer
类,它保证对 AtomicInteger
类型变量的增加和减少操作是原子性的,不会出现多个线程下的数据不一致问题。如果不使用 AtomicInteger
,要实现一个按顺序获取的 ID,就必须在每次获取时进行加锁操作,以避免出现并发时获取到同样的 ID 的现象。
通过源码来看 JDK 8 中 AtomicInteger
的 incrementAndGet()
方法的实现:
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
,表示在这个过程中,值已经被其他线程修改过了,循环会重新获取当前值并尝试更新,直到成功为止。
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 的核心。
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);
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。