原子类型:AtomicReference详解

原子类型:AtomicReference详解

1 AtomicReferencey引入

AtomicReference类提供了对象引用的非阻塞原子性读写操作,并且提供了其他一些高级的用法

对象的引用其实是一个4字节的数字,代表着在JVM堆内存中的引用地址,对一个4字节数字的读取操作和写入操作本身就是原子性的**,通常情况下,我们对对象引用的操作一般都是获取该引用或者重新赋值(写入操作),我们也没有办法对对象引用的4字节数字进行加减乘除运算**,那么为什么JDK要提供AtomicReference类用于支持引用类型的原子性操作呢?

这里通过设计一个个人银行账号资金变化的场景,逐渐引入AtomicReference的使用,该实例有些特殊,需要满足如下几点要求:

  • 个人账号被设计为不可变对象,一旦创建就无法进行修改。
  • 个人账号类只包含两个字段:账号名、现金数字。
  • 为了便于验证,我们约定个人账号的现金只能增多而不能减少。

根据前两个要求,我们简单设计一个代表个人银行账号的Java类DebitCard,该类将被设计为不可变。

public class DebitCard {

    /**
     * 账户名名
     */
    private final String account;

    /**
     * 账户金额
     */
    private final int amount;

    public DebitCard(String account, int amount) {
        this.account = account;
        this.amount = amount;
    }

    public String getAccount() {
        return account;
    }

    public int getAmount() {
        return amount;
    }

    @Override
    public String toString() {
        return "DebitCard{" +
                "account='" + account + '\'' +
                ", amount=" + amount +
                '}';
    }
}

1.多线程下增加账号金额
假设有10个人不断地向这个银行账号里打钱,每次都存入10元,因此这个个人账号在每次被别人存入钱之后都会多10元。

import java.util.concurrent.TimeUnit;

import static java.util.concurrent.ThreadLocalRandom.current;

/**
 * @author wyaoyao
 * @date 2021/4/19 17:19
 */
public class AtomicReferenceExample1 {

    /**
     * volatile关键字修饰,每次对DebitCard对象引用的写入操作都会被其他线程看到
     * 创建初始DebitCard,账号金额为0元
     */
    static volatile DebitCard debitCard = new DebitCard("zhangsan", 0);

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread("T-" + i){
                @Override
                public void run() {
                    final DebitCard dc = debitCard;
                    // 基于全局DebitCard的金额增加10元并且产生一个新的DebitCard
                    DebitCard newDC = new DebitCard(dc.getAccount(),
                            dc.getAmount() + 10);
                    // 输出全新的DebitCard
                    System.out.println(newDC);
                    // 修改全局DebitCard对象的引用
                    debitCard = newDC;
                    try {
                        TimeUnit.MILLISECONDS.sleep(current().nextInt(20));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }.start();
        }
    }
}

声明了一个全局的DebitCard对象的引用,并且用volatile关键字进行了修饰,其目的主要是为了使DebitCard对象引用的变化对其他线程立即可见,在每个线程中都会基于全局的DebitCard金额创建一个新的DebitCard,并且用新的DebitCard对象引用更新全局DebitCard对象的引用。
程序输出

DebitCard{account='zhangsan', amount=10}
DebitCard{account='zhangsan', amount=20}
DebitCard{account='zhangsan', amount=20}
DebitCard{account='zhangsan', amount=20}
DebitCard{account='zhangsan', amount=30}
DebitCard{account='zhangsan', amount=40}
DebitCard{account='zhangsan', amount=40}
DebitCard{account='zhangsan', amount=50}
DebitCard{account='zhangsan', amount=60}
DebitCard{account='zhangsan', amount=70}

分明已有3个人向这个账号存入了10元钱,为什么账号的金额却少于30元呢?因为volatile关键字不保证原子性

虽然被volatile关键字修饰的变量每次更改都可以立即被其他线程看到,但是我们针对对象引用的修改其实至少包含了如下两个步骤,获取该引用和改变该引用(每一个步骤都是原子性的操作,但组合起来就无法保证原子性了)。

2.多线程下加锁增加账号金额

public class AtomicReferenceExample1 {

    /**
     * volatile关键字修饰,每次对DebitCard对象引用的写入操作都会被其他线程看到
     * 创建初始DebitCard,账号金额为0元
     */
    static volatile DebitCard debitCard = new DebitCard("zhangsan", 0);

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread("T-" + i){
                @Override
                public void run() {
                    synchronized (AtomicReferenceExample1.class){
                        final DebitCard dc = debitCard;
                        // 基于全局DebitCard的金额增加10元并且产生一个新的DebitCard
                        DebitCard newDC = new DebitCard(dc.getAccount(),
                                dc.getAmount() + 10);
                        // 输出全新的DebitCard
                        System.out.println(newDC);
                        // 修改全局DebitCard对象的引用
                        debitCard = newDC;
                    }
                    try {
                        TimeUnit.MILLISECONDS.sleep(current().nextInt(20));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }.start();
        }
    }
}

此时输出

DebitCard{account='zhangsan', amount=10}
DebitCard{account='zhangsan', amount=20}
DebitCard{account='zhangsan', amount=30}
DebitCard{account='zhangsan', amount=40}
DebitCard{account='zhangsan', amount=50}
DebitCard{account='zhangsan', amount=60}
DebitCard{account='zhangsan', amount=70}
DebitCard{account='zhangsan', amount=80}
DebitCard{account='zhangsan', amount=90}
DebitCard{account='zhangsan', amount=100}

3.AtomicReference的非阻塞解决方案
synchronized是一种阻塞式的解决方案,同一时刻只能有一个线程真正在工作,其他线程都将陷入阻塞,因此这并不是一种效率很高的解决方案,这个时候就可以利用AtomicReference的非阻塞原子性解决方案提供更加高效的方式了

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

import static java.util.concurrent.ThreadLocalRandom.current;

/**
 * @author wyaoyao
 * @date 2021/4/19 17:19
 */
public class AtomicReferenceExample2 {


    /**
     * 定义AtomicReference并且初始值为DebitCard("zhangsan", 0)
     */
    private static AtomicReference<DebitCard> debitCardRef
            = new AtomicReference<>(new DebitCard("zhangsan", 0));

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread("T-" + i) {
                @Override
                public void run() {
                    // 获取AtomicReference的当前值
                    final DebitCard dc = debitCardRef.get();
                    // 基于AtomicReference的当前值创建一个新的DebitCard
                    DebitCard newDC = new DebitCard(dc.getAccount(),
                            dc.getAmount() + 10);
                    // 基于CAS算法更新AtomicReference的当前值
                    if(debitCardRef.compareAndSet(dc,newDC)){
                        // 更新成功
                        System.out.println(newDC);
                    }
                    try {
                        TimeUnit.MILLISECONDS.sleep(current().nextInt(20));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }.start();
        }
    }
}

在上面的程序代码中,我们使用了AtomicReference封装DebitCard的对象引用,每一次对AtomicReference的更新操作,我们都采用CAS这一乐观非阻塞的方式进行,因此也会存在对DebitCard对象引用更改失败的问题(更新时所持有的期望值引用有可能并不是AtomicReference所持有的当前引用,这也是第1小节中程序运行出现错误的根本原因。比如,A线程获得了DebitCard引用R1,在进行修改之前B线程已经将全局引用更新为R2,A线程仍然基于引用R1进行计算并且最终将全局引用更新为R1)。

CAS算法在此处就是要确保接下来要修改的对象引用是基于当前线程刚才获取的对象引用,否则更新将直接失败

DebitCard{account='zhangsan', amount=20}
DebitCard{account='zhangsan', amount=40}
DebitCard{account='zhangsan', amount=30}
DebitCard{account='zhangsan', amount=10}
DebitCard{account='zhangsan', amount=50}
DebitCard{account='zhangsan', amount=80}
DebitCard{account='zhangsan', amount=70}
DebitCard{account='zhangsan', amount=60}
DebitCard{account='zhangsan', amount=90}
DebitCard{account='zhangsan', amount=100}

控制台的输出显示账号的金额按照10的步长在增长,由于非阻塞的缘故,数值20的输出有可能会出现在数值10的前面,数值40的输出则出现在了数值30的前面,但这并不妨碍amount的数值是按照10的步长增长的。

AtomicReference所提供的非阻塞原子性对象引用读写解决方案,被应用在很多高并发容器中,比如ConcurrentHashMap

2 AtomicReference的基本用法

2.1 AtomicReference的构造

AtomicReference是一个泛型类,它的构造与其他原子类型的构造一样,也提供了无参和一个有参的构造函数。

// 当使用无参构造函数创建AtomicReference对象的时候,
// 需要再次调用set()方法为AtomicReference内部的value指定初始值。
AtomicReference()
// 创建AtomicReference对象时顺便指定初始值。
AtomicReference(V initialValue);

2.2 方法

/**
原子性地更新AtomicReference内部的value值,
其中expect代表当前AtomicReference的value值,update则是需要设置的新引用值。
该方法会返回一个boolean的结果,
当expect和AtomicReference的当前值不相等时,修改会失败,返回值为false,
若修改成功则会返回true。
**/
compareAndSet(V expect, V update)
// 原子性地更新AtomicReference内部的value值,并且返回AtomicReference的旧值。
getAndSet(V newValue)
// 原子性地更新value值,并且返回AtomicReference的旧值,该方法需要传入一个Function接口。
getAndUpdate(UnaryOperator<V> updateFunction)
// 原子性地更新value值,并且返回AtomicReference更新后的新值,该方法需要传入一个Function接口。
updateAndGet(UnaryOperator<V> updateFunction)
// 原子性地更新value值,并且返回AtomicReference更新前的旧值。
// 该方法需要传入两个参数,第一个是更新后的新值,第二个是BinaryOperator接口。
getAndAccumulate(V x, BinaryOperator<V> accumulatorFunction)
// 原子性地更新value值,并且返回AtomicReference更新后的值。
// 该方法需要传入两个参数,第一个是更新的新值,第二个是BinaryOperator接口。
accumulateAndGet(V x, BinaryOperator<V> accumulatorFunction)
// 获取AtomicReference的当前对象引用值。
get()
// 设置AtomicReference最新的对象引用值,该新值的更新对其他线程立即可见。
set(V newValue)
// 设置AtomicReference的对象引用值。
lazySet(V newValue)

你可能感兴趣的:(JAVA,AtomicReference,cas)