CAS小窥
在JDK 1.5之后Java提供了并发包(java.util.concurrent
),加强对并发的支持。该包下的类大量使用 CAS(Compare and Swap)来实现原子操作、锁优化等。在java.util.concurrent.atomic
包中,提供了对基本类型的原子封装,比如AtomicInteger,用来原子性访问和更新。我们十分熟悉的++i操作,在多线程环境下并非是线程安全的,因为它包含取值、相加与赋值三步操作,所以整体不是原子操作。
CAS是Java中所谓lock-free的基础。CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。CAS的描述如下(返回布尔值是为了让调用者知道是否更新成功):
public boolean compareAndSwap(int destValue, int expectedValue, int newValue) { if (destValue == expectedValue) { destValue = newValue; return true; } return false; }
那么在JDK源码中是如何使用CAS的呢?我们先来看看AtomicInteger(JDK10)的源码实现。在JDK10版本中,CAS这部分操作是在jdk.internal.misc.Unsafe
这个类中提供的,所以在AtomicInteger源码中,自增一是调用了Unsafe 的 getAndAddInt 方法。代码如下:
private static final jdk.internal.misc.Unsafe U = jdk.internal.misc.Unsafe.getUnsafe(); private static final long VALUE = U.objectFieldOffset(AtomicInteger.class, "value"); private volatile int value; public final int incrementAndGet() { return U.getAndAddInt(this, VALUE, 1) + 1; }
VALUE
代表AtomicInteger对象value成员变量在内存中的偏移量,因为Unsafe是根据内存偏移地址获取数据的。value
代表当前值,使用volatile修饰,保证多线程环境下值一致。
Unsafe这个类比较厉害,提供一些低级、不安全操作的方法, JDK不推荐我们直接调用这个类。下面getAndAddInt的代码:
@HotSpotIntrinsicCandidate public final int getAndAddInt(Object o, long offset, int delta) { int v; do { v = getIntVolatile(o, offset); } while (!weakCompareAndSetInt(o, offset, v, v + delta)); return v; }
方法的执行流程如下:
- 假设现在有两个线程同时进入getAndAddInt方法, 主内存的value假设为2,根据Java内存模型,线程A和线程B各自持有一份value的副本,值为2;
- 线程A 执行
getIntVolatile(o, offset)
方法,拿到value值为2,线程A被挂起了; - 线程B执行
getIntVolatile(o, offset)
方法,拿到value值为2, 接着执行weakCompareAndSetInt(o, offset, v, v + delta)
方法,发现内存值和获取的值都是2,成功修改value的值为1; - 线程A继续执行
weakCompareAndSetInt(o, offset, v, v + delta)
方法,发现自己获取的值2和内存中的值1不一样了,修改失败,继续循环执行 - 继续上面同样的操作,由于value被volatile修饰,所以此时线程A的value的副本就为1了,执行
getIntVolatile(o, offset)
方法,获取的值也为1,最后执行weakCompareAndSetInt(o, offset, v, v + delta)
,直至成功修改value的值。
weakCompareAndSetInt
最终调用compareAndSetInt
方法,注意该方法为本地方法,如下。该方法有四个参数,分别是对象(这里是AtomicInteger)、对象的偏移地址、预期值、修改值。
@HotSpotIntrinsicCandidate public final native boolean compareAndSetInt(Object o, long offset, int expected, int x);
该方法根据操作系统的不同有不同的实现。在openjdk调用的c++代码为:unsafe.cpp。。我也看不懂
ABA问题
从上面的分析可以看出,CAS涉及到修改值操作,如果一个值先被修改然后再修改为原值,那么就出现了ABA问题。具体流程如下:
- 线程P1读取指定内存的值为A
- 线程P1被挂起,线程P2运行
- 线程P2将指定内存的值从A修改为B,再改回A。
- 再次调度到线程P1
- 线程P1发现指定内存的值没有变,于是继续执行。
java.util.concurrent.atomic
包中,JDK提供了AtomicStampedReference,在该类中提供stamp来记录每次对值修改的操作,通过判断stamp来解决ABA问题的发生。示例如下:
/** * 使用版本号解决CAS中的ABA问题。 * 每一次修改都记录下版本号, 此版本号+1. * */ public class AtomicStampedReferenceDemo { private static AtomicStampedReference<Integer> atomicStampedRef = new AtomicStampedReference<Integer>(1, 0); public static void main(String[] args){ Thread main = new Thread(() -> { System.out.println("操作线程" + Thread.currentThread() +", 初始值 a = " + atomicStampedRef.getReference()); // 获取当前Stamp final int stamp = atomicStampedRef.getStamp(); try { // 等待2秒 ,以便让干扰线程执行 Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } // 此时stamp已经被修改了, 所以CAS失败 boolean isCASSuccess = atomicStampedRef.compareAndSet(1, 2, stamp, stamp + 1); System.out.println("操作线程" + Thread.currentThread() +", CAS操作结果: " + isCASSuccess); }, "主操作线程"); Thread other = new Thread(() -> { // 干扰线程让出自己的cpu时间片,回到Runnable状态,让自己或主操作线程先执行 Thread.yield(); // 加一 atomicStampedRef.compareAndSet(1, 2, atomicStampedRef.getStamp(),atomicStampedRef.getStamp() + 1); System.out.println("操作线程" + Thread.currentThread() + ", 【increment】, Reference = " + atomicStampedRef.getReference() + ", Stamp = " + atomicStampedRef.getStamp()); // 然后减一 atomicStampedRef.compareAndSet(2, 1, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1); System.out.println("操作线程" + Thread.currentThread() + ", 【decrement】, Reference = " + atomicStampedRef.getReference() + ", Stamp = " + atomicStampedRef.getStamp()); }, "干扰线程"); main.start(); other.start(); } }
执行结果为:
操作线程Thread[主操作线程,5,main], 初始值 a = 1 操作线程Thread[干扰线程,5,main], 【increment】, Reference = 2, Stamp = 1 操作线程Thread[干扰线程,5,main], 【decrement】, Reference = 1, Stamp = 2 操作线程Thread[主操作线程,5,main], CAS操作结果: false
Variable Handles
在JDK9之前,JDK源码中在进行CAS操作时需要调用sun.misc.Unsafe
类native修饰的方法,该类由于涉及到底层操作被JDK视为不安全,不推荐使用,在以后的版本中会被逐渐替代,所以在JDK9引入了Variable Handles(变量句柄)这个概念,主要提供java.util.concurrent.atomic
和 sun.misc.Unsafe
相似的功能,但会更加安全和易用,并且在并发方面提高了性能。详细参考Variable Handles(变量句柄)
参考:
- Non-blocking algorithm
- The java.util.concurrent Synchronizer Framework
- 无锁程序设计
- JAVA CAS原理深度分析
- JAVA中CAS-ABA的问题解决方案AtomicStampedReference
- 杨晓峰,AtomicInteger底层实现原理是什么?如何在自己的产品代码中应用CAS操作?
title: CAS小窥
tags: [CAS,java,JUC]
author: Mingshan
categories: [Java, JUC]
date: 2018-10-01
本文来自博客园,作者:mingshan,转载请注明原文链接:https://www.cnblogs.com/mingshan/p/17793511.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)