目录
- CAS概念
- 没有使用CAS之前
- 使用CAS之后
- CAS介绍
- 代码案例
- 为什么CAS能保证原子性?
- 1、Unsafe
- 2、offset 偏移量
- 3、变量value用volatile修饰
- 4、自旋保持原子性
- 5、底层汇编语言的具体执行
- 原子引用
- CAS与自旋锁
- 实现一个自旋锁
- CAS缺点及解决
- AtomicStampedRefernce
CAS概念
没有使用CAS之前
多线程不使用原子类保证线程安全i++(基本数据类型)通常要这么写
使用CAS之后
多线程环境,使用原子类保证线程安全i++(基本数据类型),类似于安全锁
上面的代码就可以直接演变成这样,AtomicInteger ,整数原子类
没有加synchronized重锁,在性能上更好,且又保证了原子性
CAS介绍
compare and swap的缩写,中文翻译成比较并交换,实现并发算法时常用到的一种技术。
它包含三个操作数–内存位置、预期原值及更新值。
执行CAS操作的时候,将内存位置的值与预期原值比较:
如果相匹配,那么处理器会自动将该位置值更新为新值
如果不匹配,处理器不做任何操作,多个线程同时执行CAS操作只有一个会成功。
CAS有3个操作数,位置内存值V,旧的预期值A,要修改的更新值B。当且仅当旧的预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做或重来当它重来重试的这种行为成为----自旋!!
为什么说有点类似乐观锁呢,我利用CAS原理举例说明:
一个数据块,里面放了一个数字5,有三个线程A、B、C带着i++任务,要对数据块的数据进行+1操作,假如线程A先到数据块,线程A自身带着一个数字5,这时候就会拿数据块的数字5跟自身数字5做比较,一看都是5,那么线程A就往数据块里面的5加1,此时数据块里面的数据就是6,线程A完成任务后走了,这时候线程B来了,他身上也带着数字5,但是数据块里面变成6,不匹配,线程B不服输,重新从数据块中拿数字(重新拿这个就是一个自旋),这时候重新拿的6跟数据块中的6一致,这时候往数据块的6+1做个任务,把7丢了进去,任务结束(线程B乐观的任务数据块的数据和自己总能保持一致,如果不一致就重新拿,再修改)
以上线程B做了两个事:比较(比较数字5)+交换(丢了个7给数据块),这就是CAS原理
代码案例
把上面的案例转化为代码,可见5数字比较是一样的,那么就返回了7
那如果再重复一次,数据就有不一致了,因为重复部分atomicInteger已经是数值7了,不匹配,所以false
为什么CAS能保证原子性?
CAS是JDK提供的非阻塞原子性操作,它通过硬件保证了比较-更新的原子性它是非阻塞的且自身具有原子性,也就是说这玩意效率更高且通过硬件保证,说明这玩意更可靠。
CAS是一条CPU的原子指令(cmpxchg指令),不会造成所谓的数据不一致问题,Unsafe提供的CAS方法(如compareAndSwapXXX)底层实现即为CPU指令cmpxchg。
执行cmpxchg指令的时候,会判断当前系统是否为多核系统,如果是就给总线加锁,只有一个线程会对总线加锁成功,加锁成功之后会执行cas操作,也就是说CAS的原子性实际上是CPU实现独占的,比起用synchronized重量级锁,这里的排他时间要短很多,所以在多线程情况下性能会比较好。
参数说明:
o:要操作的对象
offset:对象中属性地址的偏移量
expected:需要修改数据的期望值(匹配条件的值)
x:需要修改的新值
那么就从上引出,为什么Unsafe类能实现CAS的功能呢?
1、Unsafe
是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsa类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因此java中CAS操作的执行依赖于Unsafe类的方法。
注意unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务。
2、offset 偏移量
是内存中偏移地址Unsafe是根据内存偏移地址来获取数据的
3、变量value用volatile修饰
保证多线程之间的内存可见性
4、自旋保持原子性
通过AtomicInteger…getAndIncrement()源码可以看到
CAS并发原语体现在java语言中就是sum.misc.Unsafe类中的各个方法,并且原语的执行必须是连续,在执行中不可被打断,所以不会造成数据不一致的问题。
5、底层汇编语言的具体执行
Jnsafe类中的compareAndSwapint,是一个本地方法,该方法的实现位于unsafe.cpp中
- 先想办法拿到变量value在内存中的地址,根据偏移量valueOffset,计算 value 的地址
- 调用 Atomic 中的函数 cmpxchg来进行比较交换,其中参数x是要交换的值 e是要比较的值
- cas成功,返回期望值e,等于e,此方法返回true
- cas失败,返回内存中的value值,不等于e,此方法返回false
JDK提供的CAS机制,在汇编层级会禁止变量两侧的指令优化,然后使用cmpxchg指令比较并更新变量值(原子性)
原子引用
那么既然CAS给我们准备了一些基础原子类去使用,我们可不可以自定义原子类呢?
jdk给我们提供了一个原子引用类AtomicReference,具体使用方法见如下
public static void main(String[] args) {AtomicReference<User> atomicReference = new AtomicReference<>();User a = new User("z3", 22);User b = new User("l3", 24);atomicReference.set(a);System.out.println(atomicReference.compareAndSet(a, b)+"\t "+atomicReference.get().toString());System.out.println(atomicReference.compareAndSet(a, b)+"\t "+atomicReference.get().toString());}
@Data
@AllArgsConstructor
@ToString
class User {String userName;int age;
}
与提供的原子类是有相同方法
CAS与自旋锁
自旋锁(spinlock)
CAS 是实现自旋锁的基础,CAS 利用 CPU 指令保证了操作的原子性,以达到锁的效果,至于自旋呢,看字面意思也很明白,自己旋转。是指尝试获取锁的线程不会立即阻寒,而是采用循环的方式去尝试获取锁,当线程发现锁被占用时,会不断循环判断锁的状态,直到获取。这样的好处是减少线程上下文切换的消耗,缺点是环会消耗CPU
CAS 是实现自旋锁的基础,自旋翻译成人话就是循环,一般是用一个无限循环实现。这样一来,一个无限循环中,执行一个CAS 操作
当操作成功返回 true 时,循环结束:
当返回 false 时,接着执行循坏,继续尝试CAS 操作,直到返回true。
实现一个自旋锁
AtomicReference<Thread> atomicReference = new AtomicReference<>();public void lock() {Thread thread = Thread.currentThread();System.out.println(Thread.currentThread().getName() + "\t ---come in");while (!atomicReference.compareAndSet(null, thread)) {}}public void unlock() {Thread thread = Thread.currentThread();atomicReference.compareAndSet(thread,null);System.out.println(Thread.currentThread().getName() + "\t ---task over,unlock...");}public static void main(String[] args) {final Demo demo = new Demo();new Thread(()->{demo.lock();//暂停几秒try {TimeUnit.SECONDS.sleep(2);} catch (Exception e) {e.printStackTrace();}demo.unlock();},"A").start();//让a先启动try {TimeUnit.MILLISECONDS.sleep(500);} catch (Exception e) {e.printStackTrace();}new Thread(()->{demo.lock();demo.unlock();},"B").start();}
效果图,b没有等到a结束就一直在自旋等待a
CAS缺点及解决
-
循环时间开销很大(do while )
-
引出ABA问题
例子:财务记账,A财务记账时账上有100元,这时候A离开了,B进来,挪用了50块,过了会又补充进来50块,B就走了,这时候账上依旧是100,A回来,明面上看着没问题,但是这中间就被操作了(用其他人的钱补充了进去,账面不干净了),这就是ABA问题
那怎么解决呢?版本号,戳记流水
AtomicStampedRefernce
AtomicStampedRefernce自带设置版本号的属性
这样子就能暂时解决ABA问题,这是单线程的,那么多线程情况下呢?
多线程问题代码示例:
static AtomicInteger atomicInteger = new AtomicInteger(100);public static void main(String[] args) {new Thread(()->{atomicInteger.compareAndSet(100,101);try {TimeUnit.MILLISECONDS.sleep(10);} catch (Exception e) {e.printStackTrace();}atomicInteger.compareAndSet(101,100);},"A").start();new Thread(()->{try {TimeUnit.MILLISECONDS.sleep(200);} catch (Exception e) {e.printStackTrace();}System.out.println(atomicInteger.compareAndSet(100, 2024) + "\t" + atomicInteger.get());},"B").start();}
虽然结果是对的,但是无法预料到中间出现了什么变动
多线程情况下的使用:
static AtomicStampedReference<Integer> atomicStampedReference =new AtomicStampedReference<>(100,1);public static void main(String[] args) {new Thread(()->{final int stamp = atomicStampedReference.getStamp();System.out.println(Thread.currentThread().getName() + "\t 首次版本号" + stamp);//暂停一段时间,让b线程初始化的值跟我一样try {TimeUnit.MILLISECONDS.sleep(500);} catch (Exception e) {e.printStackTrace();}atomicStampedReference.compareAndSet(100,101,atomicStampedReference.getStamp(),stamp+1);System.out.println(Thread.currentThread().getName() + "\t 2次流水号" + atomicStampedReference.getStamp());atomicStampedReference.compareAndSet(101,100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);System.out.println(Thread.currentThread().getName() + "\t 3次流水号" + atomicStampedReference.getStamp());},"A").start();new Thread(()->{final int stamp = atomicStampedReference.getStamp();System.out.println(Thread.currentThread().getName() + "\t 首次版本号" + stamp);//等待a线程出现aba问题try {TimeUnit.SECONDS.sleep(1);} catch (Exception e) {e.printStackTrace();}final boolean b = atomicStampedReference.compareAndSet(100, 2024, stamp, stamp + 1);System.out.println(b + "\t" + atomicStampedReference.getReference()+"\t "+atomicStampedReference.getStamp());},"B").start();}
中间有被改变的痕迹,因此b线程想要去修改时,就不让修改,解决ABA问题
就先说到这 \color{#008B8B}{ 就先说到这} 就先说到这
在下 A p o l l o \color{#008B8B}{在下Apollo} 在下Apollo
一个爱分享 J a v a 、生活的小人物, \color{#008B8B}{一个爱分享Java、生活的小人物,} 一个爱分享Java、生活的小人物,
咱们来日方长,有缘江湖再见,告辞! \color{#008B8B}{咱们来日方长,有缘江湖再见,告辞!} 咱们来日方长,有缘江湖再见,告辞!