并发编程------CAS

项目地址
JUC_04

CAS
CAS是C++写的,底层已经实现了原子性,所以需要使用UnSafe类,UnSafe类中使用jni技术来调用CAS中的compareAndSet,所以我们在 java只能看到compareAndSwapObject、compareAndSwapInt、compareAndSwapLong这三个方法是有四个参数,而第一个就是需要改变的对象(也就是this),后面两个可以分为E,N(前两个合并就是V)。当且仅当V=E时,将V=N

V:内存值(共享变量)
E:旧预值(工作内存)
N:新值(修改后的值)

在前面提到的compareAndSwapObject、compareAndSwapInt、compareAndSwapLong三个方法,为什么他们都是有四个参数呢?其实第一个参数为本对象,第二个就是变量的存储地址偏移量,只有拥有前面两个参数才能准确找到某个的某个共享变量。

执行流程

  1. 获取内存值V,赋值给E
  2. 如果要修改V的值
    如果V == E,说明共享变量V没有发生变化,就直接将V的值改为N
    如果V != E,说明有其他线程将V的值进行了更改,则在从1开始

在第二步的重新读取的过程称为自旋。

使用AtomicLong写出案例:

import sun.misc.Unsafe;

import java.util.concurrent.atomic.AtomicLong;

/**
 * @author 龙小虬
 * @date 2021/4/23 10:43
 */
public class Main extends Thread {

    private static AtomicLong atomicLong = new AtomicLong();

    @Override
    public void run() {
        while (atomicLong.get()<1000){
            System.out.println(Thread.currentThread().getName() + "," + atomicLong.incrementAndGet());
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Long startTime=System.currentTimeMillis();
        Main main1 = new Main();
        Main main2 = new Main();
        main1.start();
        main2.start();
        main1.join();
        main2.join();
        Long endTime=System.currentTimeMillis();
        System.out.println(endTime-startTime);
    }
}

1.atomicLong.get()
获取value值
2.atomicLong.incrementAndGet()
value做自增
我们来看看这个自增的方法。
在这里插入图片描述
在这里插入图片描述
我们先看getAndAddLong()方法。
1.this.getLongVolatile(var1, var2);
获取工作内存的value值,也就是主内存中获取value
2.this.compareAndSwapLong(var1, var2, var6, var6 + var4)
利用compareAndSet(),对主内存数据进行修改,并对比工作空间和主内存变量中的值是否相同,如果不相同则继续执行步骤1,直至相同,最后更改主内存数据为原主内存数据+1。

在getAndAddLong()方法中,为什么他的调用之后,返回的是原主内存数据中的数据,为什么不是返回最新的主内存数据,而是在原主内存数据+1,猜测是因为do…while(),如果再去查找一次那么则会降低效率,一旦是几百个线程去修改这个数据,那么相当于有99个线程都需要再多一次自旋,而且在修改之前,直接从主内存获取数据,所以没有必要返回值的时候再去查找。

基于AtomicLong手写锁

import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.IntStream;

/**
 * @author 龙小虬
 * @date 2021/4/23 13:32
 */
public class AtomicTryLock {


    //定义AtomicInteger
    //0表示该锁已经被使用
    //1表示未被使用
    private volatile AtomicInteger atomicInteger = new AtomicInteger(0);

    // 存放当前获取到锁的线程名
    private Thread lockCurrentThread;

    //尝试获取锁
    public boolean tryLock() {
        boolean result;
        // 加入自旋
        while (!(result = atomicInteger.compareAndSet(0, 1))) {
            System.out.println(Thread.currentThread() + ",自旋中.....");
        }
        lockCurrentThread = Thread.currentThread();
        System.out.println(lockCurrentThread + ",获取锁成功");
        return result;
    }

    //释放锁
    public boolean unLock() {
        // 只有获取到锁的线程才能释放锁
        if (lockCurrentThread != null && lockCurrentThread != Thread.currentThread()) {
            return false;
        }
        return atomicInteger.compareAndSet(1, 0);
    }

    public static void main(String[] args) {
        AtomicTryLock atomicTryLock = new AtomicTryLock();
        IntStream.range(1, 10).forEach((i) -> new Thread(() -> {
            // 不管发生什么,线程结束都需要释放锁
            try {
                atomicTryLock.tryLock();
            } catch (Exception e) {
                e.printStackTrace();
                atomicTryLock.unLock();
            } finally {
                atomicTryLock.unLock();
            }
        }).start());
    }
}

还有个问题,在高并发下,我觉得CAS的ABA问题会很严重。
我们来谈谈ABA问题吧。

CAS的ABA问题
如果将原来的值A,改为了B,B有改为了A 发现没有发生变化,实际上已经发生了变化,所以存在ABA问题。

那么经典案例是什么?为什么要去解决这个问题?

CAS经典案例:

用户要给储值卡充值20元,用户卡内余额15元,A线程首先获取余额是15,然后准备加上20,A线程因为某种原因暂停了,而B线程成功将余额加上20并且成功提交,此时余额为35。但是紧接着用户又消费了20,所以余额还是15,终于A线程获取到了时间片,它比对之后发现余额还是15,所以A线程就执行了。
ABA的问题核心在于一个线程在提交的时候,如果只是根据要修改的值和之前是否一样,这样是无法证明这个值没有被其他线程改过。因为在这段时间它的值可能被改为其他值,然后又改原来的,实际上如果能避免重复提交就能避免ABA问题,而版本号控制可以避免重复提交 -----> 这也就是解决ABA的方案

官方也给出了AtomicMarkableReference对象来解决这个问题。
使用方法:

import java.util.concurrent.atomic.AtomicStampedReference;

/**
 * @author 龙小虬
 * Long、Integer、Short、Byte缓存区间是在-128~127,会直接存在常量池中,而不在这个区间内对象的值则会每次都new一个对象,那么即使两个对象的值相同,CAS方法都会返回false
 * @date 2021/4/23 13:49
 */
public class AtomicStampedReferenceTest {
    // 先声明初始值,修改后的值和临时的值是为了保证使用CAS方法不会因为对象不一样而返回false
    private static final Integer INIT_NUM = 100;
    private static final Integer UPDATE_NUM = 200;

    private static AtomicStampedReference atomicStampedReference = new AtomicStampedReference(INIT_NUM, 1);

    public static void main(String[] args) {
        new Thread(() -> {
            Integer value = (Integer) atomicStampedReference.getReference();
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + " : 当前值为:" + value + " 版本号为:" + stamp);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //第一个参数expectedReference:表示预期值
            //第二个参数newReference:表示要更新的值
            //第三个参数expectedStamp:表示预期的时间戳
            //第四个参数newStamp:表示要更新的时间戳。
            if (atomicStampedReference.compareAndSet(value, UPDATE_NUM, 1, stamp + 1)) {
                System.out.println(Thread.currentThread().getName() + " : 当前值为:" + atomicStampedReference.getReference() + " 版本号为:" + atomicStampedReference.getStamp());
            } else {
                System.out.println("版本号不同,更新失败!");
            }
        }).start();
    }
}

这里面就行了版本号的对比。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

龙小虬

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值