java并发编程之CAS透彻理解

CAS 介绍:

CAS,Compare And Swap,即比较并交换。Doug lea 大神在同步组件中大量使用 CAS 技术鬼斧神工地实现了 Java 多线程的并发操作。整个 AQS 同步组件、Atomic 原子类操作等等都是以 CAS 实现的。可以说 CAS 是整个 J.U.C 的基石。

CAS 比较交换的过程 CAS(V,A,B)
V-一个内存地址存放的实际值、A-旧的预期值、B-即将更新的值,当且仅当预期值 A 和内存值 V 相同时,将内存值修改为 B 并返回 true,否则什么都不做,并返回 false
这个我们一会在下面详细解释上述的交换过程

代码演示CAS:

public class CASDemo {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(1);
        //mian do someting----
        System.out.println(atomicInteger.compareAndSet(1,2)+
                "\t 当前最新数据:"+atomicInteger.get());
        System.out.println(atomicInteger.compareAndSet(1,3)+
                "\t 当前最新数据:"+atomicInteger.get());
    }
}

compareAndSet源码:参数一expectedValue是预期的旧值(第一次从主物理内存中拷贝到工作内存中的旧值,即就是我们第一次拿到的值的拷贝),参数二newValue是修改成的新值

//U是Unsafe的一个实例
private static final jdk.internal.misc.Unsafe U = jdk.internal.misc.Unsafe.getUnsafe();
private static final long VALUE;// VALUE是内存地址的值

public final boolean compareAndSet(int expectedValue, int newValue) {
        return U.compareAndSetInt(this, VALUE, expectedValue, newValue);//this是当前对象,即atomicInteger 实例
    }

在这里插入图片描述

CAS底层:

例如我们这路选择原子整型类的一个方法getAndIncrement方法来实现类似于i++的原子操作:

package com.fan.applet;

import java.util.concurrent.atomic.AtomicInteger;

public class CASDemo {
    static int a = 0;//声明非原子类的整型变量,static证明只有一份,共享的资源
    public static void main(String[] args) {
    
        AtomicInteger atomicInteger = new AtomicInteger(0);
        for (int i = 1; i <= 10; i++) {
            //创建十个线程
            new Thread(()->{
                for (int j = 0; j < 1000; j++) {
                    a++;
                    atomicInteger.getAndIncrement();//i++的原子操作方法

                }
            },String.valueOf(i)).start();
        }
        while(Thread.activeCount()>2){//gc和异常处理的线程在运行着
            Thread.yield();//阻塞操作,main出让自己的cpu时间,等待以上十个线程计算完
        }
        System.out.println("非原子类计算结果:"+a);
        System.out.println("原子类计算结果:"+atomicInteger.get());
    }
}

在这里插入图片描述

非原子运算结果多线程不安全。

getAndIncrement方法的源码:类似于i++
在这里插入图片描述
在这里插入图片描述

Unsafe类介绍

在这里插入图片描述

Java中要想使用CAS原子的修改某值,怎么做呢?幸运的是Java提供了这样的API,就是在rt.jar下的sun.misc.Unsafe.java类中。Unsafe,中文名不安全的(因为能直接操作内存地址,所以不安全),也被称为魔术类,魔法类

Unsafe类使Java拥有了像C语言的指针一样操作内存空间的能力,一旦能够直接操作内存,这也就意味着
(1)不受JVM管理,意思就是使用Unsafe操作内存无法被JVM GC,需要我们手动GC,稍有不慎就会出现内存泄漏。
(2)Unsafe的不少方法中必须提供原始地址(内存地址)和被替换对象的地址,并且偏移量要自己计算(其提供的有计算偏移量的方法),所以一旦出现问题就是JVM崩溃级别的异常,会导致整个JVM实例崩溃,表现为应用程序直接crash掉。
(3)直接操作内存,所以速度更快,在高并发的条件之下能够很好地提高效率。

因此,从上面三个角度来看,虽然在一定程度上提升了效率但是也带来了指针的不安全性。这也是它被取名为Unsafe的原因吧。

下面我们深入到源码中看看,提供了什么方法直接操作内存。

打开Unsafe这个类,我们会发现里面有大量的被native关键字修饰的方法,这意味着这些方法是C语言提供的实现,底层调的是C语言的库函数,我们无法直接看到他的源码实现,需要去从OpenJDK去看了。另外还有一些基于native方法封装的其他方法,整个Unsafe中的方法大致可以归结为以下几类:
(1)初始化操作
(2)操作对象属性
(3)操作数组元素
(4)线程挂起和恢复
(5)CAS机制

在这里插入图片描述
在这里插入图片描述

CAS底层原理:

getAndIncrement()(加一的方法)–>调用getAndAddInt(Object o, long offset, int delta)–>调用weakCompareAndSetInt(o, offset, v, v + delta)

//CAS本质
@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方法:

/*
        Object o  :AtomicInteger对象实例本身
        long offset:AtomicInteger对象的值的引用内存地址
        int delta:需要变动的数量
        int v: 是用Object o, long offset 找出的主内存中真实的值 
        过程:‘
        1.用该对象的第一次do操作读取内存中的数据的值到自己的工作内存中的值,作为预期的旧值。
        2.然后经过一些操作之后,再次看内存中的最新值v(可见性)和预期的旧值(拷贝到自己工作内存中的旧值)是否相等,如果相等,则设置成新值v + delta,不相等,
        则继续循环读取内存最新的值,再次比较。直到更新成功为止。
         */
        
        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方法:
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

Unsafe类的compareAndSwapInt方法,它有四个参数,分别是要修改的类实例对象、要修改的值的偏移量、旧值、新值。解释一下偏移量

假设有两个线程想要更新变量 V=10;
线程A 想把V 更新为20;
线程B 想把V 更新为30;
对于线程A 来说,V 值为10,旧的预期值E当然也为10,希望更新的值N 为20;
如果B 线程在A 线程更新之前完成对V 的更新,那么此时V 的值为30 。
那么对于A 线程来说 V 的值等于30 ,而预期值E 还是10,这说明V 的值已经被更新了,A线程就什么也不做。

CAS的缺点:

  • CPU开销大,当多个线程同时尝试修改某一变量时,线程将尝试不断自旋,直至修改成功,这将给CPU带来压力。
  • 只能保证单个变量的原子性操作,对于多个变量进行的原子性更新,CAS无能为力。
  • ABA问题

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

ABA 问题:

CAS 需要检查操作值有没有发生改变,如果没有发生改变则更新。但是存在这样一种情况:如果一个值原来是 A,变成了 B,然后又变成了 A,那么在 CAS 检查的时候会认为没有改变,但是实质上它已经发生了改变,这就是 ABA 问题。

解决方案可以沿袭数据库中常用的乐观锁方式,添加一个版本号可以解决。原来的变化路径 A->B->A 就变成了 1A->2B->3A。

在 java 1.5 后的 atomic 包中提供了 AtomicStampedReference 来解决 ABA 问题,解决思路就是这样的。

原子引用类AtomicReference:

package com.fan.applet;
import java.util.concurrent.atomic.AtomicReference;
class User{
    String userName;
    int age;

    public User(String userName, int age) {
        this.userName = userName;
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{" +
                "userName='" + userName + '\'' +
                ", age=" + age +
                '}';
    }
}
public class ABATest {
    public static void main(String[] args) {
        User z3 = new User("张三",11);
        User li4 = new User("李四",22);
        //原子引用类
        AtomicReference<User> userAtomicReference = new AtomicReference<>();
        userAtomicReference.set(z3);//将对象设置成原子类对象
        System.out.println(userAtomicReference.compareAndSet(z3,li4)+
                "\t"+userAtomicReference.get().toString());
        System.out.println(userAtomicReference.compareAndSet(z3,li4)+
                "\t"+userAtomicReference.get().toString());

    }
}

演示ABA问题:

package com.fan.applet;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

public class ABATest2 {
    static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
    public static void main(String[] args) {
        new Thread(()->{
            atomicReference.compareAndSet(100,101);//A-->B
            atomicReference.compareAndSet(101,100);//B-->A
        },"t1").start();

        new Thread(()->{
            //暂停一秒,保证上面的线程进行了一次ABA操作
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(atomicReference.compareAndSet(100,2022)+
                    "\t"+atomicReference.get());
        },"t2").start();
    }
}

在这里插入图片描述

代码演示解决ABA问题:

带版本号的原子引用类AtomicStampedReference解决ABA问题:

在这里插入图片描述
在这里插入图片描述

package com.fan.applet;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;

public class ABATest3 {
    //给定初始值和版本号
    static AtomicStampedReference<Integer> atomicStampedReference =
            new AtomicStampedReference(100,1);
    public static void main(String[] args) {
        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+
                    "\t第一次的版本号:"+atomicStampedReference.getStamp());
            //进行ABA操作
            atomicStampedReference.compareAndSet(100,101,
                    atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName()+
                    "\t第二次的版本号:"+atomicStampedReference.getStamp());
            atomicStampedReference.compareAndSet(101,100,
                    atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName()+
                    "\t第三次的版本号:"+atomicStampedReference.getStamp());
        },"t3").start();

        new Thread(()->{
            //先获取版本号,然后休眠2秒,让t3先执行
            int oldStamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName()+
                    "\t第一次的版本号:"+oldStamp);
            //先让上面线程进行一次ABA的操作
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            boolean b = atomicStampedReference.compareAndSet(100, 2020,
                    oldStamp, atomicStampedReference.getStamp() + 1);
            System.out.println(Thread.currentThread().getName()+"\t修改成功否:"+b+
                    ",当前最新版本号:"+atomicStampedReference.getStamp()+
                    ",当前实际最新值:"+atomicStampedReference.getReference());
        },"t4").start();
    }
}

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值