• J.U.C 系列之Atomic原子类


    一 什么是原子类?

    所谓原子类必然是具有原子性的类,原子性操作--原子操作,百度百科中给的定义如下

    "原子操作(atomic operation)是不需要synchronized",这是Java多线程编程的老生常谈了。所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切[1]  换到另一个线程)。

    顾名思义,原子类就是一个一旦被执行就不能中断的类。

    二 为什么需要原子类?

    在看为什么需要原子类之前,我们看看普通Number类,在处理问题时可能存在的问题,这里我们通过Integer来演示

    public class Main {
    
        private static Integer sum = 0;
    
        public static void main(String[] args) {
            for (int i = 0; i < 20; i++) {
                new Thread(new Task()).start();
            }
    
            Thread.yield();
            System.out.println(sum);
    
        }
    
        static class Task implements Runnable {
            @Override
            public void run() {
                for(int i = 0;i<100;i++){
                    sum++;
                }
            }
        }
    }

    这段代码意思是开20个线程,每个线程对sum自加100次,理论上应该最后输出2000;但是事实上每次都是小于2000;

    这是听说过volatile关键字的小伙伴可能会说,使用volatile来修饰sum,好,我们继续试验

    public class Main {
    
        private static volatile Integer sum = 0;
    
        public static void main(String[] args) {
            for (int i = 0; i < 20; i++) {
                new Thread(new Task()).start();
            }
    
            Thread.yield();
            System.out.println(sum);
    
        }
    
        static class Task implements Runnable {
            @Override
            public void run() {
                for(int i = 0;i<100;i++){
                    sum++;
                }
            }
        }
    }

    这是试验五次的输出

    1766,1616,1859,1980,1800

    还是都是小于2000,这是怎么回事,这里先提一下,volatile只能保证单个操作的原子性,而sum++,包括三个操作

    sum = getSum()  //读取sum
    
    temp = sum +1;  //sum+1,赋给临时变量
    
    sum =  setSum(temp)      //将sum写回

    因此,即使是volatile也无法保证sum++的原子性,volatile只能保证单个操作的原子性,而++操作是复合操作,volatile变量会在后续章节详细讨论;

    那么,在Atomic未出现之前,是如何处理i++在多线程环境下的线程安全问题,主要是通过Synchronize加锁来处理,处理过程复杂,性能低

    JDK5.0之后出现的Java.util.concurrent.Atomic包中为我们提供了13中原子类,来保证单个原子变量复合操作的原子性。下面我们通过AtomicInteger的使用来认识一下原子类

    三 原子类示例详解

    AtomicInteger 字段

       // setup to use Unsafe.compareAndSwapInt for updates
       //这里, unsafe是java提供的获得对对象内存地址访问的类,注释已经清楚的写出了,它的作用就是在更新操作时提供“比较并替换”的作用。实际上就是AtomicInteger中的一个工具。
        private static final Unsafe unsafe = Unsafe.getUnsafe();
       //valueOffset是用来记录value本身在内存的便宜地址的,这个记录,也主要是为了在更新操作在内存中找到value的位置,方便比较。
        private static final long valueOffset;
       //value是用来存储整数的时间变量,这里被声明为volatile,就是为了保证在更新操作时,当前线程可以拿到value最新的值(并发环境下,value可能已经被其他线程更新了)。
        private volatile int value;

    AtomicInteger 构造方法

     /**
         * Creates a new AtomicInteger with the given initial value.
         *
         * @param initialValue the initial value
         */
        public AtomicInteger(int initialValue) {
            value = initialValue;
        }
    
        /**
         * Creates a new AtomicInteger with initial value {@code 0}.
         */
        public AtomicInteger() {
        }

    AtomicInteger 并发安全实现

    那么AtomicInteger是如何实现多线程的自增操作的线程安全的呢?核心思想就是CAS自旋;CAS:Compare And Swap 比较并交换。自旋:通过循环知道预期值和内存之相同,进行CAS操作,AtomicInteger的自增如下所示

           public final int incrementAndGet() {  
               for (;;) {  
                   //这里可以拿到value的最新内存值
                   int current = get();  
                   int next = current + 1;  
                   if (compareAndSet(current, next))  
                       return next;  
               }  
           }  
          
           public final boolean compareAndSet(int expect, int update) {  
               //使用unsafe的native方法,实现高效的硬件级别CAS  
           return unsafe.compareAndSwapInt(this, valueOffset, expect, update);  
           }  

     AtomicInteger 其他常用方法

      /**
         * 返回旧值,然后自增1
         *
         * @return the previous value
         */
        public final int getAndIncrement() {
            return unsafe.getAndAddInt(this, valueOffset, 1);
        }
    
        /**
         * 返回旧值,然后自减1
         *
         * @return the previous value
         */
        public final int getAndDecrement() {
            return unsafe.getAndAddInt(this, valueOffset, -1);
        }
    
        /**
         * 返回旧值,然后  旧值+delta
         *
         * @param delta the value to add
         * @return the previous value
         */
        public final int getAndAdd(int delta) {
            return unsafe.getAndAddInt(this, valueOffset, delta);
        }
    
        /**
         * 先进行自增,返回自增后的值
         *
         * @return the updated value
         */
        public final int incrementAndGet() {
            return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
        }
    
        /**
         * 先进行自减,然后返回自减后的值
         *
         * @return the updated value
         */
        public final int decrementAndGet() {
            return unsafe.getAndAddInt(this, valueOffset, -1) - 1;
        }
    
        /**
         *先在原值上加delta,再返回加之后的值
         *
         * @param delta the value to add
         * @return the updated value
         */
        public final int addAndGet(int delta) {
            return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
        }

    注:其他Atomic类类似,不一一介绍;

    四 Atomic 存在的问题

        》 长时间自旋,导致CPU和资源的占用

      》 只能保证单个原子变量的多线程安全操作,当然可以将多个变量封装成一个类,通过原子引用类型实现

      》 ABA问题;使用AtomicStampedReference 原子更新带有版本号的引用类型解决

  • 相关阅读:
    软考估分
    极限编程(XP)12个最佳实践
    常见符号的英文读法
    又一道信号量的问题--做多了就容易错
    一道信号量前驱图的题目--有技巧
    信号量计算问题--n个进程, 共享3个资源, 当前信号量为-1, 其他进程继续执行P操作, 那么信号量应该继续减
    一道信号量的问题---卖火车票
    一道关于信号量的问题
    一道关于信号量的题目
    C语言int型数据范围
  • 原文地址:https://www.cnblogs.com/zabulon/p/5854868.html
Copyright © 2020-2023  润新知