1 2 3 4

【JUC】原子操作

juc包下的原子类

针对基础类型地原子性读写而设计的原子类:

    AtomicBoolean
    AtomicInteger
    AtomicIntegerArray
    AtomicIntegerFieldUpdater<T>
    AtomicLong
    AtomicLongArray
    AtomicLongFieldUpdater<T>

针对引用类型地原子性读写而设计的原子类:

    AtomicReference<V>    
    AtomicReferenceArray<E>
    AtomicReferenceFieldUpdater<T,V>
    AtomicMarkableReference<V>
    AtomicStampedReference<V>

 

原子类实现原理:

  主要是依赖于 sun.misc.Unsafe 提供的一些 native 方法保证操作的原子性。  

  采用了基于volatile关键字+CAS算法无锁化的操作方式来确保共享数据在多线程下的线程安全。

CAS算法:

  通俗来说就是先比较,再更新(交换)。

ABA问题:

  假设线程1将一个变量A变成了B,紧接着又从B变成了A(完璧归赵);对于线程2来说,拿到的数据虽然是A,但自己不知道该数据已经被修改过了(A已经脏了,对于有洁癖地线程1来说是不能忍的),这就是CAS算法的ABA问题。

 

AtomicReference

功能概述:

  该类提供了对象引用的非阻塞原子性读写操作。

源码:

public class AtomicReference<V> implements java.io.Serializable {
    private static final long serialVersionUID = -1848883965231344442L;
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;
    //volatile修饰了一个泛型的value属性
    private volatile V value;
}

  Unsafe 的 objectFieldOffset 方法可以获取成员属性在内存中的地址相对于对象内存地址的偏移量。这个偏移量也就是 valueOffset ,说得简单点就是找到这个变量在内存中的地址,便于后续通过内存地址直接进行操作。

  value 就是 AtomicReference 中的实际值,因为有 volatile ,这个值实际上就是内存值。

  不同之处就在于 AtomicInteger 是对整数的封装,而 AtomicReference 则对应普通的对象引用。也就是它可以保证你在修改对象引用时的线程安全性。

方法:

get()方法 :

  获取当前 AtomicReference 的值。

set() 方法:

  设置当前 AtomicReference 的值。

  get() 可以原子性的读取 AtomicReference 中的数据,set() 可以原子性的设置当前的值,因为 get() 和 set() 最终都是作用于 value 变量,而 value 是由 volatile 修饰的,所以 get 、set 相当于都是对内存进行读取和设置。如下图所示:

lazySet()方法:

     volatile 有内存屏障你知道吗?

  内存屏障是什么呢?

  内存屏障,也称内存栅栏,内存栅障,屏障指令等, 是一类同步屏障指令,是 CPU 或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作。也是一个让CPU 处理单元中的内存状态对其它处理单元可见的一项技术。

  CPU 使用了很多优化,使用缓存、指令重排等,其最终的目的都是为了性能,也就是说,当一个程序执行时,只要最终的结果是一样的,指令是否被重排并不重要。所以指令的执行时序并不是顺序执行的,而是乱序执行的,这就会带来很多问题,这也促使着内存屏障的出现。

  语义上,内存屏障之前的所有写操作都要写入内存;内存屏障之后的读操作都可以获得同步屏障之前的写操作的结果。因此,对于敏感的程序块,写操作之后、读操作之前可以插入内存屏障。

  内存屏障的开销非常轻量级,但是再小也是有开销的,LazySet 的作用正是如此,它会以普通变量的形式来读写变量。

  也可以说是:懒得设置屏障了。

 

getAndSet() 方法

  以原子方式设置为给定值并返回旧值。它的源码如下:

  它会调用 unsafe 中的 getAndSetObject 方法,源码如下:

  可以看到这个 getAndSet 方法涉及两个 cpp 实现的方法,一个是 getObjectVolatile ,一个是 compareAndSwapObject 方法,他们用在 do...while 循环中,也就是说,每次都会先获取最新对象引用的值,如果使用 CAS 成功交换两个对象的话,就会直接返回 var5 的值,var5 此时应该就是更新前的内存值,也就是旧值。

compareAndSet() 方法

  这就是 AtomicReference 非常关键的 CAS 方法了,与 AtomicInteger 不同的是,AtomicReference 是调用的 compareAndSwapObject ,而 AtomicInteger 调用的是 compareAndSwapInt 方法。这两个方法的实现如下:

  我们之前解析过 AtomicInteger 的源码,所以我们接下来解析一下 AtomicReference 源码。

  因为对象存在于堆中,所以方法 index_oop_from_field_offset_long 应该是获取对象的内存地址,然后使用 atomic_compare_exchange_oop 方法进行对象的 CAS 交换。

这段代码会首先判断是否使用了 UseCompressedOops,也就是指针压缩。

这段代码会首先判断是否使用了 UseCompressedOops,也就是指针压缩。

这里简单解释一下指针压缩的概念:JVM 最初的时候是 32 位的,但是随着 64 位 JVM 的兴起,也带来一个问题,内存占用空间更大了 ,但是 JVM 内存最好不要超过 32 G,为了节省空间,在 JDK 1.6 的版本后,我们在 64位中的 JVM 中可以开启指针压缩(UseCompressedOops)来压缩我们对象指针的大小,来帮助我们节省内存空间,在 JDK 8来说,这个指令是默认开启的。

如果不开启指针压缩的话,64 位 JVM 会采用 8 字节(64位)存储真实内存地址,比之前采用4字节(32位)压缩存储地址带来的问题:

  • 增加了 GC 开销:64 位对象引用需要占用更多的堆空间,留给其他数据的空间将会减少,从而加快了 GC 的发生,更频繁的进行 GC。
  • 降低 CPU 缓存命中率:64 位对象引用增大了,CPU 能缓存的 oop 将会更少,从而降低了 CPU 缓存的效率。

由于 64 位存储内存地址会带来这么多问题,程序员发明了指针压缩技术,可以让我们既能够使用之前 4 字节存储指针地址,又能够扩大内存存储。

可以看到,atomic_compare_exchange_oop 方法底层也是使用了 Atomic:cmpxchg 方法进行 CAS 交换,然后把旧值进行 decode 返回。

 

非线程安全的代码测试:

public static void main(String[] args) {
        Prize prize = new Prize("小米汽车", 100);
        AtomicInteger atomicInteger = new AtomicInteger();
        IntStream.range(0, 300).forEach(
                value -> {
                    new Thread(
                            () -> {
                                //①获得当前还剩多少号
                                int count = prize.getCount();
                                if (count > 0) {
                                    //②对剩余号源减1,并更新回奖池
                                    prize.setCount(count - 1);
                                    atomicInteger.incrementAndGet();
                                    log.info("当前线程:{},抢到了 {} 号", Thread.currentThread().getName(), count);
                                }
                            }
                    ).start();
                }
        );
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.info("中奖人数:{}", atomicInteger.get());
    }
以上代码存在线程不安全。其不安全的本质是:当线程1、线程2同时执行到处,假如都获得了当前剩余号数10,继续往下执行到处,都对其进行了减1,最终两个线程更新回去却是9;针对这种情况我们有很多解决方案,这里我选择使用AtomicReference类进行测试:
 
首先定义需要引用的奖品类:
//定义我们的奖品类
@Data
public class Prize {

    /**
     * 一等奖:小米汽车
     */
    private String level;

    /**
     * 数量
     */
    private int count;

    public Prize(String level, int count) {
        this.level = level;
        this.count = count;
    }
}

接下来对该类创建的对象做引用类型的原子性操作:

public static void main(String[] args) {
        //将我们的初始奖池封装到AtomicReference中
        AtomicReference<Prize> reference = new AtomicReference<>(new Prize("小米汽车", 100));
        AtomicInteger atomicInteger = new AtomicInteger(0);
        IntStream.range(0, 300).forEach(
                value -> {
                    new Thread(
                            () -> {
                                //①获得当前还剩多少号的对象(使用 AtomicReference.get 获取)
                                final Prize prize = reference.get();
                                if (prize.getCount() > 0) {
                                      //②对剩余号源进行减1
                                    Prize prizeNew = new Prize(prize.getLevel(), reference.get().getCount() - 1);
                                      //③将数据更新到奖池(使用 CAS 乐观锁进行非阻塞更新)
                                    if (reference.compareAndSet(prize, prizeNew)) {
                                        log.info("当前线程:{},抢到了 {} 号", Thread.currentThread().getName(), prize.getCount());
                                        atomicInteger.incrementAndGet();
                                    }
                                }
                            }
                    ).start();
                }
        );
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.info("中奖人数:{}", atomicInteger.get());
    }
观察上面代码:虽然①②处也会出现之前基础版本的情况,但是最终将数据刷新回奖池的时候,如果prize对象的引用已经被其他线程修改,则当前线程执行**reference.compareAndSet(prize, prizeNew)**会更新失败。对于这个线程来说,好气呀,手都伸进抽奖箱了,还是没有抢到大奖;对于老板来说无伤大雅,只管送出指定数量即可;针对这种可以搞一个while循环让线程进行重试(摸一次就可以了嘛,还想摸多少次?)

 

AtomicStampedReference

  AtomicStampedReference就是来解决ABA问题的;在数据库操作的过程中,我们也曾使用过乐观锁版本号对其进行ABA问题的解决;AtomicStampedReference也是通过增加版本号的方式。

构造函数:

public AtomicStampedReference(V initialRef, int initialStamp) {
  pair = Pair.of(initialRef, initialStamp);
}

构造函数中initialStamp属性就是一个需要维护的版本号;

特别注意:此版本号需要应用程序自身去负责,AtomicStampedReference并没有提供安全性操作。

其API相对简单,大家可以去尝试着用用。

 

AtomicArray

  该类提供了对数组数据类型的原子操作: AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray

测试代码:

@Test
public void addTest() {
  int[] intArray = {1, 2, 3, 4, 5, 6};
  AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(intArray);

  //对索引为2的元素增加10
  assert atomicIntegerArray.addAndGet(2, 10) == 13;
  //获得索引为2的元素
  assert atomicIntegerArray.get(2) == 13;
}

该类可以对某个索引的操作是原子性的。

 

AtomicFieldUpdater

  该类提供了对象属性的原子性更新的操作。

测试代码:

public class AtomicFieldUpdaterTest01 {

    @Data
    public static class User {
        private String name;
        volatile int money;

        public User() {
        }

        public User(String name, int money) {
            this.name = name;
            this.money = money;
        }
    }
    
    public static void main(String[] args) {
        AtomicIntegerFieldUpdater<User> updater =
                AtomicIntegerFieldUpdater.newUpdater(User.class, "money");
        User user = new User("张三", 100);
        assert 120 == updater.addAndGet(user, 20);
        System.out.println(updater.addAndGet(user, 20)==120);;
    }

}

代码结果为true。

 AtomicIntegerFieldUpdater.newUpdater(User.class, "money");

传入需要进行原子操作的类"User.class"和需要原子操作的字段"money",然后调用updater的API,所对"money"字段的操作均是原子性的。

特别注意的是,要满足原子操作的属性要求还很高:

  1. 字段没有被volatile修饰无法被原子性地更新(volatile修饰后线程可见)
  2. 类变量无法被原子性地更新(即字段不能被static修饰)
  3. 无法直接访问的成员变量属性不能被原子性地更新(即字段不能被private修饰)
  4. final修饰的字段不能被原子性地更新
  5. 父类的成员属性无法被原子性地更新

 

 

 

原文链接:并发编程之AtomicReference - 掘金 (juejin.cn)

「进阶」AtomicReference的使用场景 - 百里浅暮 - 博客园 (cnblogs.com)

posted @ 2023-05-09 13:37  日月星宿  阅读(10)  评论(0编辑  收藏  举报