• 【多线程与并发】Java中的12个原子操作类


    从JDK1.5开始,Java提供了java.util.concurrent.atomic包,该包中的原子操作类提供了一种使用简单、性能高效(使用CAS操作,无需加锁)、线程安全地更新一个变量的方式。

    `java.util.concurrent.atomic`包中的类.png

    根据变量类型的不同,Atomic包中的这12个原子操作类可以分为4种类型:
    ①原子更新基本类型AtomicBoolean、AtomicInteger、AtomicLong
    ②原子更新数组AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
    ③原子更新引用AtomicReference、AtomicReferenceFiledUpdater、AtomicMarkableReference
    ④原子更新字段(属性)AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicStampedReference
    它们都是使用Unsafe实现的包装类。


    原子更新基本类型类

    使用原子的方式更新基本类型,Atomic包提供了以下3个类:
    AtomicBoolean
    AtomicInteger
    AtomicLong
    这三个类提供的方式几乎是一模一样,下面以AtomicInteger为例进行讲解,AtomicInteger的常用的方法如下:

    int addAndGet(int delta)
    以原子方式将AtomicInteger的value设置为:delta + 原value,返回更新后的值(即delta + 原value)
    
    boolean compareAndSet(int expect, int update)
    以原子的方式,如果AtomicInteger的当前值是expect,则将AtomicInteger的值设置为update
    
    int getAndIncrement()
    以原子的方式将AtomicInteger的当前值加1,注意:返回的是加1前的值
    
    void lazySet(int newValue)
    最终将AtomicInteger设置为newValue(使用lazySet设置值后,其他线程可能在之后的一段时间内还是可以读到旧的值)
    
    int getAndSet(int newValue)
    以原子的方式将AtomicInteger设置为newValue
    

    基本使用方式示例:

    public void test(){
        Executor executor = Executors.newFixedThreadPool(3);
        AtomicInteger atomicInteger = new AtomicInteger(0);
    
        for(int i = 0; i < 10; i++){
            executor.execute(()->{
                System.out.println("atomicInteger的当前值:" + atomicInteger.addAndGet(1));
            });
        }
    }
    
    //输出如下(输出顺序可能不同,但结果一定是正确的)
    atomicInteger的当前值:1
    atomicInteger的当前值:2
    atomicInteger的当前值:4
    atomicInteger的当前值:5
    atomicInteger的当前值:3
    atomicInteger的当前值:7
    atomicInteger的当前值:6
    atomicInteger的当前值:9
    atomicInteger的当前值:8
    atomicInteger的当前值:10
    

    其内部实现原子操作的原理是通过UnSafe类的CAS操作。//TODO 具体实现
    其他Java的基本类型均可以使用类似的思路实现。


    原子更新数组

    通过原子的方式更新数组里的某个元素,Atomic包提供了以下3个类:
    AtomicIntegerArray:原子更新整型数组里的元素
    AtomicLongArray:原子更新长整型数组里的元素
    AtomicReferenceArray:原子更新引用类型数组里的元素

    以AtomicIntegerArray为例,主要是提供以原子的方式更新数组里的整型元素,其主要方法如下:

    int addAndGet(int i, int delta)
    以原子的方式将数组中i位置处的元素值加上delta,返回:i位置处的元素的旧值+ delta
    
    boolean compareAndSet(int i, int expect, int update)
    如果当前值等于预期值(数组i位置处的元素),则以原子的方式将数组i位置处的元素值设置为update
    

    使用示例:

    public void testAtomicIntegerArray() {
    
        int[] originArray = new int[]{1, 2, 3};
    
        AtomicIntegerArray array = new AtomicIntegerArray(originArray);
    
        array.getAndSet(0, 8);
        System.out.println(array.get(0));
        System.out.println(originArray[0]);
    }
    
    //输出结果:
    8
    1   ----注意这里,构造方法中是将原数组复制了一份,所以对AtomicIntegerArray的操作,不会影响原数组
    

    原子更新引用类型

    如果要原子更新多个变量,就需要使用原子更新引用类型,Atomic提供了3个类:
    AtomicReference:原子更新引用类型
    AtomicReferenceFiledUpdater:原子更新引用类型里的字段
    AtomicMarkableReference:原子更新带有标记位的引用类型

    AtomicReference为例,演示代码如下:

    class AtomicReferenceExample{
    
        private  AtomicReference<User> userAtomicReference = new AtomicReference<>();
    
        @Test
        public void test(){
            User originUser = new User(18, "小岳");
            userAtomicReference.set(originUser);
            User updateUser = new User(28, "老岳");
            userAtomicReference.compareAndSet(originUser, updateUser);
            System.out.println(userAtomicReference.get().getName() + ":" + userAtomicReference.get().getAge());
        }
    
        class User{
            private String name;
            private int age;
    
            public User(int age, String name){
                this.name = name;
                this.age = age;
            }
    
            public int getAge() {
                return age;
            }
    
            public String getName() {
                return name;
            }
        }
    }
    
    //输出结果如下
    老岳:28
    

    原子更新字段类

    如果需要原子地更新某个类里的某个字段,就需要使用原子更新字段类,Atomic包提供了一下3个类进行原子字段更新。
    AtomicIntegerFieldUpdater:原子更新整型的字段
    AtomicLongFieldUpdater:原子更新长整型的字段
    AtomicStampedReference:原子更新带有版本号的引用类型(可以解决CAS操作的ABA问题

    使用更新字段类必须使用静态方法newUpdater(Class<U> tclass, String fieldName)创建一个更新器(同时指定要更新的类和该类中的要更新的字段名),并且该字段必须用public volatile修饰

    AtomicIntegerFieldUpdater为例,演示代码如下:

    class AtomicIntegerFieldUpdaterExample{
    
        private AtomicIntegerFieldUpdater<User> fieldUpdater = AtomicIntegerFieldUpdater.newUpdater(User.class, "age");
    
        @Test
        public void test(){
            User user = new User(18, "小岳");
            fieldUpdater.addAndGet(user, 10);
            System.out.println("user现在的年龄:" + fieldUpdater.get(user));
        }
    
        class User{
            private String name;
            public volatile int age;
    
            public User(int age, String name){
                this.name = name;
                this.age = age;
            }
    
            public int getAge() {
                return age;
            }
    
            public String getName() {
                return name;
            }
        }
    }
    
    //输出结果如下
    user现在的年龄:28
    

    理解循环CAS

    Java中可以通过两种方式来实现原子操作:加锁和循环CAS。
    Java中使用循环CAS实现原子操作的实例:

    //原子操作类AtomicInteger的incrementAndGet实现(最新源码已非如此,但思想是一致的)
    public final int incrementAndGet() {
        for (;;) {//一直循环
            int current = get();//取出AtomicInteger当前的内存值
            int next = current + 1;//要设置的新值
            if (compareAndSet(current, next)){//用CAS操作更新AtomicInteger
                return next;
            }
        }
    }
    

    boolean compareAndSet(int current, int newValue)方法是一个原子方法,该方法首先会先检查AtomicInteger的当前数值(从内存中读取,即CAS操作中内存值)是否等于current(即CAS操作中预期值),如果等于,则说明AtomicInteger的值没有被其他线程修改过,则会将AtomicInteger的值设置为newValue(即CAS操作中要修改的新值),并返回true;如果不等于,说明AtomicInteger的值被其他线程修改过,则返回fasle

    CAS实现原子操作的三个问题

    ①ABA问题
    因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。
    ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。
    从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

    ②循环时间长开销大
    自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。

    ③只能保证一个共享变量的原子操作
    当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。


    参考

    内容全部来自《Java并发编程的艺术》以及上述书籍作者的聊聊并发(五)——原子操作的实现原理


    作者:maxwellyue
    链接:https://www.jianshu.com/p/712681f5aecd
    来源:简书
    简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。
  • 相关阅读:
    马的遍历 new
    锤子剪刀布 new
    npm 打包 new
    Linux 锁机制
    ubuntu virtualbox 下安装xp,识别usb
    ubuntu设置快捷键
    linux神奇的系统请求系统救命草
    linux 内核动态内存分配测试(纯属娱乐哈)
    C之绝妙(一道很NB的面试题)
    虚拟机virtualbox:Could not find an open hard disk with UUID {368441269e88468698582d1a0568f53c}.
  • 原文地址:https://www.cnblogs.com/xiaoshen666/p/11258541.html
Copyright © 2020-2023  润新知