3.原子变量 CAS算法

3.原子变量 CAS算法

前言

在上一篇中我们讲述了关于多线程并发,导致共享属性在内存不可见的问题。以及使用 volatile 关键字设置共享属性,使其在多线程并发中内存可见。

但是我们只讲述了多线程中单一步骤的操作,我们一般在多线程并发的情况下,还会有对一个共享值存在多个步骤的执行情况。

例如:多线程并发执行i++, 那么就可能存在两个线程或者以上同时给一个 i 值相加,导致相加错误的情况。

那么该类问题该怎么解决呢?

在这里我们可以引入 CAS算法 以及 原子变量 来解决。

知识点说明

CAS 算法

CAS (Compare-And-Swap) 是一种硬件对并发的支持,针对多处理器操作而设计的处理器中的一种特殊指令,用于管理对共享数据的并发访问。
CAS 是一种无锁的非阻塞算法的实现。 
CAS 包含了 3 个操作数:
  - 需要读写的内存值 V 
  - 进行比较的值 A 
  - 拟写入的新值 B 
当且仅当 V 的值等于 A 时,CAS 通过原子方式用新值 B 来更新 V 的值,否则不会执行任何操作。

原子变量

类的小工具包,支持在单个变量上解除锁的线程安全编程。事实上,此包中的类可将 volatile 值、字段和数组元素的概念扩展到那些也提供原子条件更新操作的类。

类 AtomicBoolean、AtomicInteger、AtomicLong 和 AtomicReference 的实例各自提供对相应类型单个变量的访问和更新。每个类也为该类型提供适当的实用工具方法。

AtomicIntegerArray、AtomicLongArray 和 AtomicReferenceArray 类进一步扩展了原子操作,对这些类型的数组提供了支持。这些类在为其数组元素提供 volatile 访问语义方面也引人注目,这对于普通数组来说是不受支持的。 

核心方法:boolean compareAndSet(expectedValue, updateValue) 
  
java.util.concurrent.atomic 包下提供了一些原子操作的常用类: 
  - AtomicBoolean 、AtomicInteger 、AtomicLong 、 AtomicReference
  - AtomicIntegerArray 、AtomicLongArray 
  - AtomicMarkableReference 
  - AtomicReferenceArray 
  - AtomicStampedReference

原子性问题的演示

1.首先编写一个实现自增序列号的线程类

/**
 * 创建一个线程类
 */

class AtomicDemo implements Runnable{

    //成员属性:线程共享属性,使用 volatile 设置内存可见性
    private volatile int serialNumber = 0;

    //线程run方法
    @Override
    public void run() {

        //休眠500毫秒, 提高并发时候的重复操作概率
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //打印本线程以及当前的serialNumber值
        //注意:在getSerialNumber将会自增 +1
        System.out.println("当前的线程: " + Thread.currentThread().getName() + ", serialNumber: " + this.getSerialNumber());

    }

    // getter: 在获取序列号的同时,对序列号进行 自增 ++
    public int getSerialNumber() {
        return serialNumber++; // 自增 +1
    }
    
}

2.创建以及启动 10个线程,查看并发打印出来的序列号 serialNumber 有没有出现 相同值 的情况

public class TestAtomicDemo {

    public static void main(String[] args) {

        //创建线程类对象
        AtomicDemo atomicDemo = new AtomicDemo();

        //创建10个线程
        for (int i = 0; i < 10; i++) {
            new Thread(atomicDemo).start(); // 创建以及启动线程
        }

    }

}

测试执行如下:

3.原子变量 CAS算法
image-20201101213529004

3.多线程并发获取到相同自增序列号的问题原因

3.原子变量 CAS算法
image-20201101214002848

从上图的说明中,我们大概已经知道了,由于我在线程中设置了休眠,那么就提供了多个线程同时读取 serialNumber 相同值的情况。

例如:

  • 线程 thread-1 读取 serialNumber = 0 , 同时 thread-n 也读取 serialNumber = 0,那么这两个线程执行自增后的值就是相同的。

当时,这并不是我们希望的效果,我们更加希望每个线程就算是并发,也应该对 serialNumber 进行逐步自增(0,1,2,3,4....),而不是存在获取相同值的情况(0, 0, 1, 1 ....)

那么如果要解决这个问题,我们就需要给 serialNumber 自增的这个步骤设置原子性:在多线程并发的情况下,同一时刻只允许一个线程变更 serialNumber 的值。

使用CAS算法 解决 原子性问题

/* 
 * 二、原子变量:在 java.util.concurrent.atomic 包下提供了一些原子变量。
 *   1. volatile 保证内存可见性
 *   2. CAS(Compare-And-Swap) 算法保证数据变量的原子性
 *    CAS 算法是硬件对于并发操作的支持
 *    CAS 包含了三个操作数:
 *    ①内存值  V
 *    ②预估值  A
 *    ③更新值  B
 *    当且仅当 V == A 时, V = B; 否则,不会执行任何操作。
 */

在这里要理解号 CAS 算法如何保证原子性的话,我们来画图理解一下。

1. 画图理解

1.1 首先假设两个线程同时从内存中读取 serialNumber = 0 的值,此时设置 V = 0

3.原子变量 CAS算法
image-20201101215831818

1.2 在线程1 中,从内存获取 serialNumber 之后,立即将其设置回线程对象的属性中,也就是预估值 A(后续拿来与 V 比较)。并且同时对 A 进行自增,将更新值设置为 B。当 V == A ,也就是说没有其他线程更新内存 serialNumber 的值,此时允许 内存 serialNumber 的值改为 B

3.原子变量 CAS算法
image-20201101220242392

1.3 由于线程 1 更新了内存 serialNumber 的值为 1,那么后续 线程 n 获取的预估值 A 则为 1,此时由于 V 不等于 A,所以判断有其他线程更新了内存数据,本次不更新

3.原子变量 CAS算法
image-20201101220840003

2.代码实现原子性变量

2.1 将序列号设置为原子性变量

3.原子变量 CAS算法
image-20201101221443475
//使用AtomicInteger设置原子性变量
private AtomicInteger serialNumber = new AtomicInteger(0);

2.2 设置为原子性变量之后,就要调用相应的原子性方法进行数据操作(底层使用CAS算法)

3.原子变量 CAS算法
image-20201101221604183
serialNumber.getAndIncrement(); // 调用原子性自增操作

2.3 设置原子性操作之后,就不会有重复的值了,如下

3.原子变量 CAS算法
image-20201101221726012

模拟CAS算法示例

在上面我们设置了原子性操作来解决原子性问题。但是并没有写 CAS算法,因为这是底层调用的。

那么为了更好的理解,我们写一个简单的模拟CAS算法示例。

1.首先创建一个模拟CAS算法的类

/**
 * 模拟CAS算法的类
 */

class CompareAndSwap {

    //成员属性
    private int value;

    //1. 获取内存值 V
    public synchronized int get() {
        return value;
    }

    //2.比较: 获取预估值 A = expectedValue, 比较 V 与 A 过后,判断是否
    public synchronized int compareAndSwap(int expectedValue, int newValue) {
        //1.首先读取内存中的值 V,设置为 oldValue
        int oldValue = this.value;

        //2.进行 V 与 A(预估值 expectedValue) 的比较,
        //  如果 V == A,那么内存值 V 设置为 B
        if (oldValue == expectedValue) {
            this.value = newValue;
        }

        //3.不管上面的操作如何,返回原来的内存值 V
        return oldValue;
    }

    //3. 判断设置是否成功
    public synchronized boolean compareAndSet(int expectedValue, int newValue) {
        //1. 根据CAS判断返回的值 oldValue,判断是否 V == A
        //   如果返回的 V(oldValue) == A(expectedValue),则更新成功;反之失败
        return expectedValue == compareAndSwap(expectedValue, newValue);
    }
}

2.基于 CAS 算法类,创建多个线程,查看是否每次 CAS 更新成功

public class TestCompareAndSwap {

    public static void main(String[] args) {
        //1.创建cas算法类对象
        final CompareAndSwap cas = new CompareAndSwap();

        //2.遍历创建10个线程
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {

                @Override
                public void run() {
                    //1.在并发线程中,每个线程首先从内存获取值
                    int expectedValue = cas.get();
                    //2.将内存值设置 cas 算法中 与 随机更新值 比较
                    boolean b = cas.compareAndSet(expectedValue, (int)(Math.random() * 101));
                    //3.打印 cas 更新成功的情况
                    System.out.println(b);
                }
            }).start();
        }

    }

}

测试执行如下:

3.原子变量 CAS算法
image-20201101223613468


原文始发于微信公众号(海洋的渔夫):3.原子变量 CAS算法

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/35004.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!