1.CAS是什么(CompareAndSet)
CAS(Compare and swap)比较和替换是设计并发算法时用到的一种技术。简单来说,比较和替换是使用一个期望值和一个变量的当前值进行比较,如果当前变量的值与我们期望的值相等,就使用一个新值替换当前变量的值。
2.CAS的使用场景(原子类)
我们先看看下面的例子
package com.atguigu.springcloud.test; import org.omg.CORBA.Current; /** * @Classname Demo * @Description TODO * @Date 2021/4/25 0025 下午 3:25 * @Created by jcc */ public class Demo { int i = 0; public static void main(String[] args) { Demo d = new Demo(); new Thread(()->{ while (d.i < 100){ int j = d.i; System.out.println(Thread.currentThread().getName() + "-j-" + j + "-i-" + ++d.i); } },"aa").start();; new Thread(()->{ while (d.i < 100){ int j = d.i; System.out.println(Thread.currentThread().getName() + "-j-" + j + "-i-" + ++d.i); } },"bb").start(); } }
//一次的输出结果
bb-j-0-i-1
bb-j-2-i-3
aa-j-0-i-2
aa-j-4-i-5
aa-j-5-i-6
aa-j-6-i-7
aa-j-7-i-8
........
我们期望输出的结果i和j相差为1
看这个的输出结果的第三行 aa-j-0-i-2 输出的j的值是0,i的值是2
说明aa线程最开始获取到的i的值是0,而在++i的操作时,i已经被bb线程变为1了,所以++i的输出结果是2
如果我们想要aa线程和bb线程的j的值和++i的值必须是相差为1,也就是说,一个线程在对i进行操作的过程中不能被另外一个线程干扰
方法1,加锁
package com.atguigu.springcloud.test; import org.omg.CORBA.Current; /** * @Classname Demo * @Description TODO * @Date 2021/4/25 0025 下午 3:25 * @Created by jcc */ public class Demo { int i = 0; public static void main(String[] args) { Demo d = new Demo(); new Thread(()->{ synchronized (Demo.class){ while (d.i < 100){ int j = d.i; System.out.println(Thread.currentThread().getName() + "-j-" + j + "-i-" + ++d.i); } } },"aa").start();; new Thread(()->{ synchronized (Demo.class){ while (d.i < 100){ int j = d.i; System.out.println(Thread.currentThread().getName() + "-j-" + j + "-i-" + ++d.i); } } },"bb").start(); } }
方法2,使用原子类
package com.atguigu.springcloud.test; import org.omg.CORBA.Current; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; /** * @Classname Demo * @Description TODO * @Date 2021/4/25 0025 下午 3:25 * @Created by jcc */ public class Demo1 { AtomicInteger in = new AtomicInteger(); //默认值是0 public static void main(String[] args) { Demo1 d = new Demo1(); new Thread(()->{ while (d.in.get() < 100){ int j = d.in.get(); int next = j + 1; try { TimeUnit.MILLISECONDS.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } while (!d.in.compareAndSet(j,next)){ System.out.println(Thread.currentThread().getName() + "-false -j-" + j + "-i-" + d.in.get()); j = d.in.get(); next = j + 1; } System.out.println(Thread.currentThread().getName() + "true -j-" + j + "-i-" + d.in.get()); } },"aa").start();; new Thread(()->{ while (d.in.get() < 100){ int j = d.in.get(); int next = j + 1; try { TimeUnit.MILLISECONDS.sleep(110); } catch (InterruptedException e) { e.printStackTrace(); } while (!d.in.compareAndSet(j,next)){ System.out.println(Thread.currentThread().getName() + "-false -j-" + j + "-i-" + d.in.get()); j = d.in.get(); next = j + 1; } System.out.println(Thread.currentThread().getName() + "-true -j-" + j + "-i-" + d.in.get()); } },"bb").start(); } }
这里面最关键的代码
while (!d.in.compareAndSet(j,next)){
System.out.println("false -j-" + j + "-i-" + d.in.get());
j = d.in.get();
next = j + 1;
}
compareAndSet这个方法的意思是 把j的值和主线程中in的值进行对比,如果一致,则in的值变为next返回true,否则不进行的操作,返回false,这里就是用到了CAS。而外面加上while,则是采用了自旋锁的思想。当j的值和in在主内存中的值不一致时,重新把j的值赋值为主内存中in的值,再调用compareAndSet方法,知道j和in的值一致
看下面部分输出结果
aatrue -j-0-i-1
bb-false -j-0-i-1
bb-true -j-1-i-2
aa-false -j-1-i-2
aatrue -j-2-i-3
......
看着可输出结果
aatrue -j-0-i-1 aa线程获取in的值是0赋值给j,compareAndSet比较j和in的值一样,可以,把next的值赋值给in
bb-false -j-0-i-1 aa线程获取in的值是0赋值给j,compareAndSet比较j和in的值不一样,此时in已经变为1了,所以不把next的值赋值给in。
bb-true -j-1-i-2 把in的值1重新赋值给j,i+1赋值给next,再去调用compareAndSet,此时j=1,in的值也为1,可以,把next的值赋值给in
3.关于compareAndSet方法详解
3.1 源码
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value; //AtomicInteger 的变量value被volatile修饰,所有线程可见
/**
* Atomically sets the value to the given updated value
* if the current value {@code ==} the expected value.
*
* @param expect the expected value
* @param update the new value
* @return {@code true} if successful. False return indicates that
* the actual value was not equal to the expected value.
*/
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
}
3.2 compareAndSwapInt
在compareAndSet方法中,调用的是unsafe类的compareAndSwapInt(this, valueOffset, expect, update)方法
参数1:this,要操作的对象
参数2:valueOffset,要操作的对象属性地址的偏移量,因为Unsafe就是根据内存偏移地址获取数据的
参数3:expect期待值
参数4:expect,要修改的新值
compareAndSwapInt方法是底层的方法,就是用来比较内存中的值和期望的值是否一致,如果一致,把update的值赋值给它,返回true,否则不赋值,返回false
4 Unsafe类
4.1 简介
Unsafe是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,Java中CAS操作的执行依赖于Unsafe类的方法。注意Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务
Unsafe提供的CAS方法(如compareAndSwapXXX)底层实现即为CPU指令cmpxchg。执行cmpxchg指令的时候,会判断当前系统是否为多核系统,如果是就给总线加锁,只有一个线程会对总线加锁成功,加锁成功之后会执行cas操作,也就是说CAS的原子性实际上是CPU实现的, 其实在这一点上还是有排他锁的,只是比起用synchronized, 这里的排他时间要短的多, 所以在多线程情况下性能会比较好。它通过硬件保证了比较-更新的原子性。它是非阻塞的且自身原子性,也就是说这玩意效率更高且通过硬件保证,说明这玩意更可靠。
调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。
5 自旋锁
尝试获取锁的线程不会阻塞,而是采用循环的方式去尝试获取锁, 好处是减少上下文的切换时间,坏处是循环会占用CPU
下面是一个自旋锁的实现
package com.atguigu.springcloud.test; import org.omg.CORBA.Current; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; /** * @Classname ZiXuanSuoDemo * @Description TODO * @Date 2021/4/25 0025 下午 3:02 * @Created by jcc */ public class ZiXuanSuoDemo { //自旋锁:尝试获取锁的线程不会阻塞,而是采用循环的方式去尝试获取锁, // 好处是减少上下文的切换时间,坏处是循环会占用CPU //1.原子类-Thread AtomicReference<Thread> atomicReference = new AtomicReference<>(); void myLock(){ Thread thread = Thread.currentThread(); while (!atomicReference.compareAndSet(null,thread)){ } System.out.println(thread.getName() + "成功获取锁"); } void unLock(){ Thread thread = Thread.currentThread(); atomicReference.set(null); System.out.println(thread.getName() + "成功释放锁"); } public static void main(String[] args) { ZiXuanSuoDemo de = new ZiXuanSuoDemo(); new Thread(()->{ System.out.println("AA进来了"); de.myLock();//去获取锁 //成功获取锁后,执行下面的操作 try { System.out.println("Aa执行操作"); TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } de.unLock(); },"Aa").start(); new Thread(()->{ System.out.println("Bb进来了"); de.myLock(); //去获取锁 //成功获取锁后,执行下面的操作 try { System.out.println("Bb执行操作"); TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } de.unLock(); },"Bb").start(); } }
6 CAS中的A-B-A问题
6.1 简介
CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。
比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且线程two进行了一些操作将值变成了B,
然后线程two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后线程one操作成功。
尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。
6.2 解决方案-版本号
AtomicStampedReference有两个参数:值和版本号
compareAndSet方法传入四个参数,期待值,新值,期待版本号,新版本号,期待值和期待版本号都对才会更新新值和新版本号
public class LockTest7 { static AtomicStampedReference<Integer> at = new AtomicStampedReference(100,1); public static void main(String[] args) throws InterruptedException { new Thread(()->{ try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } int stamp = at.getStamp(); at.compareAndSet(100,101,stamp,stamp + 1); int stamp2 = at.getStamp(); at.compareAndSet(101,100,stamp2,stamp2 + 1); }).start(); new Thread(()->{ int stamp = at.getStamp(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } boolean b = at.compareAndSet(100, 101, stamp, stamp + 1); System.out.println("操作是否成功" + b); }).start(); } }
执行结果
操作是否成功false
7 CAS中的线程安全是依靠什么解决的
从这个流程上来看是存在一个问题的:当当前线程判断值相等进去准备赋值的时候,这个值整好被其它线程改变了,那么还是存在问题。
所以需要保证这两个操作的原子性,而原子类也解决了这个问题
public final boolean compareAndSet(V expect, V update) {
return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
}
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
发现它是native方法,是在c++里面实现判断和赋值操作的原子性的。
它是通过下面这条指令实现的
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?