有一个同学问我什么是JUC:JUC 是 java.util.concurrent
包的缩写。这个包是 Java 专门为处理多线程并发编程而提供的一个“工具箱”。
原子类
定义
原子类(Atomic Classes) 是 Java 在 java.util.concurrent.atomic
包下提供的一组工具类,用于在多线程环境下,无需使用锁(如 synchronized
)即可实现单个变量操作的原子性、线程安全性和内存可见性
分类
JUC包下的原子类
1. 基本类型原子类
用于通过 CAS 操作原子性地更新基本类型。
AtomicInteger
: 原子更新整型。(最常用)AtomicLong
: 原子更新长整型。AtomicBoolean
: 原子更新布尔类型。
2. 数组类型原子类
用于原子性地更新数组里的某个元素。
AtomicIntegerArray
: 原子更新整型数组里的元素。AtomicLongArray
: 原子更新长整型数组里的元素。AtomicReferenceArray
: 原子更新引用类型数组里的元素。
3. 引用类型原子类
AtomicReference
: 原子更新引用类型。可以用于实现诸如自旋锁、缓存等数据结构。AtomicStampedReference
: 原子更新引用类型,内部通过一个int
类型的版本号(Stamp) 来解决 CAS 操作中的 ABA 问题。AtomicMarkableReference
: 原子更新引用类型,内部通过一个boolean
类型的标记来表示该引用是否被修改过。它是AtomicStampedReference
的一个简化版,不关心修改次数,只关心是否被修改过。
4. 字段更新器(Updater)
以一种线程安全的方式原子性地更新某个类的特定 volatile 字段。这些字段不需要是 AtomicXXX
对象,可以是普通的成员变量。使用相对较少,主要用于性能极致的场景。
AtomicIntegerFieldUpdater
AtomicLongFieldUpdater
AtomicReferenceFieldUpdater
通俗理解
原子类封装了一个 volatile 变量,并通过 硬件级别的 CAS (Compare-And-Swap) 指令 来保证对该变量进行“读-改-写”操作的原子性。
CAS
CAS 的全称是 Compare-And-Swap(比较并交换)。它是一种无锁的、乐观的原子算法。
它的核心思想:“我认为值应该是A,如果是的话,就把它改成B;如果不是A(说明被别人改过了),那我就不修改了,然后告诉我现在的值是多少。”
核心原理
CAS 操作需要三个操作数:
V: 要读写的内存位置(例如,一个变量的地址)
A: 预期的原始值(你认为这个内存位置当前的值应该是什么)
B: 想要写入的新值
算法流程:
检查内存位置
V
的值是否与预期值A
相等。如果相等,处理器会自动将该位置的值更新为新值
B
。如果不相等,说明有其他线程修改了
V
,本次操作不做任何修改(或者可以选择重试)。无论是否修改成功,都会返回
V
的当前实际值。
优点:
高性能:在没有激烈竞争的情况下,开销远小于悲观锁(因为它避免了线程挂起和上下文切换)。
无阻塞:线程永远不会被挂起,如果失败可以立即重试或做其他操作。
缺点:
ABA 问题:如果一个值原来是 A,变成了 B,后来又变回了 A。CAS 在“比一比”时会发现它没变,于是操作成功,但其实它中间已经发生过变化。(可以用
AtomicStampedReference
加版本号解决)。循环时间长开销大:如果竞争非常激烈,CAS 一直失败,线程会不停重试,消耗 CPU 资源。
只能保证一个变量的原子性:只能对一个变量进行原子操作,不能保证多个变量共同操作的原子性(但可以合并成一个对象再用
AtomicReference
来保证)。
理解示例
操作i++操作
1、现成不安全时:
public class UnsafeCounter {
private int count = 0;
public void increment() {
count++; // 这不是原子操作!
}
public int getCount() {
return count;
}
}
2、线程安全、能保证原子性,但是性能损耗
public class SynchronizedCounter {
private int count = 0;
public synchronized void increment() { // 加锁,保证原子性
count++;
}
public synchronized int getCount() { // 读操作也需要加锁保证可见性
return count;
}
}
3、乐观锁/CAS
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicCounter {
// 定义:一个 AtomicInteger 类型的原子变量
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
// 底层使用 CAS 保证原子性
count.incrementAndGet(); // 相当于 ++count
}
public int getCount() {
// 直接返回,因为内部 volatile 保证了可见性
return count.get();
}
}
原子类 = volatile
+ CAS
核心是它通过 volatile
保证了可见性,通过 CAS 操作保证了原子性,两者结合最终实现了无锁化的线程安全。
底层硬件实现
CAS 并非通过软件(如Java代码)实现,它的原子性依赖于计算机硬件。
主要实现方式有两种:
总线锁:早期处理器通过在总线上发出一个 LOCK# 信号,锁定整个内存系统,阻止其他处理器或核心访问内存。这种方式锁的粒度太粗,性能开销大。
缓存锁(MESI协议):现代处理器更常用的方式。它不锁总线,而是基于缓存一致性协议(如 Intel 的 MESI 协议)来保证原子性。
MESI 定义了缓存行(Cache Line)的四种状态:Modified(修改)、Exclusive(独占)、Shared(共享)、Invalid(无效)。
当某个CPU核心要执行CAS操作时,它会锁定自己缓存中的对应缓存行。
如果它发现缓存行的状态表明数据是独占的(E),并且值等于期望值A,它就可以安全地更新缓存行为新值B,并将状态置为M。
如果它发现数据是共享的(S),或者值不等于A,说明有其他核心也在使用这个数据,本次CAS操作就会失败。
核心要点: 无论哪种方式,最终都通过CPU提供的一条机器指令(如 x86 架构下的 CMPXCHG
指令)来完成这个比较和交换的操作。JVM 只是调用了这条指令的包装。
悲观锁和乐观锁对比
特性 | 悲观锁(sch) | 乐观锁 |
---|---|---|
基本思想 | “悲观”地认为并发冲突一定会发生。因此,在操作数据之前,会先加锁,确保在整个操作过程中,数据不会被其他线程修改。 | “乐观”地认为并发冲突很少发生。因此,不会直接加锁,而是在提交更新时,才检查数据在此期间是否被其他线程修改过。 |
类比 | 就像独占写文章。你认为只要自己离开座位,别人就一定会来改动你的文章。所以你在写的时候就把门锁上,不让任何人进来,直到你写完才开门。 | 就像协作编辑文档(如Google Docs)。你和同事都可以同时编辑。你们各自保存时,系统会检查自你打开文档后是否有其他人的修改。如果有,它会提示你冲突并让你解决。 |
实现机制 | 依靠数据库或语言的原生锁机制,如:行锁、表锁、读写锁、synchronized 关键字等。 |
通常通过数据版本号(Version) 或时间戳(Timestamp) 实现。 |
工作流程 | 1. 开始事务 2. 申请并获得锁 3. 进行业务操作 4. 提交事务并释放锁 |
1. 读取数据,并记录版本号 V1 2. 进行业务操作(不加锁) 3. 提交更新时,检查当前版本号是否仍为 V1 - 如果是:提交成功,并更新版本号(如 V1+1) - 如果不是:提交失败,进行重试或抛出异常 |