• 那些Java中原子类的使用



    原子类都是线程安全的,所以这里没有用加入多线程来验证,只是简单api调用,毕竟先要会用嘛。

    1. 原子整型

    1.1 cas操作

    写在开头,我以原子整型为例,先介绍一个重要的方法compareAndSet,其他方法其实都是这个方法的拓展。

    这个方法其实就是CAS操作,比较并设置值,但我们需要反复比较预期值和最新值是否相同,如果比对相同,才可以将值更新,否则会再次进入循环,判断。

    public class Demo1 {
        public static void main(String[] args) {
            AtomicInteger ai = new AtomicInteger();
            set(ai, 10);
            System.out.println(ai.get());//10
        }
    
        /**
         * 传入原子整型 将值更新
         * @param ai 原子整型
         * @param value 要修改的值
         */
        public static void set(AtomicInteger ai,int value) {
            while (true) {
                int except = ai.get();
                //比较并设值
                if (ai.compareAndSet(except,value)) {
                    break;
                }
            }
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    从代码中我们可以看到,ai被正确的更新了,而如果在多线程下,进入循环,except变量其实获取的不一定是最新的值,那么compareAndSet就要返回false,然后再次循环了。

    了解了这个方法,其实变量的加减乘除都可以用这个方法来实现,但其实jdk中已经为我们提供了,来看一下吧。

    1.2 加减操作

    public class Demo1 {
        public static void main(String[] args) {
            //提前说明 这些方法都是线程安全的
            AtomicInteger ai = new AtomicInteger(0);//默认是0
            System.out.println(ai.incrementAndGet());//输出1 实际为1
            System.out.println(ai.getAndIncrement());//输出1 实际为2
            System.out.println(ai.decrementAndGet());//输出1 实际为1
            System.out.println(ai.getAndDecrement());//输出1 实际为0
            System.out.println(ai.get());//输出0
    
            System.out.println(ai.addAndGet(10));//输出10 实际为10
            System.out.println(ai.getAndAdd(10));//输出10 实际为20
            System.out.println(ai.get());//输出20
    
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    总结一下这几个方法的规律。

    以加法为例,incrementAndGet就是线程安全版的++i,即先对值加一,然后获取值。
    getAndIncrement就是线程安全版的i++,即先获取值,在对值加一。
    其他方法也是类似,这里不多做介绍了,一般我们都是先加再获取用的较多。

    1.3 更新操作

    public class Demo1 {
        public static void main(String[] args) {
            //提前说明 这些方法都是线程安全的
            AtomicInteger ai = new AtomicInteger(10);//默认是0
            System.out.println(ai.updateAndGet(x -> x * 10));//输出100
            System.out.println(ai.getAndUpdate(x -> x / 10));//输出为100
            System.out.println(ai.get());//输出10
            System.out.println(ai.accumulateAndGet(10, (x1, x2) -> x1 * x2));//输出100
            System.out.println(ai.getAndAccumulate(10, (x1, x2) -> x1 - x2));//输出100
            System.out.println(ai.get());//输出90
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    这里可以分为两个api,一个是update,一个是accumulate,而先获取再计算还是先计算再获取相信大家根据方法名称就能搞明白了。

    updateAndGet方法接受IntUnaryOperator类型参数,那这玩意是什么呢?

    其实就是一个函数式接口,点进去看。
    在这里插入图片描述
    可以看到就是接受一个int类型参数,以lambda的方式返回一个整型即可,所以就跟上面一样,我对传入的值做了乘10的操作,当然也可以用除法。

    accumulateAndGet方法接受的是两个参数,第一个参数就是计算所需要的值x,第二个参数和上面类似,但需要接受两个传参,是IntBinaryOperator类型。
    在这里插入图片描述
    这里需要说明,一般我们用在lambda表达式中的第一个参数,即left,代表的是AtomicInteger原本的值,而不是传入需要计算的值x,顺序不要颠倒即可,个人认为用带update的方法就足够了。

    PS:因为Long,Boolean类型的使用比较简单,这里就不做演示了,掌握一个Integer另外的也能掌握了。

    2. 原子引用

    原子引用是针对一些除了整型外的其他类型,如果要进行更新而提供的,一些字符串,浮点类型的更新,就需要用到原子引用了,当然,原子引用里也可以使用Integer,当成原子整型来用,但一般人做不成这事。。。

    public class Demo1 {
        public static void main(String[] args) {
            AtomicReference<String> reference = new AtomicReference<>();
            System.out.println(reference.updateAndGet(s -> "hello bb"));//直接更新成功
            
            String bb = "bb";
            //带版本号的原子引用
            AtomicStampedReference<String> stampedReference = new AtomicStampedReference<>(bb, 0);
            if (stampedReference.compareAndSet(bb, "cc", 0, 1)) {
                System.out.println(stampedReference.getReference());//cc
                System.out.println(stampedReference.getStamp());//1
            }
    
            AtomicMarkableReference<String> markableReference = new AtomicMarkableReference<>(bb, false);
            if (markableReference.compareAndSet(bb, "cc", false, true)) {
                System.out.println(markableReference.getReference());//cc
                System.out.println(markableReference.isMarked());//true
            }
        }
    
    }
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    因为代码较少,所以写在一起了。

    首先看使用AtomicReference的,我们可以通过updateAndGet直接更新字符串,内部会有自旋比较是否是最新值的。

    而为什么原子引用又多出另外两个类呢?

    因为涉及ABA问题,我来讲个小故事吧。

    比如张三是财务,原先公司账上有100w,私自拿去炒股了,结果还赚了20%,再把100w钱转了回去,而公司其他人对此是没有感知的,你说危不危险?

    相当于是 100w->0->100w

    这就是所谓的ABA问题,所以我们可以通过增加版本号的方式,每次修改,版本号+1,这样一来,数值的每次修改都会和原先你获取的版本号做对比,一旦对不上,那将无法修改这条数据,说回张三,即便他想把钱转回去,老板也会发现记录不一致,张三在劫难逃。

    所以你可以看到在代码中,stampedReference.compareAndSet,传入四个参数。
    在这里插入图片描述
    预期值,新值,预期版本号,新版本号,如果对的上才能够修改,否则修改失败。

    AtomicMarkableReference是干嘛的呢?

    它的功能没那么强大,只是为了解决数据是否修改问题,如果数据修改了,增加一个为true的标记,而下次就不能再修改了。

    对比来讲,我觉得AtomicStampedReference功能更加强大一些。

    3. 原子数组

    对于原子数组还是稍微提一下,可以多个线程对该数组进行操作,不会有线程安全问题,但个人认为用一些线程安全的集合来的比较方便,这个用的会少一些。

    public class Demo1 {
        public static void main(String[] args) {
            AtomicLongArray atomicLongArray = new AtomicLongArray(10);
            atomicLongArray.set(0, 10L);
            AtomicReferenceArray<String> stringAtomicReferenceArray = new AtomicReferenceArray<>(10);
            stringAtomicReferenceArray.set(1, "hello");
            System.out.println(atomicLongArray.get(0));//10
            System.out.println(stringAtomicReferenceArray.get(1));//hello
        }
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    4. 原子更新器

    可以对对象中的单字段更新,支持AtomicIntegerFieldUpdaterAtomicLongFieldUpdater,但这里用引用类型做演示。

    public class Demo1 {
        public static void main(String[] args) {
            Student student = new Student();
            AtomicReferenceFieldUpdater<Student, String> updater = AtomicReferenceFieldUpdater.newUpdater(Student.class, String.class, "name");
            updater.compareAndSet(student, null, "bb");//更新成功返回true
            System.out.println(student);//Student(name=bb)
        }
    }
    
    @Data
    class Student {
        //注意 修饰符不能是private
        volatile String name;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    我们直接使用静态方法构造对象即可,第一个参数是对象类型,第二个参数是属性类型,第三个参数是属性名称。

    但指的我们注意的是,被更新的属性必须要volatile修饰,并且不能是private修饰的,否则会报错,权限提升即可。

    5. 原子累加器

    为什么有原子整型了还要有原子累加器呢?

    因为这些类是jdk8出的,性能更加优越,让我们用代码来比较下吧。

    @Slf4j
    public class Demo1 {
        public static void main(String[] args) throws InterruptedException {
            AtomicInteger ai = new AtomicInteger();
            LongAdder la = new LongAdder();
            for (int i = 0; i < 5; i++) {
                run(ai);
            }
            for (int i = 0; i < 5; i++) {
                run(la);
            }
        }
    
        private static void run(AtomicInteger ai) throws InterruptedException {
            TimeInterval timer = DateUtil.timer();
            CountDownLatch countDownLatch = new CountDownLatch(8);
            for (int i = 0; i < 8; i++) {
                new Thread(() -> {
                    for (int j = 0; j < 1000000; j++) {
                        ai.incrementAndGet();
                    }
                    countDownLatch.countDown();
                }).start();
            }
            countDownLatch.await();
            log.info("AtomicInteger用时:{}", timer.interval());
        }
        private static void run(LongAdder longAdder) throws InterruptedException {
            TimeInterval timer = DateUtil.timer();
            CountDownLatch countDownLatch = new CountDownLatch(8);
            for (int i = 0; i < 8; i++) {
                new Thread(() -> {
                    for (int j = 0; j < 1000000; j++) {
                        longAdder.increment();
                    }
                    countDownLatch.countDown();
                }).start();
            }
            countDownLatch.await();
            log.info("LongAdder用时:{}", timer.interval());
        }
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    做了5次比较,但结果也很明显了,LongAddr越往后运行性能越棒。
    在这里插入图片描述
    而关于LongAddr的原理咱就不剖析了,能力有限,但大致可以讲一下。在并发度比较低的情况下,累加操作与AtomicInteger是类似的,但并发度高了以后,会创建几个cell数组,不同线程将自增的1放入数组中,最终将所有数组和基准值做个汇总得到结果。

    世界上往往很多事情,发生的那一刻并没有多么惊艳,依旧是蓝天白云,人来人往,但回想起那一天,你才知道当时的决定是多么重要,而当时的你,以为仅仅只是普通的一天罢了。

  • 相关阅读:
    Rpc-实现Client对ZooKeeper的服务监听
    多种方法解决leetcode经典题目-LCR 155. 将二叉搜索树转化为排序的双向链表, 同时弄透引用变更带来的bug
    【golang】多个defer的执行顺序以及器相关练习
    波奇学C++:继承
    基于AERMOD模型在大气环境影响评价中的实践技术应用
    【二十三】springboot整合spring事务详解以及实战
    导入JankStats检测卡帧库遇到问题记录
    安全可信 | 首批 天翼云通过可信云安全云工作负载保护平台评估
    K8S之prometheus-operator监控
    Leetcode1971. 寻找图中是否存在路径
  • 原文地址:https://blog.csdn.net/weixin_44353507/article/details/127571387