美文网首页
JAVA 多线程与高并发学习笔记(九)——JUC原子类

JAVA 多线程与高并发学习笔记(九)——JUC原子类

作者: 简单一点点 | 来源:发表于2022-07-29 17:17 被阅读0次

    在多线程并发中,诸如“++”或“--”元素不具备原子性,不是线程安全的,需要使用JDK中的JUC原子类

    Atomic 原子操作包

    JUC中的 java.util.concurrent.atomic 类的原子类主要分为以下四种:

    • 基本原子类:
      • AtomicInteger:整型原子类。
      • AtomicLong:长整型原子类。
      • AtomicBoolean:布尔型原子类。
    • 数组原子类:
      • AtomicIntegerArray:整型数组原子类。
      • AtomicLongArray:长整型数组原子类。
      • AtomicReferenceArray:引用类型数组原子类。
    • 引用原子类:
      • AtomicReference:引用类型原子类。
      • AtomicMarkableReference:带有更新标记位的原子引用类型。
      • AtomicStampedReference:带有更新版本号的原子引用类型。
    • 字段更新原子类:
      • AtomicIntegerFieldUpdater:原子更新整型字段的更新器。
      • AtomicLongFieldUpdater:原子更新长整型字段的更新器。
      • AtomicRefernceFieldUpdater:原子更新引用类型中的字段。

    基础原子类 AtomicInteger

    基础原子类以 AtomicInteger 为例来进行介绍。常用方法如下:

    // 获取当前的值
    public final int get();
    
    // 获取当前的值,然后设置新的值
    public final int getAndSet(int newValue);
    
    // 获取当前的值,然后自增
    public final int getAndIncrement();
    
    // 获取当前的值,然后自减
    public final int getAndDecrement();
    
    // 获取当前的值,并加上预期的值
    public final int getAndAdd(int delta);
    
    // 通过CAS方式设置整数值
    boolean compareAndSet(int expect, int update);
    

    下面是一个简单的实践例子:

     @Test
    public void atomicIntegerTest() {
        int tempVal = 0;
    
        // 定义一个整数原子类实例,赋值到变量i
        AtomicInteger i = new AtomicInteger(0);
    
        // 取值,然后设置一个新值
        tempVal = i.getAndSet(3);
        System.out.println("tempVal: " + tempVal + "; i:" + i.get());
    
        // 取值,然后自增
        tempVal = i.getAndIncrement();
        System.out.println("tempVal: " + tempVal + "; i:" + i.get());
    
        // 取值,然后增加5
        tempVal = i.getAndAdd(5);
        System.out.println("tempVal: " + tempVal + "; i:" + i.get());
    
        // CAS交换
        boolean flag = i.compareAndSet(9, 100);
        System.out.println("flag: " + flag + "; i:" + i.get());
    }
    

    数组原子类AtomicIntegerArray

    AtomicIntegerArray 为例介绍数组原子类。包含如下常用方法:

    // 获取索引为i的元素
    public final int get(int i);
    
    // 获取索引为i的元素,并将其设置为新值 newValue
    public final int getAndSet(int i, int newValue);
    
    // 获取索引为i的元素,并让该位置的元素自增
    public final int getAndIncrement(int i);
    
    // 获取索引为i的元素,并让该位置的元素自减
    public final int getAndDecrement(int i);
    
    // 获取索引为i的元素,并加上预期的值
    public final int getAndAdd(int i, int delta);
    
    // 如果输入的值等于预期值,则将位置i的元素设置为输入值update
    boolean compareAndSet(int i, int expect, int update);
    
    // 最终将索引为IDE元素设置为newValue
    public final void lazySet(int i, int newValue);
    

    下面看一个实践小例子:

    @Test
    public void testAtomicIntegerArray() {
        int tempVal = 0;
        int[] array = {1, 2, 3, 4, 5, 6};
    
        // 包装为原子数组
        AtomicIntegerArray i = new AtomicIntegerArray(array);
        // 获取第0个元素并设置为2
        tempVal = i.getAndSet(0, 2);
        System.out.println("tempVal: " + tempVal + "; i:" + i);
    
        // 获取第0个元素,然后自增
        tempVal = i.getAndIncrement(0);
        System.out.println("tempVal: " + tempVal + "; i:" + i);
    
        // 获取第0个元素,然后增加 5
        tempVal = i.getAndAdd(0, 5);
        System.out.println("tempVal: " + tempVal + "; i:" + i);
    }
    

    AtomicInteger 线程安全原理

    基础原子类主要通过CAS自旋和volatile实现,下面以 AtomicInteger 源码为例分析一下。

    public class AtomicInteger extends Number implements java.io.Serializable {
        private static final long serialVersionUID = 6214790243416807050L;
    
        // Unsafe类实例
        private static final Unsafe unsafe = Unsafe.getUnsafe();
    
        // value属性值的地址偏移量
        private static final long valueOffset;
    
        static {
            try {
                // 计算 value 属性值的地址偏移量
                valueOffset = unsafe.objectFieldOffset
                    (AtomicInteger.class.getDeclaredField("value"));
            } catch (Exception ex) { throw new Error(ex); }
        }
    
        // 内部 value值,使用volatile保证线程可见性
        private volatile int value;
    
        // 初始化
        public AtomicInteger(int initialValue) {
            value = initialValue;
        }
    
        public AtomicInteger() {
        }
    
        // 获取当前 value 值
        public final int get() {
            return value;
        }
    
        // 设置值
        public final void set(int newValue) {
            value = newValue;
        }
    
    
    
        // 返回旧值赋予新值
        public final int getAndSet(int newValue) {
            return unsafe.getAndSetInt(this, valueOffset, newValue);
        }
    
        // 封装底层CAS操作
        // 对比expect和value,不同返回false,相同则将新值赋予给value,并返回true
        public final boolean compareAndSet(int expect, int update) {
            return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
        }
    
    
        // 安全自增
        public final int getAndIncrement() {
            return unsafe.getAndAddInt(this, valueOffset, 1);
        }
    
        ...
    

    可以看到,其中的主要方法都通过 CAS 自旋实现,CAS自旋的主要操作为:如果一次CAS操作失败,获取最新的 value 值,再次进行 CAS 操作,直到成功。

    其中的内部成员 value 使用了关键字 volatile 进行修饰,保证任何时刻总能拿到该变量的最新值,其目的在于保障变量值的线程可见性。

    引用类型原子类

    下面以 AtomicReference 为例来介绍一下引用类型原子类。

    创建一个简单的对象类。

    public class User implements Serializable {
    
        private String uid;
        private String nickName;
        public volatile int age;
    
        public User(String uid, String nickName) {
            this.uid = uid;
            this.nickName = nickName;
        }
    
        public String getUid() {
            return uid;
        }
    
        public void setUid(String uid) {
            this.uid = uid;
        }
    
        public String getNickName() {
            return nickName;
        }
    
        public void setNickName(String nickName) {
            this.nickName = nickName;
        }
    
        @Override
        public String toString() {
            return "User{" +
                    "uid='" + uid + '\'' +
                    ", nickName='" + nickName + '\'' +
                    ", age=" + age +
                    '}';
        }
    }
    

    然后使用 AtomicReference 修改类的引用。

    @Test
    public void testAtomicReference() {
        AtomicReference<User> userRef = new AtomicReference<User>();
        User user = new User("1", "张三");
        userRef.set(user);
        System.out.println("userRef is:" + userRef.get());
    
        User updateUser = new User("2", "李四");
        boolean success = userRef.compareAndSet(user, updateUser);
        System.out.println("cas result is:" + success);
        System.out.println("after cas, userRef is:" + userRef.get());
    }
    

    可以得到输出:

    userRef is:User{uid='1', nickName='张三', age=0}
    cas result is:true
    after cas, userRef is:User{uid='2', nickName='李四', age=0}
    

    需要注意的是,要通过 compareAndSet 方法来修改 AtomicReference 类型的引用包装值。

    另外,还要说明,使用原子引用,只能保障对象引用的原子操作,对被包装对象的字段值修改时不能保证原子性。

    属性更新原子类

    这里以 AtomicIntegerFieldUpdater 为例进行介绍。属性更新原子类保障属性安全更新的流程大致需要两步:

    1. 更新的对象属性必须使用 public volatile 修饰符。
    2. 因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须调用静态方法 newUpdater 创建一个更新器,并且需要设置想要更新的类和属性。

    下面是一个简单的实战小例子。

    @Test
    public void testAtomicIntegerFieldUpdater() {
        // 创建更新器
        AtomicIntegerFieldUpdater<User> updater = AtomicIntegerFieldUpdater.newUpdater(User.class,
                "age");
        User user = new User("1", "张三");
        // 更新user的age属性
        System.out.println(updater.getAndIncrement(user));
        System.out.println(updater.getAndAdd(user, 100));
    
        System.out.println(updater.get(user));
    }
    

    ABA问题

    CAS操作如果使用不合理,会存在 ABA 问题。

    ABA 问题介绍

    通过一个例子来说明 ABA 问题。

    一个线程A 从内存位置M中取出V1,另一个线程B也取出V1。现在假设线程B进行了一些操作之后将M位置的数据V1变成了V2,然后又有一些操作之后将V2变成了V1。之后,线程A进行CAS操作,发现M位置的数据仍然是V1,然后线程A操作成功。

    尽管线程A的CAS操作陈宫,但是A操作的数据V1可能已经不是之前的V1,而是被线程B替换过的V1,这就是ABA问题。

    ABA问题解决方案

    很多乐观锁的实现版本都是使用版本号(Version)方式来解决ABA问题。乐观锁每次在执行数据的修改操作时都会带上一个版本号,版本号和数据的版本号一致就可以执行修改操作并对版本号执行加1操作,否则执行失败。因为每次操作的版本号都会随之增加,所以不会出现ABA问题。

    使用 AtomicStampedReference 解决ABA问题

    参考乐观锁的版本号,JDK提供了一个 AtomicStampedReference 类来解决ABA问题。它在CAS的基础上增加了一个Stamp,用来观察数据是否发生变化。

    AtomicStampedReferencecompareAndSet() 方法首先检查当前的对象引用值是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,就以原子方式将引用值和标志的值更新为给定的更新值。

    AtomicStampReference 的构造器有两个参数,具体如下:

    AtomicStampedReference(V initialRef, int initialStamp);
    

    包含常用方法如下:

    //获取被封装的数据
    public V getReference();
    
    // 获取被封装的数据的版本标志
    public int getStamp();
    

    CAS 操作定义如下:

    public boolean compareAndSet {
        V expectedReference,   // 预发引用值
        V newReference,  // 更新后的引用值
        int expectedStamp, // 预期标志
        int newStamp // 更新后的标志
    }
    

    使用 AtomicMarkableReference 解决ABA 问题

    AtomicMarkableReferenceAtomicStampedReference 的简化版,只关心是否修改过,其标记 markboolean 类型。

    AtomicMarkableReference 适用于只要知道对象是否被修改过,而不适用于对象被反复修改的场景。

    相关文章

      网友评论

          本文标题:JAVA 多线程与高并发学习笔记(九)——JUC原子类

          本文链接:https://www.haomeiwen.com/subject/mqyqwrtx.html