Java并发基础:深入理解JMM、volatile关键字与CAS机制
在Java并发编程中,线程安全是核心挑战之一。Java内存模型(JMM)定义了主内存与工作内存的关系,由此衍生出可见性、原子性和有序性问题。volatile关键字作为轻量级同步机制,能有效解决可见性与有序性,但不具备原子性保障。本文将结合代码实例,解析volatile的工作原理,并深入探讨CAS机制及其引发的ABA问题解决方案。
JMM的核心概念
Java内存模型(Java Memory Model,简称JMM)并非指代堆、栈或方法区等物理内存布局,而是规范了虚拟机中各项变量(共享数据)的访问规则。JMM规定了所有变量都存储在主内存(Main Memory)中,每个线程拥有独立的工作内存(Working Memory),线程对变量的操作必须在工作内存中进行,不可直接读写主内存。
这种架构设计虽然提升了性能,但在多线程环境下容易引发三大问题:
- 可见性:当一个线程修改了共享变量后,其他线程是否能立即感知。在串行程序中此问题不存在,但在并行程序中,若未及时同步到主内存,其他线程可能读取到过期的副本值。
- 原子性:指操作不可中断。例如对全局变量赋值,若操作被拆分为多个步骤,中间可能被其他线程干扰,导致数据不一致。简单的如
int i赋值通常具有原子性,但复合操作如自增则不然。 - 有序性:单线程下指令按顺序执行,但编译器或CPU优化可能导致指令重排序。这在并发环境下若未加控制,可能破坏依赖关系,导致程序执行结果异常。
volatile关键字的作用与局限
volatile是Java提供的轻量级同步机制,它主要保证了两个特性:可见性和有序性(禁止指令重排),但不保证原子性。
可见性测试演示
通过以下示例可直观观察volatile对可见性的影响。定义一个资源类MyData,其中包含普通整型变量number和AtomicInteger对象。
class MyData {
int number = 0;
// volatile int number = 0;
AtomicInteger atomicInteger = new AtomicInteger();
public void setTo60() {
this.number = 60;
}
public void addPlusPlus() {
number++;
}
public void addAtomic() {
atomicInteger.getAndIncrement();
}
}
在可见性测试中,启动线程AAA将number设为60,而main线程循环检测number == 0。
private static void volatileVisibilityDemo() {
System.out.println("可见性测试");
MyData myData = new MyData();
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t come in");
try { TimeUnit.SECONDS.sleep(3); myData.setTo60(); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println(Thread.currentThread().getName() + "\t update number value: " + myData.number);
}, "AAA").start();
while (myData.number == 0) { /* main线程持有拷贝,一直为0 */ }
System.out.println(Thread.currentThread().getName() + "\t mission is over. main get number value: " + myData.number);
}
如果number未加volatile修饰,main线程因持有旧值0而陷入死循环,程序无法结束。一旦添加volatile,线程AAA修改的值会立即刷新至主内存,main线程能立刻检测到变化并退出循环,证明了其可见性保证。
原子性问题与解决方案
volatile无法保证复合操作的原子性。以number++为例,它在字节码层面分解为多条指令:
getfield // 读当前值
iconst_1 // 常量1
iadd // 加法运算
putfield // 写回新值
当多个线程同时执行此操作时,可能出现A线程读到0,B线程也读到0,各自加1后写回,最终结果仅为1而非2。为解决此问题,可采用以下方案:
- 对方法使用
synchronized或ReentrantLock加锁。 - 使用
java.util.concurrent.atomic.AtomicInteger类,利用CAS机制实现无锁原子操作。
在并发压力测试中(20个线程各执行1000次自增),普通int类型因覆盖问题结果常小于20000,而AtomicInteger能准确达到20000。
原子性测试
main int type finally number value: 17542
main AtomicInteger type finally number value: 20000
有序性保证
volatile通过插入内存屏障禁止指令重排序,确保语义正确的代码按预期执行。例如在单例模式的DCL(Double Check Lock)模式中,实例化对象new SingletonDemo()可分为分配内存、初始化对象、设置引用三步。若无volatile,可能发生1-3-2的重排序,导致其他线程获取到未初始化的“半初始化”对象,引发错误。声明实例变量为volatile可有效防止此类重排序。
CAS机制深度解析
CAS(Compare And Swap,比较并交换)是实现无锁并发的重要思想。其逻辑为:若主内存值等于期望值,则更新为新值;否则重试。这解释了为何AtomicInteger无需synchronized也能保证线程安全。
AtomicInteger内部维护了volatile int value和Unsafe对象。其getAndIncrement方法调用Unsafe.getAndAddInt,底层通过无限循环尝试CAS操作:
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while (!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
CAS的缺点与ABA问题
CAS虽高效,但存在以下局限:
- 循环开销大:长时间竞争失败会导致自旋消耗CPU资源。
- 仅限变量级:只能保证单个变量的原子操作,多个变量需整体加锁。
- ABA问题:线程T1将值从A改为B再改回A,线程T2检查时发现值仍为A,误认为未变化,从而完成CAS。但这掩盖了中间过程的修改,可能在需要关注过程完整性的场景(如链表操作)引发逻辑错误。
为解决ABA问题,Java提供了AtomicStampedReference和AtomicReference。AtomicStampedReference引入版本号(Stamp),不仅比较值,还比较版本号。只有值和版本都一致才执行更新。
// ABA问题示例:t1将100->101->100,t2检测100->2019成功,尽管值变了但CAS认为没变
static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
// 解决ABA:增加版本号
static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);
在使用compareAndSet时,需传入旧期望值、新值、旧版本号和新的版本号。通过版本号的递增,即使值发生“A-B-A”的变化,版本号也会改变,从而被识别并拒绝更新。
总结
Java并发编程的基础在于理解JMM带来的可见性、原子性和有序性问题。volatile是解决可见性和有序性的轻量级工具,但需注意其非原子性缺陷。在处理简单计数器时,AtomicInteger凭借CAS机制提供了高效的无锁并发支持,但在面对复杂状态变更时,需警惕ABA问题,此时应借助AtomicStampedReference等带版本号的原子类来确保逻辑严谨。以上就是Java中JMM与volatile关键字及相关并发工具的详细内容,更多关于Java多线程编程的资料请关注本站其它相关文章!
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













