CAS无锁机制介绍和实现原理

01 CAS(Compare And Swap) 比较和替换

无锁机制,是乐观锁的一种实现

并发情况下保证对共享变量值更改的原子性
CAS是Java中Unsafe类里面的方法
底层通过调用C语言接口,再通过cup硬件指令保证原子性

实现算法
三个参数(V,E,N): V是要更新的变量,E是预期值,N是新值。当V的值等于E值时,将V的值设为N,若V值和E值不同,则说明已经有其他线程做了更新,当前线程方式自旋重来。最后,CAS返回当前V的真实值,CAS 一定要volatile变量配合,这样才能保证每次拿到的变量是主内存中最新的那个值

 使用乐观锁思想,多个线程用CAS操作一个变量时,只有一个成功且成功更新。失败的线程不会被挂起,仅是被告知失败,允许再次尝试,也允许失败线程放弃操作。基于这样的原理,CAS操作即使没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理

 与锁相比,使用CAS会使程序看起来更加复杂一些,但由于其非阻塞性,对死锁天生免疫,且线程间的相互影响也远远比基于锁的方式要小,无锁方式没有锁竞争,也没有线程间频繁调度的开销,因此,比基于锁的方式拥有更优越的性能
  
 底层实现
      通过硬件保证了比较更新的原子性和可见性,实现方式是基于硬件平台的汇编指令,大部分的现代处理器都已经支持原子化的CAS指令,在JDK 5.0以后,虚拟机便可以使用这个指令来实现并发操作和并发数据结构,CAS是cup的原子指令(cmpxchg),不会造成数据不一致,执行cmpxchg指令时,会判断当前系统是否是多核系统,如果是就给总线加锁,只会有一个线程给总线加锁成功,加速成功之后会执行cas操作,也就是cas的原子性实际上是cup实现的,比起synchronized,排他时间很短,多线程下性能高

优点
(1) 高并发的情况下,比有锁的程序拥有更好的性能,是轻量级锁
(2) 它天生就是死锁免疫的
(3) 线程不会阻塞(线程阻塞到唤醒运行成本较高),一直处于用户态
 
缺点
  (1) 若一直获取不到锁死循环,消耗cup资源,可能导致cup飙高(需要控制次数)
  (2) ABA问题:若内存地址V初次读取的值是A,在CAS等待期间它的值曾经被改成了B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。解决办法,加版本号属性,每次值变化了给版本号加1,。ABA的问题对结果没有影响,只会和CAS概念冲突了
(3) 不能保证代码块的原子性:CAS只保证一个变量的原子性,若更新多变量同时原子性要用synchronized

适用场景
  (1) CAS 适合简单对象的操作,比如布尔值、整型值等;

典型的使用场景有两个

 (1)   J.U.C里面Atomic的原子实现,比如AtomicInteger,AtomicLong。
 (2)   实现多线程对共享资源竞争的互斥性质,比如在AQS/ConcurrentHashMap/ConcurrentLinkedQueue等。

通常和自旋锁同时使用


02 代码

有一个成员变量state,默认值是0,
定义了一个方法doSomething(),判断state是否为0 ,如果为0,就修改成1。

这个逻辑在多线程环境下,会存在原子性的问题,因为这里是一个典型的,Read - Write的操作。
一般会在doSomething()这个方法上加同步锁来解决原子性问题,
但加同步锁,会带来性能上的损耗,

public class Example {
    private int state = 0;
    public void doSomething() {
        if (state == 0) {
            state = 1;
        }
    }
}

使用CAS机制来进行优化

调用Unsafe类的objectFieldOffset()传入类的变量得到变量在内存中的偏移量
调用Unsafe类的compareAndSwapInt()方法进行更新,传入四个参数:当前对象实例/变量在内存地址中的偏移量/预期值/更新值
比较变量内存地址偏移量对应的值和传入的预期值是否相等,若相等,修改内存地址中变量的值为要更新的值,否则,返回false
compareAndSwap()是native方法,底层实现中,在多核CPU环境下,会增加一个Lock指令对缓存或总线加锁,从而保证比较并替换这两个指令的原子性。
这里注意变量要加volatile
public class Example {
    
    private volatile int state = 0;
    private static final Unsafe UNSAFE = Unsafe.getUnsafe();
    private static final long stateOffset;

    static {
        try {
            stateOffset = UNSAFE.objectFieldOffset(Example.class.getDeclaredField("state"));
        } catch (Exception ex){
            throw new Error(ex);
        }
    }

    public void doSomething() {
        if (UNSAFE.compareAndSwapInt(this, stateOffset, 0 ,1)) {
            // TODO
        }
    }

    public int getState() {
        return state;
    }
}
public static void main(String[] args) {
    Example example = new Example();
    example.doSomething();
    System.out.println(example.getState());
}

测试的时候会报错会在这行代码 Unsafe.getUnsafe();

Caused by: java.lang.SecurityException: Unsafe
	at sun.misc.Unsafe.getUnsafe(Unsafe.java:90)
	at com.test.Example.<clinit>(Example.java:7)
	... 1 more

查看Unsafe.getUnsafe()源码

@CallerSensitive
public static Unsafe getUnsafe() {
    Class var0 = Reflection.getCallerClass();
    if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
        throw new SecurityException("Unsafe");
    } else {
        return theUnsafe;
    }
}

如果不是systemClassLoader则会抛出SecurityException(“Unsafe”)异常,所以用户编写的程序使用不了unsafe实例。


怎么解决ABA问题,加一个版本号
单线程情况下

public static void main(String[] args) {

    Book javaBook = new Book(1,"javaBook");

    // 参数1是版本号
    AtomicStampedReference<Book> stampedReference = new AtomicStampedReference<>(javaBook,1);

    System.out.println(stampedReference.getReference() + "\t" + stampedReference.getStamp());

    Book mysqlBook = new Book(2,"mysqlBook");

    boolean b;
    // 在判断的时候不断要判断book,还要判断版本号,更新的时候更新book的值,也要更新版本号(这里是加1)
    b = stampedReference.compareAndSet(javaBook, mysqlBook, stampedReference.getStamp(), stampedReference.getStamp() + 1);

    System.out.println(b + "\t" + stampedReference.getReference() + "\t" + stampedReference.getStamp());

    b = stampedReference.compareAndSet(mysqlBook, javaBook, stampedReference.getStamp(), stampedReference.getStamp() + 1);

    System.out.println(b+"\t"+stampedReference.getReference()+"\t"+stampedReference.getStamp());

}

对线程情乱下

public class ABADemo {
 
    static AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference<>(100,1);

    public static void main(String[] args) {
        new Thread(() -> {
            int stamp = stampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + "\t" + "首次版本号:" + stamp);

            // 暂停500毫秒,保证后面的t4线程初始化拿到的版本号和我一样
            try { TimeUnit.MILLISECONDS.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); }

            // 从100改到101
            stampedReference.compareAndSet(100,101,stampedReference.getStamp(),stampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName()+"\t"+"2次流水号:"+stampedReference.getStamp());

            // 从101改到100
            stampedReference.compareAndSet(101,100,stampedReference.getStamp(),stampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName()+"\t"+"3次流水号:"+stampedReference.getStamp());

        },"t3").start();

        new Thread(() -> {
            int stamp = stampedReference.getStamp();
            System.out.println(Thread.currentThread().getName()+ "\t" + "首次版本号:" + stamp);

            // 暂停1秒钟线程,等待上面的t3线程,发生了ABA问题
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }

            boolean b = stampedReference.compareAndSet(100, 2022, stamp, stamp + 1);

            System.out.println(b+"\t"+stampedReference.getReference()+"\t"+stampedReference.getStamp());

        },"t4").start();

    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值