JUC并发编程16 | CAS自旋锁

这篇具有很好参考价值的文章主要介绍了JUC并发编程16 | CAS自旋锁。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

CAS自旋锁

是什么,干什么,解决了什么痛点?如何解决,如何使用。

原子类:java.util.concurrent.atomic

在没有CAS之前,多线程环境不使用原子类保证线程安全i++等操作,会出现数据问题,如果直接加锁synchronized,资源的开销就比较大

在出现CAS之后,多线程环境,使用原子类保证线程安全i++,类似我们的乐观锁

CAS是什么

CAS是compare and swap的缩写,中文翻译为比较并交换,实现并发算法时常用的一种技术

CAS 包含三个操作数 —— 内存位置、预期原值及更新值

在执行CAS操作的时候,将内存位置的值与预期原值比较,

  • 如果相匹配,那么处理器会自动将该位置值更新为新值
  • 如果不匹配,处理器不做任何操作,多个线程同时执行CAS只有一个会成功

CAS的原理

CAS 有三个操作数,位置内存值V,旧的预期值A,要修改的更新值为B

当且仅当就得预期值A与内存值V相同时,将内存值V修改位B,否则什么都不做,重来——即自旋

JUC并发编程16 | CAS自旋锁

这是通过硬件级别保证的

Unsafe 类

CAS是JDK提供的非阻塞原子性操作,它通过硬件保证了比较-更新的原子性。

它是非阻塞的且自身具有原子性,也就是说这玩意效率更高且通过硬件保证,说明这玩意更可靠。

CAS是一条CPU的原子指令(cmpxchg指令),不会造成所谓的数据不一致问题,Unsafe提供的CAS方法(如compareAndSwapXXX)底层实现即为CPU指令cmpxchg。

执行cmpxchg指令的时候,会判断当前系统是否为多核系统,如果是就给总线加锁,只有一个线程会对总线加锁成功,加锁成功之后会执行cas操作,也就是说CAS的原子性实际上是CPU实现独占的,比起用synchronized重量级锁,这里的排他时间要短很多,所以在多线程情况下性能会比较好。

进入Unsafe方法查看源码

public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);

/**
上面三个方法都是类似的,主要对4个参数做一下说明。
var1:表示要操作的对象
var2:表示要操作对象中属性地址的偏移量
var4:表示需要修改数据的期望的值
var5/var6:表示需要修改为的新值
*/

1 Unsafe
是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地〈native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中,共内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。

注意Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务

2 变量valueOffset,表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。

// AtomicInteger 类
public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

// Unsafe 类
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        // volatile 修饰,一旦var5被修改会被立即获知
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

3 变量value使用volatile修饰,保证了多线程之间的内存可见性

假设线程A和线程B两个线程同时执行getAndAddInt操作(分别跑在不同CPU上)

  1. AtomicInteger里面的value原始值为3,即主内存中AtomicInteger的value为3,根据JMM模型,线程A和线程B各自持有一份值为3的value的副本分别到各自的工作内存。
  2. 线程A通过getIntVolatile(var1, var2)拿到value值3,这时线程A被挂起。
  3. 线程B也通过getlntVolatile(var1, var2)方法获取到value值3,此时刚好线程B没有被挂起并执行compareAndSwaplnt方法比较内存值也为3,成功修改内存值为4,线程B打完收工,一切OK。
  4. 这时线程A恢复,执行compareAndSwapInt方法比较,发现自己手里的值数字3和主内存的值数字4不一致,说明该值已经被其它线程抢先一步修改过了,那A线程本次修改失败,只能重新读取重新来一遍了。
  5. 线程A重新获取value值,因为变量value被volatile修饰,所以其它线程对它的修改,线程A总是能够看到,线程A继续执行compareAndSwapInt进行比较替换,直到成功。

原子引用AtomicReference

public class CASDemo {
    public static void main(String[] args) {
        AtomicReference<User> atomicReference = new AtomicReference<>();
        User zhangsan = new User("zhangsan", 22);
        User lisi = new User("lisi", 24);
        atomicReference.set(zhangsan);
        System.out.println(atomicReference.compareAndSet(zhangsan,lisi)+"\t" + atomicReference.get().toString());
        System.out.println(atomicReference.compareAndSet(zhangsan,lisi)+"\t" + atomicReference.get().toString());
    }
}

CAS与自旋锁

通过cas操作完成自旋锁,A线程先进来,调用lock方法自己持有锁5秒;

B随后进来发现当前线程支持有所,进行自旋等待,直到A释放锁后B随后抢到

public class CASDemo {
    AtomicReference<Thread> atomicReference = new AtomicReference<>();
    public void lock(){
        Thread thread = Thread.currentThread();
        System.out.println("==============="+Thread.currentThread().getName()+" come in ==============");
        while (!atomicReference.compareAndSet(null,thread)) {}
    }
    public void unlock(){
        Thread thread =  Thread.currentThread();
        atomicReference.compareAndSet(thread,null);
        System.out.println("==============="+Thread.currentThread().getName()+" task is over ==============");
    }
    public static void main(String[] args) throws InterruptedException {
        CASDemo casDemo = new CASDemo();
        new Thread(()->{
            casDemo.lock();
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                casDemo.unlock();
            }
        },"t1").start();
        TimeUnit.SECONDS.sleep(1);
        new Thread(()->{
            casDemo.lock();
            try {
                TimeUnit.SECONDS.sleep(8);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                casDemo.unlock();
            }
        },"t2").start();
    }
}

CAS的缺点

  • 循环时间长开销大
  • 具有ABA问题

循环时间长开销大

如果cas失败,会一直进行尝试。如果cas长时间一直不成功,可能会给cpu带来很大的开销

ABA问题

CAS会导致“ABA问题”。

CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。

比如说一个线程1从内存位置V中取出A,这时候另一个线程2也从内存中取出A,并且线程2进行了一些操作将值变成了B,然后线程2又将V位置的数据变成A,这时候线程1进行CAS操作发现内存中仍然是A,预期OK,然后线程1操作成功。

尽管线程1的CAS操作成功,但是不代表这个过程就是没有问题的。

@Data
@AllArgsConstructor
@NoArgsConstructor
class Book{
    private String name;
    private int id;
}
public class ABADemo {

    public static void main(String[] args) {

        Book java = new Book("java",1);
        Book mysql = new Book("mysql",2);
        AtomicStampedReference<Book> stampedReference = new AtomicStampedReference<>(java, 1);
        new Thread(()->{
            // 初始条件是java
            System.out.println(stampedReference.getReference()+"\t初始条件是java:" + stampedReference.getStamp());
            // 此时邮戳莫有启动,但是已经被改为mysql了
            stampedReference.compareAndSet(java, mysql, stampedReference.getStamp(), stampedReference.getStamp());
            System.out.println(stampedReference.getReference()+"\t被改为mysql了" + stampedReference.getStamp());
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 修改回Java了
            stampedReference.compareAndSet(mysql, java, stampedReference.getStamp(), stampedReference.getStamp());
            System.out.println(stampedReference.getReference()+"\t修改回Java了:" + stampedReference.getStamp());
        },"t1").start();
        new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 不知道被就该过了
            stampedReference.compareAndSet(java, mysql, stampedReference.getStamp(), stampedReference.getStamp());
            System.out.println(stampedReference.getReference()+"\t不知道被就该过了,此时还能改为mysql" + stampedReference.getStamp());

        },"t2").start();

    }
}
/**
Book(name=java, id=1)	初始条件是java:1
Book(name=mysql, id=2)	被改为mysql了1
Book(name=java, id=1)	修改回Java了:1
Book(name=mysql, id=2)	不知道被就该过了,此时还能改为mysql1
*/

解决:ABA

使用 AtomicStampedReference

内容 版本
A 1
B 2
A 3

解决代码

@Data
@AllArgsConstructor
@NoArgsConstructor
class Book{
    private String name;
    private int id;
}
public class ABADemo {
    public static void main(String[] args) {
        Book java = new Book("java",1);
        Book mysql = new Book("mysql",2);
        AtomicStampedReference<Book> stampedReference = new AtomicStampedReference<>(java, 1);
        System.out.println(stampedReference.getReference()+"\t" + stampedReference.getStamp());
        boolean b;
        // 如果是java,且邮戳不变,那就换成mysql,同时邮戳+1
        b = stampedReference.compareAndSet(java, mysql, stampedReference.getStamp(), stampedReference.getStamp()+1);
        System.out.println(stampedReference.getReference()+"\t" + stampedReference.getStamp());
        // 把 java 换回来
        b = stampedReference.compareAndSet(mysql, java, stampedReference.getStamp(), stampedReference.getStamp()+1);
        System.out.println(stampedReference.getReference()+"\t" + stampedReference.getStamp());
    }
}

上面演示了单线程的情况,下面演示多线程的cas情况文章来源地址https://www.toymoban.com/news/detail-443113.html

@Data
@AllArgsConstructor
@NoArgsConstructor
class Book{
    private String name;
    private int id;
}
public class ABADemo {

    public static void main(String[] args) {

        Book java = new Book("java",1);
        Book mysql = new Book("mysql",2);
        AtomicStampedReference<Book> stampedReference = new AtomicStampedReference<>(java, 1);
        new Thread(()->{
            int stamp = stampedReference.getStamp();
            System.out.println(Thread.currentThread().getName()+"\t首次版本号:" + stampedReference.getStamp());
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 此时邮戳莫有启动,但是已经被改为mysql了
            stampedReference.compareAndSet(java, mysql, stampedReference.getStamp(), stampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName()+"\t"+stampedReference.getReference()+"\t版本号2:" + stampedReference.getStamp());

            // 修改回Java了
            stampedReference.compareAndSet(mysql, java, stampedReference.getStamp(), stampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName()+"\t"+stampedReference.getReference()+"\t版本号3:" + stampedReference.getStamp());
        },"t1").start();
        new Thread(()->{
            int stamp = stampedReference.getStamp();
            System.out.println(Thread.currentThread().getName()+"\t首次版本号:" + stampedReference.getStamp());
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 被修改过了
            boolean b = stampedReference.compareAndSet(java, mysql, stamp, stampedReference.getStamp()+1);
            System.out.println(b+"\t"+stampedReference.getReference()+"\t" + stampedReference.getStamp());

        },"t2").start();
    }
}
/**

t1	首次版本号:1
t2	首次版本号:1
t1	Book(name=mysql, id=2)	版本号2:2
t1	Book(name=java, id=1)	版本号3:3
false	Book(name=java, id=1)	3
*/

到了这里,关于JUC并发编程16 | CAS自旋锁的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • DP读书:不知道干什么就和我一起读书吧——以《鲲鹏处理器 架构与编程》中鲲鹏软件的构成为例

    虽然清楚知识需要靠时间沉淀,但在看到自己做不出来的题别人会做,自己写不出的代码别人会写时还是会感到焦虑怎么办? 你是否也因为自身跟周围人的差距而产生过迷茫,这份迷茫如今是被你克服了还是仍旧让你感到困扰?来分享一下吧! 我就读了几天书,就这样了。

    2024年02月09日
    浏览(29)
  • AI(Artificial Intelligence)解决方案工程师是干什么的? Solution Engineer

    作者:禅与计算机程序设计艺术 1.简介 从事智能解决方案开发、架构设计、产品开发等工作。主要负责智能业务系统的研发及项目管理。 本人拥有丰富的产品研发经验,包括传统IT行业的软件开发经验、数据库设计、业务需求分析、项目管理、团队合作等方面。在智元的工作

    2024年02月08日
    浏览(28)
  • 《JUC并发编程 - 高级篇》05 -共享模型之无锁 (CAS | 原子整数 | 原子引用 | 原子数组 | 字段更新器 | 原子累加器 | Unsafe类 )

    有如下需求,保证 account.withdraw 取款方法的线程安全 原有实现并不是线程安全的 测试代码 执行测试代码,某次执行结果 5.1.1 为么不安全 withdraw 方法是临界区,会存在线程安全问题 查看下字节码 多线程在执行过程中可能会出现指令的交错,从而结果错误! 5.1.2 解决思路1

    2023年04月12日
    浏览(31)
  • 【并发编程】CAS到底是什么

    Java实现CAS的原理 | Java程序员进阶之路 美团终面:CAS确定完全不需要锁吗? CAS 是 Compare-And-Swap (比较并交换)的缩写,是一种 轻量级的同步机制 ,主要用于实现多线程环境下的无锁算法和数据结构,保证了并发安全性。它可以在 不使用锁 (如synchronized、Lock)的情况下,对

    2024年02月20日
    浏览(25)
  • 【Docker】什么是Docker,它用来干什么

    作者简介: 辭七七,目前大一,正在学习C/C++,Java,Python等 作者主页: 七七的个人主页 文章收录专栏: 七七的闲谈 欢迎大家点赞 👍 收藏 ⭐ 加关注哦!💖💖 Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的镜像中,然后发布到

    2024年02月07日
    浏览(33)
  • 大数据是干什么的?

    大数据技术的战略意义不在于掌握庞大的数据信息,而在于对这些有意义的数据进行专业的处理。换句话说,如果把大数据比作一个行业,这个行业盈利的关键在于提高数据的“处理能力”,通过“处理”实现数据的“增值”。 从技术上讲,大数据和云计算的关系就像硬币的

    2024年01月21日
    浏览(30)
  • 数字藏品可以用来干什么?

    一、作为数字收藏艺术品,满足收藏者的爱好。绘画、文物等艺术品是数字收藏品是最基础的应用,也是目前最受欢迎的种类,它与现实生活中的其他艺术品具有相似性,一样通过网上购买的方式获得。 数字藏品,虽然“摸不着”,但与传统艺术品相比较,又具有一定优势,

    2024年02月09日
    浏览(29)
  • 服务器是什么?它是用来干什么的?

    作者: Insist-- 个人主页: insist--个人主页 作者会持续更新网络知识和python基础知识,期待你的关注   目录 一、服务器是什么? 二、服务器的作用 1、提高访问速度 2、提高安全性 三、云服务器与物理服务器 1、云服务器 云服务器的优点: 2、物理服务器 物理服务器的优点:

    2024年02月08日
    浏览(34)
  • 什么是tomcat?tomcat是干什么用的?

    什么是tomcat Tomcat是常见的免费的web服务器. Tomcat 这个名字的来历,Tomcat是一种野外的猫科动物,不依赖人类,独立生活。 Tomcat的作者,取这个名字的初衷是希望,这一款服务器可以自力更生,自给自足,像Tomcat这样一种野生动物一般,不依赖其他插件,而可以独立达到提供

    2023年04月11日
    浏览(32)
  • python cv2是什么,可以用来干什么

    OpenCV (Open Source Computer Vision Library) 是一个流行的开源计算机视觉库,提供了丰富的图像和视频处理功能。通过使用 OpenCV 的 Python 绑定库 cv2,可以实现以下一些功能: 图像读取和显示:使用 cv2.imread() 读取图像文件,使用 cv2.imshow() 显示图像窗口。 图像处理:包括图像滤波、

    2024年02月14日
    浏览(34)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包