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

 更新时间:2024年01月13日 10:10:07   作者: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无锁机制原理内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • microlog4android将Android Log日志写到SD卡文件中实现方法

    microlog4android将Android Log日志写到SD卡文件中实现方法

    这篇文章主要介绍了microlog4android将Android Log日志写到SD卡文件中实现方法的相关资料,需要的朋友可以参考下
    2016-10-10
  • SpringBoot中的@EnableConfigurationProperties注解详细解析

    SpringBoot中的@EnableConfigurationProperties注解详细解析

    这篇文章主要介绍了SpringBoot中的@EnableConfigurationProperties注解详细解析,如果一个配置类只配置@ConfigurationProperties注解,而没有使用@Component或者实现了@Component的其他注解,那么在IOC容器中是获取不到properties 配置文件转化的bean,需要的朋友可以参考下
    2024-01-01
  • Java的函数式编程详解

    Java的函数式编程详解

    用了这么久的Java8,我寻思这种话也好意思说出来吗,难道自己是PythonBoy出身就是看不懂Java的理由吗,身为一个合格的后端Boy不会还有人看不明白Java的函数式编程吧,接下来小编和大家浅聊一下Java的函数式编程,需要的朋友可以参考下
    2023-10-10
  • java面向对象编程重要概念继承和多态示例解析

    java面向对象编程重要概念继承和多态示例解析

    这篇文章主要为大家介绍了java面向对象编程的两个重要概念继承和多态示例解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-05-05
  • Java根据表达式获取对象中的值及设置值的例子

    Java根据表达式获取对象中的值及设置值的例子

    这篇文章主要介绍了Java根据表达式获取对象中的值及设置值的例子,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧
    2025-03-03
  • 详解springboot WebTestClient的使用

    详解springboot WebTestClient的使用

    WebClient是一个响应式客户端,它提供了RestTemplate的替代方法。这篇文章主要介绍了详解springboot WebTestClient的使用, 具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-11-11
  • Java doGet, doPost方法和文件上传实例代码

    Java doGet, doPost方法和文件上传实例代码

    这篇文章主要介绍了Java doGet, doPost方法和文件上传实例代码的相关资料,需要的朋友可以参考下
    2016-11-11
  • 三道java新手入门面试题,通往自由的道路--JVM

    三道java新手入门面试题,通往自由的道路--JVM

    这篇文章主要为大家分享了最有价值的3道JVM面试题,涵盖内容全面,包括数据结构和算法相关的题目、经典面试编程题等,对hashCode方法的设计、垃圾收集的堆和代进行剖析,感兴趣的小伙伴们可以参考一下
    2021-07-07
  • 使用Spring开启@Async异步方式(javaconfig配置)

    使用Spring开启@Async异步方式(javaconfig配置)

    这篇文章主要介绍了使用Spring开启@Async异步方式(javaconfig配置),具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-08-08
  • Spring Boot全局异常处理实战指南

    Spring Boot全局异常处理实战指南

    在开发过程中,异常是不可避免的,Spring Boot提供了一种简单而强大的机制来处理应用程序中的异常,即全局异常处理,这篇文章主要介绍了Spring Boot全局异常处理的相关资料,需要的朋友可以参考下
    2025-09-09

最新评论