CAS
1. CAS 的定义
CAS(Compare-And-Swap)是一种无锁算法,用于实现多线程环境下的原子操作。它的基本思想是:
- 检查当前变量的值是否与预期值相同。
- 如果相同,则将变量更新为目标值;否则,不进行更新。
在 Java 中,CAS 通常通过 java.util.concurrent.atomic
包中的类实现,例如 AtomicInteger
、AtomicReference
等,底层依赖于 CPU 提供的硬件指令(如 x86 架构下的 CMPXCHG
指令)。
2. 乐观锁的定义
乐观锁是一种并发控制策略,假设冲突的可能性较低,因此在操作数据时不加锁,而是在提交更新时检查是否有冲突。如果检测到冲突,则采取重试或其他措施。
乐观锁的核心特点:
- 不会阻塞线程。
- 适用于读多写少的场景。
- 需要某种机制来检测冲突,例如版本号或时间戳。
3. CAS 为什么可以被视为乐观锁
-
无锁设计:
- CAS 不需要像
synchronized
或ReentrantLock
那样显式加锁,而是通过比较和交换的方式尝试更新数据。 - 这种设计符合乐观锁的理念:假设冲突较少,直接尝试操作。
- CAS 不需要像
-
冲突检测:
- CAS 的核心机制是 “比较并交换” ,即在更新前检查当前值是否与预期值一致。如果不一致,说明发生了冲突,操作失败。
- 这种冲突检测机制正是乐观锁的核心特性。
-
重试机制:
- 在 CAS 操作失败时,通常会通过循环重试(如自旋锁)来重新尝试更新。这种重试逻辑与乐观锁的冲突处理方式类似。
4. CAS 和乐观锁的区别
尽管 CAS 可以被看作乐观锁的一种实现形式,但两者并不完全等同:
对比维度 | CAS | 乐观锁 |
---|---|---|
范围 | 是一种具体的实现机制,基于硬件支持的原子操作。 | 是一种抽象的并发控制策略,不一定依赖 CAS。 |
冲突处理 | 通常通过自旋重试解决冲突。 | 冲突处理方式多样(如重试、回滚、抛异常)。 |
适用场景 | 适用于简单的原子操作(如计数器)。 | 适用于更复杂的业务场景(如数据库事务)。 |
性能开销 | 自旋可能导致高 CPU 占用。 | 冲突较少时性能较高,冲突较多时可能效率低。 |
5. 示例代码
CAS 实现的乐观锁
以下是一个使用 AtomicInteger
实现 CAS 的例子,模拟乐观锁的行为:
import java.util.concurrent.atomic.AtomicInteger;
public class CASExample {
private AtomicInteger balance = new AtomicInteger(100);
public void withdraw(int amount) {
while (true) {
int currentBalance = balance.get();
if (currentBalance < amount) {
System.out.println("余额不足,无法取款");
break;
}
// CAS 更新余额
if (balance.compareAndSet(currentBalance, currentBalance - amount)) {
System.out.println("取款成功,当前余额:" + balance.get());
break;
}
// 如果 CAS 失败,说明有其他线程修改了余额,继续重试
}
}
public static void main(String[] args) {
CASExample account = new CASExample();
Runnable task = () -> account.withdraw(50);
new Thread(task).start();
new Thread(task).start();
}
}
数据库中的乐观锁
以下是数据库中使用版本号实现乐观锁的例子:
-- 假设表中有 version 字段
UPDATE accounts
SET balance = balance - 50, version = version + 1
WHERE id = 1 AND version = 2;
如果 version
不匹配,说明数据已被其他事务修改,更新失败。
6. 总结
- CAS 是乐观锁的一种具体实现,它通过无锁的方式实现了冲突检测和重试机制。
- 乐观锁是一种更广泛的概念,不仅限于 CAS,还可以通过
版本号
、时间戳
等方式实现。 - 在实际应用中,CAS 更适合简单场景(如计数器、状态标志位),而乐观锁则更适合复杂业务场景(如数据库事务)。
因此,虽然可以笼统地将 CAS 视为乐观锁,但需要根据具体场景理解两者的区别和适用范围。
基于 CAS 的实现类
以下是常见的基于 CAS 的实现类及其用途,它们都依赖于 CAS 机制来实现线程安全的原子操作:
1. 基本数据类型的原子类
这些类用于对基本数据类型(如整数、长整型、布尔值等)进行原子操作。
-
AtomicInteger
- 提供对
int
类型的原子操作。 - 示例:
incrementAndGet()
、getAndIncrement()
、compareAndSet(int expect, int update)
、getAndSet(int newValue)
等。
- 提供对
-
AtomicLong
:- 提供对
long
类型的原子操作。 - 示例:
incrementAndGet()
、getAndSet()
等。
- 提供对
-
AtomicBoolean
:- 提供对
boolean
类型的原子操作。 - 示例:
compareAndSet(true, false)
可以原子地将true
改为false
。
- 提供对
-
AtomicReference<V>
:- 提供对引用类型的原子操作。
- 示例:可以原子地更新一个对象引用。
2. 数组类型的原子类
这些类用于对数组中的元素进行原子操作。
-
AtomicIntegerArray
:- 提供对
int[]
数组的原子操作。 - 示例:
getAndIncrement(int i)
可以原子地增加数组中某个位置的值。
- 提供对
-
AtomicLongArray
:- 提供对
long[]
数组的原子操作。
- 提供对
-
AtomicReferenceArray<E>
:- 提供对对象数组的原子操作。
3. 带版本号或标记位的原子类
这些类解决了 CAS 中的 ABA 问题(即变量从 A 变为 B 再变回 A 的情况)。
-
AtomicStampedReference<V>
:- 在引用的基础上引入了一个“版本号”字段。
- 示例:
compareAndSet(expectedReference, newReference, expectedStamp, newStamp)
。
-
AtomicMarkableReference<V>
:- 在引用的基础上引入了一个“标记位”字段。
- 示例:
compareAndSet(expectedReference, newReference, expectedMark, newMark)
。
4. 高性能计数器
在高并发场景下,传统的原子类(如 AtomicLong
)可能会因为自旋重试导致性能下降。为此,Java 提供了以下高性能计数器类:
-
LongAdder
:- 提供高效的加法操作,适用于频繁更新但较少读取的场景。
- 内部通过分段计数的方式减少竞争。
-
DoubleAdder
:- 类似于
LongAdder
,但适用于double
类型。
- 类似于
5. 其他高级工具
-
Striped64
:- 是
LongAdder
和DoubleAdder
的基类,提供了分段计数的通用实现。
- 是
-
AtomicIntegerFieldUpdater<T>
:- 提供对指定类的
volatile int
字段的原子更新功能。
- 提供对指定类的
-
AtomicLongFieldUpdater<T>
:- 提供对指定类的
volatile long
字段的原子更新功能。
- 提供对指定类的
-
AtomicReferenceFieldUpdater<T, V>
:- 提供对指定类的
volatile
引用字段的原子更新功能。
- 提供对指定类的
CAS 的核心实现方式
所有的上述类都依赖于 Unsafe
类提供的底层 CAS 方法。例如:
Unsafe.compareAndSwapInt(Object o, long offset, int expected, int update)
:- 对
int
类型的字段执行 CAS 操作。
- 对
Unsafe.compareAndSwapLong(Object o, long offset, long expected, long update)
:- 对
long
类型的字段执行 CAS 操作。
- 对
Unsafe.compareAndSwapObject(Object o, long offset, Object expected, Object update)
:- 对引用类型的字段执行 CAS 操作。
如果你需要处理简单的整数操作,可以选择 AtomicInteger
;如果需要高性能计数器,可以使用 LongAdder
;如果需要解决 ABA 问题,可以使用 AtomicStampedReference
。
AtomicInteger源码解读
AtomicInteger
是 Java 并发包 java.util.concurrent.atomic
中的一个类,用于在多线程环境下提供原子操作的整数类型。它的核心实现依赖于 CAS(Compare-And-Swap) 操作,这是硬件级别的支持机制,能够实现无锁的线程安全操作。
1. 基本结构
AtomicInteger
的核心是一个 volatile
修饰的整型变量,确保其可见性和有序性
:
private volatile int value;
volatile
:保证了变量的可见性,即一个线程对value
的修改对其他线程立即可见。int value
:存储当前的整数值。
此外,AtomicInteger
还使用了 Unsafe
类提供的底层方法来实现 CAS 操作。
2. 核心方法解析
(1) get()
方法
public final int get() {
return value;
}
- 直接返回当前的
value
值。 - 因为
value
是volatile
的,所以读取时总是能获取到最新的值
。
(2) compareAndSet(int expect, int update)
方法
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
- 作用:尝试将
value
从expect
更新为update
,如果当前值等于expect
,则更新成功;否则失败。 - 实现细节:
unsafe.compareAndSwapInt
是一个底层方法,直接调用 CPU 的 CAS 指令(如 x86 架构下的CMPXCHG
)。valueOffset
是value
字段在内存中的偏移量,通过Unsafe
获取。- CAS 操作是原子性的,不会被其他线程打断。
(3) incrementAndGet()
方法
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
- 作用:将
value
增加 1,并返回增加后的值。 - 实现细节:
- 调用了
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
),不断尝试 CAS 操作,直到更新成功为止。 - 如果多个线程同时竞争,可能会导致多次重试,但最终总能成功。
- 调用了
(4) getAndIncrement()
方法
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
- 作用:将
value
增加 1,并返回增加前的值。 - 实现细节:
- 同样调用了
Unsafe
的getAndAddInt
方法。
- 同样调用了
3. 乐观锁的核心思想
AtomicInteger
实现乐观锁的核心在于 CAS 操作 和 自旋重试机制:
-
CAS 操作:
- CAS 是一种
无锁算法
,基于硬件指令实现。 - 它通过
比较当前值与预期值是否一致
来决定是否更新,避免了传统锁的开销。
- CAS 是一种
-
自旋重试:
- 当多个线程同时尝试更新同一个
AtomicInteger
时,只有一个线程会成功,其他线程会进入自旋重试。 - 自旋的优点是无需阻塞线程,减少了上下文切换的开销。
- 当多个线程同时尝试更新同一个
4. 示例代码
以下是一个简单的例子,展示 AtomicInteger
在多线程环境下的使用:
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicIntegerExample {
private static AtomicInteger counter = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
counter.incrementAndGet(); // 原子性地增加计数器
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Final Counter Value: " + counter.get());
}
}
输出结果:
Final Counter Value: 2000
- 两个线程分别对
counter
增加 1000 次,最终结果是 2000,证明了AtomicInteger
的线程安全性。
5. 优点与局限性
优点
- 高性能:
- CAS 操作比传统的锁(如
synchronized
)更高效,尤其是在低竞争场景下。
- CAS 操作比传统的锁(如
- 无锁设计:
- 不需要显式加锁,减少了线程阻塞和上下文切换的开销。
- 简单易用:
- 提供了丰富的原子操作方法,简化了并发编程。
局限性
- 高竞争场景下的性能问题:
- 在高并发场景下,CAS 可能会导致大量线程自旋重试,增加 CPU 开销。
- ABA 问题:
- 如果变量的值从 A 变为 B,再变回 A,CAS 会误认为没有变化。
- 解决方案:使用
AtomicStampedReference
或AtomicMarkableReference
,引入版本号或标记位。
6. 总结
AtomicInteger
通过 CAS 操作 和 自旋重试机制 实现了乐观锁,能够在多线程环境下提供高效的原子操作。它适用于低竞争的场景,但在高竞争场景下可能需要结合其他工具(如锁)来优化性能。理解 AtomicInteger
的工作原理有助于更好地掌握 Java 并发编程的核心思想。
有关于CAS的面试题
CAS(Compare and Swap)是高频面试题的一个热门话题,尤其在多线程和并发编程方面经常被问及。
什么是CAS操作?
CAS是一种
乐观锁机制
,用于实现多线程环境下的原子操作。
它通过比较共享变量的当前值与期望值是否相等
,如果相等
则将共享变量的值更新为新值
。
CAS是一种非阻塞算法
,可以避免传统锁机制带来的性能开销和线程阻塞。
CAS操作的基本步骤是什么?
- 获取当前共享变量的值和期望值。
- 比较共享变量的当前值和期望值是否相等,如果相等则更新为新值。
- 如果当前值与期望值不相等,说明有其他线程已经修改了共享变量的值,需要重新获取最新值并重复步2。
在Java中,CAS操作由哪些类提供支持?
CAS操作由java.util.concurrent.atomic包下的类提供支持,主要包括
AtomicInteger
、AtomicLong
、AtomicReference
等。这些类提供了原子性的CAS操作方法。
CAS操作在并发编程中有什么优点?
● 确保共享变量的原子性操作,避免了数据不一致的问题。
● CAS是非阻塞的,不会导致线程阻塞和上下文切换,提高了性能
● 它适用于高并发环境,如数据库事务、分布式系统等,提供了高度的并发度。
CAS操作存在哪些问题?
其中最常见的是ABA问题。
ABA问题指的是,一个共享变量的值从A变为B,然后再从B变回A,这样CAS操作可能会错误地认为没有其他线程修改过值。
为了解决ABA问题,可以使用带有版本号的CAS操作。
什么是CAS操作的ABA问题?如何避免?
它指的是在CAS操作期间,共享变量的值由A变为B,然后再从B变回A。这可能导致CAS操作错误地认为没有其他线程修改过值。
为了避免ABA问题,可以使用 版本号或标记(一般加时间戳比较保险) 来跟踪共享变量的变化,确保CAS操作同时检查值和版本号。
CAS操作和互斥锁有何不同 ?
CAS操作和互斥锁的主要不同在于:
- CAS是一种
乐观锁
,不会导致线程阻塞,而互斥锁是一种悲观锁
,可能导致线程阻塞。- CAS操作是
非阻塞的
,而互斥锁需要等待资源释放
。- CAS操作通常用于
高并发环境
,互斥锁用于临界区的互斥访问
。
在哪些应用场景中可以使用CAS操作?
CAS操作适用于多种高并发应用场景,包括但不限于:
- 数据库事务:用于实现乐观锁机制,避免死锁和性能问题。
- 分布式系统:CAS可以用于分布式锁、分布式数据同步等。
- 线程安全的数据结构:CAS可用于实现线程安全的队列、栈、集合等数据结构。JDK1.8 中的 ConcurrentHashMap 使用 CAS 和 synchronized 两种机制来实现线程安全。