java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > CAS无锁机制原理

Java中的CAS无锁机制实现原理详解

作者:java架构师-太阳

这篇文章主要介绍了Java中的CAS无锁机制实现原理详解,无锁机制,是乐观锁的一种实现,并发情况下保证对共享变量值更改的原子性,CAS是Java中Unsafe类里面的方法,底层通过调用C语言接口,再通过cup硬件指令保证原子性,需要的朋友可以参考下

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

适用场景

CAS 适合简单对象的操作,比如布尔值、整型值等;

典型的使用场景有两个

 (1)   J.U.C里面Atomic的原子实现,比如AtomicInteger,AtomicLong。

 (2)   实现多线程对共享资源竞争的互斥性质,比如在AQS/ConcurrentHashMap/ConcurrentLinkedQueue等

通常和自旋锁同时使用

代码示例

有一个成员变量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();
    }
}

到此这篇关于Java中的CAS无锁机制实现原理详解的文章就介绍到这了,更多相关CAS无锁机制原理内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文